srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/tests.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-11-30 23:03:00 +0200
committersrdusr <[email protected]>2024-11-30 23:03:00 +0200
commit583063c3094ca0cf9f6cceba92d710b238547f6b (patch)
treea1ff8f181027892f4a45846b67898beb136a9312 /crates/core/src/manager/tests.rs
parent87f5c04b2593dc49409644e69e7962b73d26b9d8 (diff)
downloadsrdwm-583063c3094ca0cf9f6cceba92d710b238547f6b.tar.gz
srdwm-583063c3094ca0cf9f6cceba92d710b238547f6b.zip
Accumulate core-crate additions: capture requests, focus/workspace fixes, test coverage
Bundles several related changes to crates/core built up over this session rather than committed incrementally: - WindowManager::request_capture_workspace/drain_capture_requests (new manager/capture.rs) - backend-agnostic queuing for an off-screen workspace render, see the wayland-side commit for why this exists. - focus_window now switches workspace as a side effect when the target isn't on the current one, matching Hyprland's focuswindow convention (manager/focus.rs). - Assorted window/rules/theme field additions and their test coverage. Left less granular than the repo's usual one-purpose-per-commit convention deliberately: these accumulated across a long session without being committed as they landed, and are too entangled line-by-line to safely split apart now without risking mis-attributing changes to the wrong commit message.
Diffstat (limited to 'crates/core/src/manager/tests.rs')
-rw-r--r--crates/core/src/manager/tests.rs251
1 files changed, 226 insertions, 25 deletions
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index e1518f1..1c80304 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -23,6 +23,30 @@
}
#[test]
+ fn add_window_picks_up_the_configured_default_decoration_mode() {
+ let mut wm = wm_with_monitor();
+ wm.theme.default_decorated = false;
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "a"));
+ assert!(!wm.window(id).unwrap().decorated, "must pick up the live theme default, not Window::new's own hardcoded one");
+ }
+
+ #[test]
+ fn a_rules_decorated_action_still_overrides_the_theme_default() {
+ let mut wm = wm_with_monitor();
+ wm.theme.default_decorated = false;
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { class: Some("nemo".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { decorated: Some(true), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "a");
+ w.app_id = "nemo".into();
+ wm.add_window(w);
+ assert!(wm.window(id).unwrap().decorated, "an explicit rule must still win over the theme-wide default");
+ }
+
+ #[test]
fn tiling_workspace_arranges_two_windows_side_by_side() {
let mut wm = wm_with_monitor();
wm.set_layout(wm.current_workspace(), "tiling");
@@ -144,6 +168,30 @@
}
#[test]
+ fn apply_snap_zone_resizes_to_the_named_zones_rect() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.geometry = Rect::new(50, 50, 300, 200);
+ wm.add_window(w);
+ wm.apply_snap_zone(a, SnapZoneKind::LeftHalf);
+ assert_eq!(wm.window(a).unwrap().geometry, Rect::new(0, 0, 960, 1080));
+ }
+
+ #[test]
+ fn apply_snap_zone_on_a_maximized_window_un_maximizes_it() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ wm.toggle_maximize(a);
+ assert!(wm.window(a).unwrap().maximized);
+ wm.apply_snap_zone(a, SnapZoneKind::TopRightQuarter);
+ let w = wm.window(a).unwrap();
+ assert!(!w.maximized, "snapping a maximized window must clear the maximized flag");
+ assert_eq!(w.geometry, Rect::new(960, 0, 960, 540));
+ }
+
+ #[test]
fn maximize_records_anim_from_when_animations_enabled() {
let mut wm = wm_with_monitor();
let a = wm.alloc_window_id();
@@ -357,6 +405,51 @@
}
#[test]
+ fn focusing_a_window_on_another_workspace_switches_to_it() {
+ // Regression test: `focus_window` used to mark the target focused
+ // without ever touching `current_workspace` - reported live
+ // (relayed from the AGS peer session, measured directly over IPC):
+ // `srd dispatch focus <id>` on a window from a different workspace
+ // left the active workspace unchanged and the newly-"focused"
+ // window `visible: false`, so keyboard input had nowhere visible
+ // to go while whatever was actually on screen kept looking
+ // focused. Reachable by ordinary Alt-Tab, a dock icon, or anything
+ // else that ends up calling `focus_window` on a window that isn't
+ // on the current workspace.
+ let mut wm = wm_with_monitor();
+ let ws2 = wm.add_workspace("2", "dynamic");
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "a"));
+ wm.move_window_to_workspace(id, ws2);
+ assert_eq!(wm.current_workspace(), 0, "sanity: still on the default workspace");
+
+ wm.focus_window(id);
+ assert_eq!(wm.current_workspace(), ws2, "focusing a window must bring its workspace along");
+ assert_eq!(wm.focused_id(), Some(id));
+ }
+
+ #[test]
+ fn refocusing_an_already_visible_window_does_not_trigger_auto_back_and_forth() {
+ // The fix above must not call `switch_workspace` unconditionally --
+ // `switch_workspace`'s own `auto_back_and_forth` handling treats
+ // being asked to "switch" to the *already*-current workspace as a
+ // deliberate toggle-to-previous gesture. An ordinary redundant
+ // `focus_window` call (re-focusing something already focused and
+ // already visible - ordinary mouse click traffic, not a workspace
+ // switch request) must not be misread as that gesture and jump the
+ // user to `previous_workspace` as a surprise side effect.
+ let mut wm = wm_with_monitor();
+ wm.auto_back_and_forth = true;
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "a"));
+
+ wm.focus_window(id);
+ assert_eq!(wm.current_workspace(), ws2, "must stay put - this is not a workspace-switch request");
+ }
+
+ #[test]
fn switching_to_a_nonexistent_workspace_does_not_move_or_touch_previous() {
let mut wm = wm_with_monitor();
let ws2 = wm.add_workspace("2", "dynamic");
@@ -372,6 +465,31 @@
}
#[test]
+ fn output_position_requests_drain_in_arrival_order() {
+ let mut wm = wm_with_monitor();
+ wm.request_output_position(0, 100, 0);
+ wm.request_output_position(1, 0, 0);
+ assert_eq!(wm.drain_output_position_requests(), vec![(0, 100, 0), (1, 0, 0)]);
+ // Draining empties the queue - a second drain with nothing new
+ // queued in between must come back empty, not repeat the same
+ // requests the backend already applied.
+ assert!(wm.drain_output_position_requests().is_empty());
+ }
+
+ #[test]
+ fn a_second_output_position_request_for_the_same_output_replaces_the_first() {
+ // Only the latest requested position for a given output should
+ // survive to the next drain - e.g. a display-settings panel
+ // dragging a monitor preview around fires many requests for the
+ // same output before the user lets go; the backend only needs to
+ // apply where it ended up, not replay the whole drag.
+ let mut wm = wm_with_monitor();
+ wm.request_output_position(0, 100, 0);
+ wm.request_output_position(0, 200, 50);
+ assert_eq!(wm.drain_output_position_requests(), vec![(0, 200, 50)]);
+ }
+
+ #[test]
fn rename_workspace_is_a_no_op_for_an_id_that_does_not_exist() {
let mut wm = wm_with_monitor();
wm.rename_workspace(9999, "ghost");
@@ -419,6 +537,29 @@
}
#[test]
+ fn scratchpad_show_brings_it_back_even_when_minimized_through_a_different_path() {
+ // A scratchpad window can be minimized several ways besides the
+ // `scratchpad_show` toggle-off branch itself - a titlebar minimize
+ // button, a client's own `minimize_request` (both ultimately call
+ // this same `minimize_window`). `scratchpad` is pool membership,
+ // tracked independently of *how* the window ended up minimized, so
+ // pressing the scratchpad binding afterward must still find and
+ // show it - not treat it as "already handled" just because
+ // something other than `scratchpad_show` did the hiding.
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "term"));
+ wm.scratchpad_add(a);
+ wm.scratchpad_show(); // shown + focused
+ wm.minimize_window(a); // hidden via the generic path, not the toggle
+ assert!(wm.window(a).unwrap().scratchpad, "must still be pool-managed after an ordinary minimize");
+ wm.scratchpad_show();
+ let w = wm.window(a).unwrap();
+ assert!(!w.minimized, "the binding must show it again, not treat it as already visible");
+ assert_eq!(wm.focused_id(), Some(a));
+ }
+
+ #[test]
fn scratchpad_show_moves_the_window_onto_the_current_workspace() {
let mut wm = wm_with_monitor();
let a = wm.alloc_window_id();
@@ -630,8 +771,14 @@
let id = wm.alloc_window_id();
let mut w = Window::new(id, "firefox");
w.geometry = Rect::new(100, 100, 400, 300);
- w.decorated = false;
wm.add_window(w);
+ // Set after `add_window`, not before - `add_window` now applies
+ // `theme.default_decorated` unconditionally (same as `corner_radius`/
+ // `border_color` already did), matching how a real client's
+ // negotiated CSD mode actually lands in production too:
+ // `set_decorated_from_mode` runs against an already-added window,
+ // never folded into the `Window` passed into `add_window` itself.
+ wm.window_mut(id).unwrap().decorated = false;
wm.toggle_fullscreen(id);
assert!(!wm.window(id).unwrap().decorated, "fullscreen itself must still drop the titlebar");
@@ -647,6 +794,22 @@
fn monitor_with_dock() -> Monitor {
let mut m = Monitor::new(0, "primary", Rect::new(0, 0, 1920, 1020));
m.full_geometry = Rect::new(0, 0, 1920, 1080);
+ // No top bar in this fixture - maximize ignores the dock the same
+ // way fullscreen does, so it's the same rect as `full_geometry`.
+ m.maximize_geometry = Rect::new(0, 0, 1920, 1080);
+ m.primary = true;
+ m
+ }
+
+ /// A monitor with *both* a bottom dock's exclusive zone and a top bar's,
+ /// distinguishing `maximize_geometry` (stops at the bar, ignores the
+ /// dock) from `full_geometry` (ignores both) and `geometry` (stops at
+ /// both) - `monitor_with_dock` alone can't tell these apart since it
+ /// has no bar to stop at.
+ fn monitor_with_dock_and_bar() -> Monitor {
+ let mut m = Monitor::new(0, "primary", Rect::new(0, 34, 1920, 986));
+ m.full_geometry = Rect::new(0, 0, 1920, 1080);
+ m.maximize_geometry = Rect::new(0, 34, 1920, 1046);
m.primary = true;
m
}
@@ -654,12 +817,14 @@
#[test]
fn fullscreen_covers_the_full_monitor_ignoring_a_dock_reservation() {
// Regression test: fullscreen used to target `Monitor::geometry`
- // (the usable, exclusive-zone-shrunk area), the same field maximize
- // correctly uses - so a fullscreened window stopped short of a
- // dock's reserved strip instead of covering (or going under) it
- // like fullscreen does everywhere else. `full_geometry` is what
- // fixes that; `geometry` must stay untouched so maximize keeps
- // respecting the dock.
+ // (the usable, exclusive-zone-shrunk area) - so a fullscreened
+ // window stopped short of a dock's reserved strip instead of
+ // covering (or going under) it like fullscreen does everywhere
+ // else. `full_geometry` is what fixes that. `toggle_maximize` now
+ // targets the same rect (see `maximize_also_covers_the_full_monitor_
+ // ignoring_a_dock_reservation` below) - on the user's own request,
+ // not a bug fix - so this is no longer the one place `full_geometry`
+ // matters, just the first.
let mut wm = WindowManager::new();
wm.set_monitors(vec![monitor_with_dock()]);
let id = wm.alloc_window_id();
@@ -670,42 +835,78 @@
}
#[test]
- fn maximize_still_respects_the_dock_reservation() {
+ fn maximize_also_covers_the_full_monitor_ignoring_a_dock_reservation() {
+ // `toggle_maximize` used to target `Monitor::geometry` (the usable,
+ // exclusive-zone-shrunk area), deliberately different from
+ // fullscreen's `full_geometry` - several desktops' convention of a
+ // maximized window stopping short of a persistent dock. Changed on
+ // the user's own request ("maximize should still go past dock
+ // area/no dock in that mode"): maximize now covers the same full
+ // rect fullscreen does, the only remaining difference being
+ // `decorated`. A layer-shell client with its own overlap-based
+ // auto-hide (AGS's dock) can react to the window now genuinely
+ // overlapping its band - nothing here forces the dock/bar to hide.
let mut wm = WindowManager::new();
wm.set_monitors(vec![monitor_with_dock()]);
let id = wm.alloc_window_id();
wm.add_window(Window::new(id, "a"));
wm.toggle_maximize(id);
- assert_eq!(wm.window(id).unwrap().geometry, Rect::new(0, 0, 1920, 1020), "maximize must still stop at the dock, unlike fullscreen");
+ assert_eq!(wm.window(id).unwrap().geometry, Rect::new(0, 0, 1920, 1080), "maximize must reach the true monitor edge, past the dock, same as fullscreen");
}
#[test]
- fn maximized_window_grows_when_the_dock_drops_its_reservation_live() {
- // Regression test: a dock that hides/reduces its exclusive zone
- // while a window is already maximized (an auto-hide dock reacting
- // to monocle/maximize, exactly the scenario an AGS peer session hit
- // live) used to leave that window stuck at its stale, dock-shrunk
- // size - `set_monitors` updated `Monitor::geometry` correctly but
- // never touched already-maximized/fullscreen windows' `geometry`,
- // so nothing re-grew until the window was manually un-maximized and
- // re-maximized.
+ fn maximize_covers_a_dock_but_still_stops_at_a_top_bar() {
+ // Live-tested regression: making maximize target `full_geometry`
+ // (the test above) fixed "maximize stops at the dock" but as a side
+ // effect also let it extend behind a top bar's reserved strip,
+ // which was never asked for and was reported back once the user
+ // actually tried it. `maximize_geometry` is the fix - distinct
+ // from both `geometry` (stops at everything) and `full_geometry`
+ // (stops at nothing).
+ let mut wm = WindowManager::new();
+ wm.set_monitors(vec![monitor_with_dock_and_bar()]);
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "a"));
+
+ wm.toggle_maximize(id);
+ assert_eq!(
+ wm.window(id).unwrap().geometry,
+ Rect::new(0, 34, 1920, 1046),
+ "maximize must cover the dock's strip but still stop at the top bar's"
+ );
+ }
+
+ #[test]
+ fn maximized_window_live_tracks_a_monitor_geometry_change() {
+ // Regression test: `set_monitors` updated `Monitor::geometry`/
+ // `full_geometry` correctly but never touched already-maximized/
+ // fullscreen windows' own `geometry`, so an already-maximized
+ // window stayed stuck at its stale size until manually
+ // un-maximized and re-maximized - reported live as "maximize does
+ // not extend past the dock" even after the dock's own zone change
+ // (or, now, monitor resize/reconnect) had already taken effect in
+ // every other respect.
let mut wm = WindowManager::new();
wm.set_monitors(vec![monitor_with_dock()]);
let id = wm.alloc_window_id();
wm.add_window(Window::new(id, "a"));
wm.toggle_maximize(id);
- assert_eq!(wm.window(id).unwrap().geometry, Rect::new(0, 0, 1920, 1020));
+ assert_eq!(wm.window(id).unwrap().geometry, Rect::new(0, 0, 1920, 1080));
- // The dock drops its exclusive zone to 0.
- let mut freed = Monitor::new(0, "primary", Rect::new(0, 0, 1920, 1080));
- freed.full_geometry = Rect::new(0, 0, 1920, 1080);
- freed.primary = true;
- wm.set_monitors(vec![freed]);
+ // The monitor's real geometry changes (a resize, a reconnect at a
+ // different resolution - the same code path a dock dropping its
+ // exclusive zone used to exercise before maximize stopped
+ // respecting that zone at all).
+ let mut resized = Monitor::new(0, "primary", Rect::new(0, 0, 2560, 1420));
+ resized.full_geometry = Rect::new(0, 0, 2560, 1440);
+ resized.maximize_geometry = Rect::new(0, 0, 2560, 1440);
+ resized.primary = true;
+ wm.set_monitors(vec![resized]);
assert_eq!(
wm.window(id).unwrap().geometry,
- Rect::new(0, 0, 1920, 1080),
+ Rect::new(0, 0, 2560, 1440),
"an already-maximized window must live-track a monitor geometry change, not just windows placed afterward"
);
}