srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-01-31 14:34:00 +0200
committersrdusr <[email protected]>2026-01-31 14:34:00 +0200
commit1f708f8aa09bf8bbce82314a76b7f34def90b798 (patch)
tree3ca6468dc0507f563147cbeb4d8b7e9a40949921 /crates/core/src
parent76031dd8809701e39d3709451809851ad063bf73 (diff)
downloadsrdwm-1f708f8aa09bf8bbce82314a76b7f34def90b798.tar.gz
srdwm-1f708f8aa09bf8bbce82314a76b7f34def90b798.zip
Fix desktop-icon deselection and workspace-teleport-on-close; document a shadow limit
Desktop icons stayed highlighted after clicking a window: select_desktop_icon(None) was only ever called from start_desktop_marquee, never from the one place every focus path (click, Alt-Tab, dock IPC, scratchpad show, snap flyout) already funnels through. Added the deselect there instead of per-caller. Closing a focused window could silently switch the user's active workspace: remove_window's fallback picked self.order.last(), but that list is global, not per-workspace, so it could land on a background window elsewhere - and focus_window already switches workspace to match whatever it's given (a real, separate feature for a deliberate srd dispatch focus). Fixed by preferring a same-workspace window first. New general.close_focus_follows_workspace (default false, live-settable) controls what happens only when nothing is left on the current workspace at all: off leaves focus at nothing, matching Windows/GNOME/macOS; on restores the old always-follow-the-global-fallback behaviour. Three new tests. Also documented, not fixed: shadows can still bleed onto a neighbouring *monitor* near a multi-output seam (shadow_rect has no monitor-boundary awareness), found via a live cross-monitor screenshot. Moot for this session since general.shadows is already off in the live config, but a real, open gap for anyone who re-enables shadows on a multi-monitor setup.
Diffstat (limited to 'crates/core/src')
-rw-r--r--crates/core/src/manager/mod.rs19
-rw-r--r--crates/core/src/manager/tests.rs66
-rw-r--r--crates/core/src/manager/windows.rs19
3 files changed, 103 insertions, 1 deletions
diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs
index d449486..9265885 100644
--- a/crates/core/src/manager/mod.rs
+++ b/crates/core/src/manager/mod.rs
@@ -241,6 +241,24 @@ pub struct WindowManager {
/// see the Wayland backend's shadow render call site - so this only
/// ever turns it off entirely, not on for those.
pub shadows_enabled: bool,
+ /// Whether closing your own focused window is allowed to fall back to
+ /// a window on a *different* workspace and switch you there to follow
+ /// it, when nothing else is left on your current one. Read from
+ /// `general.close_focus_follows_workspace`. `false` (the default)
+ /// matches every mainstream desktop (Windows/GNOME/macOS never change
+ /// your active workspace just because a window closed) - `remove_
+ /// window`'s own fallback then only ever considers a same-workspace
+ /// window, leaving focus at `None` if there isn't one, rather than
+ /// picking whatever window was next in the *global* most-recently-
+ /// focused order regardless of which workspace it happens to be on.
+ /// Reported live as windows closing and "teleporting" the user to a
+ /// previous workspace - exactly this: the global fallback picking a
+ /// background window elsewhere, then `focus_window`'s own (separate,
+ /// correct, and unrelated to this setting) "switch workspace to match
+ /// the newly focused window" side effect following it there. `true`
+ /// restores that original always-follow behaviour for anyone who
+ /// wants it.
+ pub close_focus_follows_workspace: bool,
/// Width, in pixels, of the resize grab band along a window's edges,
/// read from `general.resize_margin`. See [`crate::window::RESIZE_MARGIN`]'s
/// doc comment for the default and why it's what it is.
@@ -548,6 +566,7 @@ impl WindowManager {
animations_enabled: true,
animation_duration_ms: 200,
shadows_enabled: true,
+ close_focus_follows_workspace: false,
resize_margin: RESIZE_MARGIN,
rounded_corners_enabled: None,
gpu_enabled: false,
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index f703f25..6a3ba3f 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -11,6 +11,72 @@
}
#[test]
+ fn closing_the_focused_window_prefers_a_same_workspace_fallback_over_a_more_recent_global_one() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a")); // workspace 1
+
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ let c = wm.alloc_window_id();
+ wm.add_window(Window::new(c, "c")); // workspace 2, now globally most-recent
+
+ wm.switch_workspace(1);
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b")); // workspace 1, now focused and globally most-recent
+
+ assert_eq!(wm.current_workspace(), 1);
+ wm.remove_window(b);
+ // The naive "global most recent" fallback would have landed on `c`
+ // (workspace 2) here - `a`, still on the workspace the user is
+ // actually looking at, is what a real desktop would land on.
+ assert_eq!(wm.focused_id(), Some(a));
+ assert_eq!(wm.current_workspace(), 1, "must not have been dragged onto workspace 2 by the fallback");
+ }
+
+ #[test]
+ fn closing_the_last_window_on_a_workspace_leaves_focus_none_by_default() {
+ let mut wm = wm_with_monitor();
+ assert!(!wm.close_focus_follows_workspace, "default must be off, matching every mainstream desktop");
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a")); // workspace 1
+
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ let c = wm.alloc_window_id();
+ wm.add_window(Window::new(c, "c")); // workspace 2
+
+ wm.switch_workspace(1);
+ wm.focus_window(a); // re-focus `a`; current_workspace stays 1 (already there)
+ assert_eq!(wm.current_workspace(), 1);
+
+ wm.remove_window(a);
+ // No window left on workspace 1 at all - with the setting off,
+ // this must not silently jump the user over to `c` on workspace 2.
+ assert_eq!(wm.focused_id(), None);
+ assert_eq!(wm.current_workspace(), 1);
+ }
+
+ #[test]
+ fn closing_the_last_window_on_a_workspace_can_still_follow_when_opted_in() {
+ let mut wm = wm_with_monitor();
+ wm.close_focus_follows_workspace = true;
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a")); // workspace 1
+
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ let c = wm.alloc_window_id();
+ wm.add_window(Window::new(c, "c")); // workspace 2
+
+ wm.switch_workspace(1);
+ wm.focus_window(a);
+ wm.remove_window(a);
+ // Opted in: the old always-follow-the-global-fallback behaviour.
+ assert_eq!(wm.focused_id(), Some(c));
+ }
+
+ #[test]
fn new_window_on_dynamic_workspace_uses_smart_placement() {
let mut wm = wm_with_monitor();
let id = wm.alloc_window_id();
diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs
index f29d19a..3271dee 100644
--- a/crates/core/src/manager/windows.rs
+++ b/crates/core/src/manager/windows.rs
@@ -271,7 +271,24 @@ impl WindowManager {
pub fn remove_window(&mut self, id: WindowId) -> Option<Window> {
self.order.retain(|&w| w != id);
if self.focused == Some(id) {
- self.focused = self.order.last().copied();
+ // Prefer the most-recently-focused window still on *this*
+ // workspace - `self.order` tracks every window globally, not
+ // per-workspace, so its own `.last()` (the previous behaviour)
+ // could just as easily be a background window sitting on a
+ // workspace the user isn't even looking at. `focus_window`
+ // switches the active workspace to match whatever it's given
+ // (a real, separate, and correct feature for a deliberate
+ // `srd dispatch focus` from elsewhere) - so handing it a
+ // cross-workspace fallback here silently dragged the user's
+ // whole view along with it the moment they closed a window,
+ // reported live as "teleports me to previous workspace".
+ // `close_focus_follows_workspace` (default `false`, matching
+ // every mainstream desktop - none of them change your active
+ // workspace just because a window closed) restores that
+ // original always-follow behaviour for anyone who wants it.
+ let current = self.current_workspace;
+ let same_workspace = self.order.iter().rev().copied().find(|&w| self.windows.get(&w).is_some_and(|win| win.workspace == current));
+ self.focused = same_workspace.or_else(|| self.close_focus_follows_workspace.then(|| self.order.last().copied()).flatten());
}
let window = self.windows.remove(&id);
// Remembers wherever this app's window actually ended up, not just