srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/workspaces.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/core/src/manager/workspaces.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz
srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
Diffstat (limited to 'crates/core/src/manager/workspaces.rs')
-rw-r--r--crates/core/src/manager/workspaces.rs115
1 files changed, 106 insertions, 9 deletions
diff --git a/crates/core/src/manager/workspaces.rs b/crates/core/src/manager/workspaces.rs
index 6ffb21b..f502d6b 100644
--- a/crates/core/src/manager/workspaces.rs
+++ b/crates/core/src/manager/workspaces.rs
@@ -30,7 +30,12 @@ impl WindowManager {
if self.workspaces.len() <= 1 {
return;
}
- let fallback = self.workspaces.iter().map(|w| w.id).find(|&w| w != id).unwrap_or(0);
+ // `unwrap_or(1)` is unreachable in practice - the `len() <= 1`
+ // guard above means `find` always has at least one other workspace
+ // to return - but `1`, not `0`, since workspace ids are 1-based
+ // (see `WindowManager::new`'s own doc comment) and `0` is no longer
+ // a real workspace id this could plausibly fall back to.
+ let fallback = self.workspaces.iter().map(|w| w.id).find(|&w| w != id).unwrap_or(1);
for w in self.windows.values_mut().filter(|w| w.workspace == id) {
w.workspace = fallback;
}
@@ -53,6 +58,23 @@ impl WindowManager {
if self.workspaces.iter().any(|w| w.id == target) && target != self.current_workspace {
self.previous_workspace = self.current_workspace;
self.current_workspace = target;
+ // Keyboard focus otherwise stayed on whatever was focused
+ // *before* the switch - this function only ever touched
+ // `current_workspace`, never `self.focused` - so real input
+ // kept going to a window that had just gone invisible while
+ // whatever's now on screen, if anything, received nothing.
+ // Reported live: switching to a workspace with an open window
+ // left that window unfocused and the previous workspace's
+ // window still receiving keystrokes. Only reassigns focus when
+ // the currently-focused window isn't actually on the new
+ // workspace - an already-correct focus (e.g. `focus_window`'s
+ // own workspace-follow call into this function, where the
+ // target window IS what should end up focused) must not get
+ // silently overridden by "pick the topmost window instead".
+ let focus_still_valid = self.focused.and_then(|id| self.windows.get(&id)).is_some_and(|w| w.workspace == target);
+ if !focus_still_valid {
+ self.focused = self.window_ids_on_workspace_front_to_back(target).into_iter().next();
+ }
}
}
@@ -60,6 +82,76 @@ impl WindowManager {
self.current_workspace
}
+ /// The workspace actually showing on `monitor` right now - `current_
+ /// workspace` directly when `per_monitor_workspaces` is `false` (every
+ /// monitor always agrees, by construction, since only `switch_
+ /// workspace` - never `switch_workspace_on_monitor` - can run in that
+ /// mode); otherwise this monitor's own independently-switched
+ /// workspace, or `current_workspace` as the fallback for a monitor
+ /// that has never had one switched independently yet (freshly
+ /// connected, or the mode was just turned on).
+ pub fn workspace_for_monitor(&self, monitor: MonitorId) -> WorkspaceId {
+ if self.per_monitor_workspaces {
+ self.monitor_workspaces.get(&monitor).copied().unwrap_or(self.current_workspace)
+ } else {
+ self.current_workspace
+ }
+ }
+
+ /// Whether `id` is showing on *any* currently-connected monitor right
+ /// now - what `srd workspaces`/AGS's own workspace pills should treat
+ /// as "active" (`crates/platform/src/ipc.rs::workspace_snapshot`).
+ /// Structurally allows more than one workspace to be active at once,
+ /// which only actually happens in `per_monitor_workspaces` mode with
+ /// two monitors on different workspaces - shared mode (the default)
+ /// always has exactly one, same as before this existed.
+ pub fn is_workspace_visible(&self, id: WorkspaceId) -> bool {
+ if self.per_monitor_workspaces {
+ self.monitors.iter().any(|m| self.workspace_for_monitor(m.id) == id)
+ } else {
+ id == self.current_workspace
+ }
+ }
+
+ /// The `per_monitor_workspaces`-aware counterpart to `switch_
+ /// workspace`: switches `monitor`'s own workspace to `id` without
+ /// affecting any other monitor, when the mode is on. Falls straight
+ /// through to the ordinary shared-mode `switch_workspace` (ignoring
+ /// `monitor` entirely) when it's off, so a caller can always use this
+ /// one entry point regardless of which mode is active rather than
+ /// branching on the config flag itself - see its own call site in
+ /// `crates/platform/src/ipc.rs`'s `activate_workspace` handler.
+ ///
+ /// `monitor` is "whichever monitor this switch should apply to", not
+ /// necessarily where the pointer is - the caller decides that (the
+ /// focused window's own monitor, in practice), same as real per-output
+ /// keybinding routing in Hyprland/niri.
+ pub fn switch_workspace_on_monitor(&mut self, id: WorkspaceId, monitor: MonitorId) {
+ if !self.per_monitor_workspaces {
+ self.switch_workspace(id);
+ return;
+ }
+ let current = self.workspace_for_monitor(monitor);
+ let target = if self.auto_back_and_forth && id == current {
+ self.monitor_workspaces.get(&monitor).copied().unwrap_or(self.previous_workspace)
+ } else {
+ id
+ };
+ if !self.workspaces.iter().any(|w| w.id == target) || target == current {
+ return;
+ }
+ self.previous_workspace = current;
+ self.monitor_workspaces.insert(monitor, target);
+ // Same reasoning as `switch_workspace`'s own matching comment:
+ // reassign focus only when the currently-focused window isn't
+ // already correctly on the new workspace, so an already-correct
+ // focus assignment from elsewhere doesn't get silently overridden.
+ let focus_still_valid = self.focused.and_then(|id| self.windows.get(&id)).is_some_and(|w| w.workspace == target);
+ if !focus_still_valid {
+ self.focused = self.window_ids_on_workspace_front_to_back(target).into_iter().next();
+ }
+ }
+
pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> {
self.workspaces.iter().find(|w| w.id == id)
}
@@ -74,15 +166,20 @@ impl WindowManager {
}
}
- /// Windows that should currently be shown to the user: those on the
- /// current workspace, and not minimized.
+ /// Windows that should currently be shown to the user: those on
+ /// whichever workspace their own monitor is currently showing, and not
+ /// minimized.
///
- /// `current_workspace` is a single value shared by every monitor --
- /// srdwm does not have Hyprland-style independent per-monitor
- /// workspaces, so switching workspace changes what's shown on every
- /// screen at once. `w.monitor` plays no part in this filter at all.
+ /// In shared mode (`per_monitor_workspaces` off, the default) every
+ /// monitor is always showing `current_workspace`, so this reduces to
+ /// exactly the original single-shared-workspace filter and `w.monitor`
+ /// plays no part in it - switching workspace still changes what's
+ /// shown on every screen at once. In per-monitor mode, each window is
+ /// checked against its *own* monitor's independently-switched
+ /// workspace (`workspace_for_monitor`) instead, so two monitors on two
+ /// different workspaces each correctly show only their own.
pub fn visible_windows(&self) -> impl Iterator<Item = &Window> {
- self.windows.values().filter(|w| w.workspace == self.current_workspace && !w.minimized)
+ self.windows.values().filter(|w| w.workspace == self.workspace_for_monitor(w.monitor) && !w.minimized)
}
/// Same windows as [`Self::visible_windows`], but in real front-to-back
@@ -95,7 +192,7 @@ impl WindowManager {
/// `self.order` reversed is the same "topmost first" convention
/// `hit_test`/`window_at` already use.
pub fn visible_windows_front_to_back(&self) -> impl Iterator<Item = &Window> {
- self.order.iter().rev().filter_map(|id| self.windows.get(id)).filter(|w| w.workspace == self.current_workspace && !w.minimized)
+ self.order.iter().rev().filter_map(|id| self.windows.get(id)).filter(|w| w.workspace == self.workspace_for_monitor(w.monitor) && !w.minimized)
}
}