From ae712b6b40c918c2477f6eac4d176860c6d32a14 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Tue, 29 Sep 2026 21:29:58 +0100 Subject: [PATCH] Keep workspace presentation in numeric order --- compositor/src/input.rs | 19 ++++----------- compositor/src/ipc.rs | 6 ++--- compositor/src/overview.rs | 6 ++--- compositor/src/shell_control.rs | 11 +++------ crates/ferese-core/src/lib.rs | 2 +- crates/ferese-core/src/workspace.rs | 38 +++++++++++++++++++++++++++++ shell/ferese-shell/src/control.rs | 6 +++++ 7 files changed, 59 insertions(+), 29 deletions(-) diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 7cd4c93..011a73d 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -602,20 +602,11 @@ impl Ferese { let Some(current) = self.output_workspaces.active_workspace(output) else { return; }; - let mut candidates = self - .workspaces - .iter() - .filter(|workspace| { - self.output_workspaces - .output_for_workspace(workspace.id) - .is_none_or(|owner| owner == output) - }) - .collect::>(); - candidates.sort_by_key(|workspace| { - ( - workspace.name.parse::().unwrap_or(u32::MAX), - workspace.id.0, - ) + let mut candidates = self.workspaces.ordered(); + candidates.retain(|workspace| { + self.output_workspaces + .output_for_workspace(workspace.id) + .is_none_or(|owner| owner == output) }); let Some(index) = candidates .iter() diff --git a/compositor/src/ipc.rs b/compositor/src/ipc.rs index 14918f1..49d80c6 100644 --- a/compositor/src/ipc.rs +++ b/compositor/src/ipc.rs @@ -507,9 +507,10 @@ impl Ferese { fn workspaces_json(&self) -> Value { let active = self.workspaces.active_id(); - let mut workspaces = self + let workspaces = self .workspaces - .iter() + .ordered() + .into_iter() .map(|workspace| { let mode = match workspace.layout.mode() { LayoutMode::Scrolling => "scrolling", @@ -525,7 +526,6 @@ impl Ferese { }) }) .collect::>(); - workspaces.sort_by_key(|workspace| workspace["id"].as_u64()); Value::Array(workspaces) } diff --git a/compositor/src/overview.rs b/compositor/src/overview.rs index f7eb51e..8d0859f 100644 --- a/compositor/src/overview.rs +++ b/compositor/src/overview.rs @@ -650,8 +650,7 @@ impl Ferese { let selected = self.output_workspaces.active_workspace(output_id); let strip = workspace_strip(bounds); let capacity = strip_capacity(strip); - let mut workspaces = self.workspaces.iter().collect::>(); - workspaces.sort_by_key(|workspace| workspace.name.parse::().unwrap_or(workspace.id.0)); + let workspaces = self.workspaces.ordered(); let selected_index = workspaces .iter() .position(|workspace| Some(workspace.id) == selected) @@ -770,8 +769,7 @@ impl Ferese { }; let capacity = strip_capacity(workspace_strip(bounds)); let cards = self.overview_workspace_cards(&output); - let mut ordered = self.workspaces.iter().collect::>(); - ordered.sort_by_key(|workspace| workspace.name.parse::().unwrap_or(workspace.id.0)); + let ordered = self.workspaces.ordered(); let current = cards .first() .and_then(|card| { diff --git a/compositor/src/shell_control.rs b/compositor/src/shell_control.rs index d80fdd0..9444c9f 100644 --- a/compositor/src/shell_control.rs +++ b/compositor/src/shell_control.rs @@ -215,9 +215,9 @@ impl Ferese { } fn workspace_snapshots(&self) -> Vec { - let mut workspaces = self - .workspaces - .iter() + self.workspaces + .ordered() + .into_iter() .map(|workspace| { let output = self.output_workspaces.output_for_workspace(workspace.id); let active = output.is_some_and(|output| { @@ -231,10 +231,7 @@ impl Ferese { active, } }) - .collect::>(); - - workspaces.sort_by_key(|workspace| workspace.id.0); - workspaces + .collect() } fn managed_window_snapshots(&self) -> Vec { diff --git a/crates/ferese-core/src/lib.rs b/crates/ferese-core/src/lib.rs index 6b8851a..bab96a0 100644 --- a/crates/ferese-core/src/lib.rs +++ b/crates/ferese-core/src/lib.rs @@ -6,5 +6,5 @@ mod workspace; pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch}; pub use workspace::{ LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, - WorkspaceSet, + WorkspaceSet, workspace_order_key, }; diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index 3db091c..75056a4 100644 --- a/crates/ferese-core/src/workspace.rs +++ b/crates/ferese-core/src/workspace.rs @@ -10,6 +10,10 @@ use ferese_layout::{ #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct WorkspaceId(pub u64); +pub fn workspace_order_key(name: &str, id: WorkspaceId) -> (u64, u64) { + (name.parse::().unwrap_or(u64::MAX), id.0) +} + #[derive(Debug)] pub struct Workspace { pub id: WorkspaceId, @@ -537,6 +541,13 @@ impl WorkspaceSet { self.workspaces.values() } + pub fn ordered(&self) -> Vec<&Workspace> { + let mut workspaces = self.iter().collect::>(); + workspaces + .sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id)); + workspaces + } + pub fn prune_empty(&mut self, protected: &HashSet) -> Vec { let mut empty = self .workspaces @@ -1134,6 +1145,33 @@ fn finite_or_zero(value: f64) -> f64 { #[cfg(test)] mod tests { + #[test] + fn numeric_order_survives_pruning_and_recreation() { + let mut set = super::WorkspaceSet::default(); + let original_first = set.active_id(); + let second = set.ensure_numeric(2).unwrap(); + set.activate(second).unwrap(); + assert_eq!( + set.prune_empty(&std::collections::HashSet::new()), + vec![original_first] + ); + + let tenth = set.ensure_numeric(10).unwrap(); + let first = set.ensure_numeric(1).unwrap(); + assert_ne!(first, original_first); + assert!(first.0 > tenth.0); + assert_eq!( + set.ordered() + .into_iter() + .map(|workspace| (workspace.name.as_str(), workspace.id)) + .collect::>(), + vec![("1", first), ("2", second), ("10", tenth)] + ); + set.activate(first).unwrap(); + assert_eq!(set.active().name, "1"); + assert!(set.validate().is_ok()); + } + #[test] fn pruning_keeps_windows_and_one_empty_workspace() { let mut set = super::WorkspaceSet::default(); diff --git a/shell/ferese-shell/src/control.rs b/shell/ferese-shell/src/control.rs index 5f26fef..4380df9 100644 --- a/shell/ferese-shell/src/control.rs +++ b/shell/ferese-shell/src/control.rs @@ -272,6 +272,12 @@ impl Dispatch for ControlState { ferese_shell_v1::Event::SnapshotEnd { serial } if state.serial.take() == Some(serial) => { + state.pending.workspaces.sort_unstable_by_key(|workspace| { + ferese_core::workspace_order_key( + &workspace.name, + ferese_core::WorkspaceId(workspace.id), + ) + }); let _ = state .sender .send(ControlUpdate::Snapshot(state.pending.clone()));