srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/tests.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-07-11 14:17:00 +0200
committersrdusr <[email protected]>2024-07-11 14:17:00 +0200
commit86b557a6c0baa4146b30712d8b7633f1b9b46d43 (patch)
tree96ab749c728ac11c2851ea361c6fda63990e588f /crates/core/src/manager/tests.rs
parent68b645b43e19330474ff106ce5c88dab520d7417 (diff)
downloadsrdwm-86b557a6c0baa4146b30712d8b7633f1b9b46d43.tar.gz
srdwm-86b557a6c0baa4146b30712d8b7633f1b9b46d43.zip
Split crates/core/src/manager.rs (2048 lines) into manager/
Pure reorganization, no behavior change - verified by diffing the function-name set before/after (identical 130 functions) plus a full cargo test pass. WindowManager's struct/field definitions, Default, new(), and the three trivial constructors (add_rule/register_layout/ available_layouts) stay in mod.rs; the rest of the single ~950-line impl block is split into one file per the section comments the file already had (monitors, windows, focus, winops, hittest, dragresize, workspaces, layout). Three methods called across section boundaries (monitor_for, windows_on_workspace, cycle_focus) went from private to pub(super) - Rust's privacy model doesn't let sibling submodules see each other's private items, only a defining module's own descendants. The ~1000-line test module moves to manager/tests.rs unsplit: its helpers (wm_with_monitor, two_monitors, monitor_with_dock) are shared across tests for every section, so splitting further would mean duplicating them or adding another shared-support file for little benefit.
Diffstat (limited to 'crates/core/src/manager/tests.rs')
-rw-r--r--crates/core/src/manager/tests.rs992
1 files changed, 992 insertions, 0 deletions
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
new file mode 100644
index 0000000..a098245
--- /dev/null
+++ b/crates/core/src/manager/tests.rs
@@ -0,0 +1,992 @@
+ use super::*;
+
+ fn wm_with_monitor() -> WindowManager {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(vec![{
+ let mut m = Monitor::new(0, "primary", Rect::new(0, 0, 1920, 1080));
+ m.primary = true;
+ m
+ }]);
+ wm
+ }
+
+ #[test]
+ fn new_window_on_dynamic_workspace_uses_smart_placement() {
+ let mut wm = wm_with_monitor();
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "first");
+ w.geometry = Rect::new(0, 0, 400, 300);
+ wm.add_window(w);
+ let placed = wm.window(id).unwrap().geometry;
+ // Grid placement starts at grid_margin, not (0,0).
+ assert_eq!(placed.x, wm.placement.grid_margin as i32);
+ }
+
+ #[test]
+ fn tiling_workspace_arranges_two_windows_side_by_side() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+
+ wm.arrange_workspace(wm.current_workspace());
+ let ra = wm.window(a).unwrap().geometry;
+ let rb = wm.window(b).unwrap().geometry;
+ assert!(!ra.overlaps(&rb));
+ assert_eq!(ra.y, rb.y);
+ assert!(ra.x < rb.x);
+ }
+
+ #[test]
+ fn floating_window_is_skipped_by_tiling_arrange() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ wm.toggle_floating(a);
+ let before = wm.window(a).unwrap().geometry;
+ wm.arrange_workspace(wm.current_workspace());
+ assert_eq!(wm.window(a).unwrap().geometry, before);
+ }
+
+ #[test]
+ fn focus_cycles_forward_and_wraps() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+ // `b` was added last, so it's focused.
+ assert_eq!(wm.focused_id(), Some(b));
+ wm.focus_next();
+ assert_eq!(wm.focused_id(), Some(a));
+ wm.focus_next();
+ assert_eq!(wm.focused_id(), Some(b));
+ }
+
+ #[test]
+ fn minimized_window_is_skipped_by_focus_cycling() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+ wm.minimize_window(a);
+ wm.focus_window(b);
+ wm.focus_next();
+ assert_eq!(wm.focused_id(), Some(b), "only unminimized window should ever be focused");
+ }
+
+ #[test]
+ fn drag_moves_window_by_pointer_delta() {
+ let mut wm = wm_with_monitor();
+ // "tiling" layout leaves add_window's requested geometry alone;
+ // "dynamic"/"floating" would override it via SmartPlacement, which
+ // these tests aren't exercising.
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.geometry = Rect::new(300, 300, 400, 300);
+ wm.add_window(w);
+ wm.start_drag(a, 310, 310);
+ wm.update_drag(360, 340);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!((g.x, g.y), (350, 330));
+ wm.end_drag();
+ assert!(!wm.is_dragging());
+ }
+
+ #[test]
+ fn drag_ending_near_edge_snaps_to_half_screen() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.geometry = Rect::new(500, 500, 400, 300);
+ wm.add_window(w);
+ wm.start_drag(a, 510, 510);
+ wm.update_drag(15, 510); // drag far left, landing within snap_threshold (8px) of edge 0
+ wm.end_drag();
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!(g, Rect::new(0, 0, 960, 1080));
+ }
+
+ #[test]
+ fn resize_from_bottom_right_grows_size_only() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.geometry = Rect::new(100, 100, 300, 200);
+ wm.add_window(w);
+ wm.start_resize(a, ResizeEdge::BottomRight, 400, 300);
+ wm.update_resize(450, 340);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!(g, Rect::new(100, 100, 350, 240));
+ wm.end_resize();
+ assert!(!wm.is_resizing());
+ }
+
+ #[test]
+ fn toggle_maximize_restores_original_geometry() {
+ 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);
+ let original = wm.window(a).unwrap().geometry;
+ wm.toggle_maximize(a);
+ assert_eq!(wm.window(a).unwrap().geometry, Rect::new(0, 0, 1920, 1080));
+ wm.toggle_maximize(a);
+ assert_eq!(wm.window(a).unwrap().geometry, original);
+ }
+
+ #[test]
+ fn maximize_records_anim_from_when_animations_enabled() {
+ 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);
+ let placed = wm.window(a).unwrap().geometry;
+ wm.toggle_maximize(a);
+ assert_eq!(wm.window(a).unwrap().anim_from, Some(placed));
+ }
+
+ #[test]
+ fn maximize_does_not_record_anim_from_when_animations_disabled() {
+ let mut wm = wm_with_monitor();
+ wm.animations_enabled = false;
+ 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.toggle_maximize(a);
+ assert_eq!(wm.window(a).unwrap().anim_from, None);
+ }
+
+ #[test]
+ fn fullscreen_records_anim_from_covering_the_full_monitor() {
+ 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);
+ let placed = wm.window(a).unwrap().geometry;
+ wm.toggle_fullscreen(a);
+ assert_eq!(wm.window(a).unwrap().anim_from, Some(placed));
+ }
+
+ #[test]
+ fn directional_focus_picks_nearest_window_in_that_direction() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let center = wm.alloc_window_id();
+ let mut wc = Window::new(center, "center");
+ wc.geometry = Rect::new(500, 500, 100, 100);
+ wm.add_window(wc);
+ let left = wm.alloc_window_id();
+ let mut wl = Window::new(left, "left");
+ wl.geometry = Rect::new(0, 500, 100, 100);
+ wm.add_window(wl);
+ let right = wm.alloc_window_id();
+ let mut wr = Window::new(right, "right");
+ wr.geometry = Rect::new(1000, 500, 100, 100);
+ wm.add_window(wr);
+
+ wm.focus_window(center);
+ assert_eq!(wm.focus_direction(Direction::Left), Some(left));
+ assert_eq!(wm.focused_id(), Some(left));
+
+ wm.focus_window(center);
+ assert_eq!(wm.focus_direction(Direction::Right), Some(right));
+ }
+
+ #[test]
+ fn hit_test_prefers_topmost_window() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut wa = Window::new(a, "a");
+ wa.geometry = Rect::new(0, 0, 400, 300);
+ wm.add_window(wa);
+ let b = wm.alloc_window_id();
+ let mut wb = Window::new(b, "b");
+ wb.geometry = Rect::new(0, 0, 400, 300); // fully overlapping, added later -> on top
+ wm.add_window(wb);
+
+ let (hit_id, hit) = wm.hit_test(200, 10).unwrap();
+ assert_eq!(hit_id, b);
+ assert_eq!(hit, TitlebarHit::Drag);
+ }
+
+ #[test]
+ fn moving_window_to_another_workspace_removes_it_from_current() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.move_window_to_workspace(a, ws2);
+ assert_eq!(wm.visible_windows().count(), 0);
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.visible_windows().count(), 1);
+ }
+
+ #[test]
+ fn matching_rule_floats_new_window_on_add() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { title_contains: Some("calculator".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { floating: Some(true), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "Calculator"));
+ assert!(wm.is_floating(id));
+ }
+
+ #[test]
+ fn non_matching_rule_leaves_window_untouched() {
+ let mut wm = wm_with_monitor();
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { title_contains: Some("calculator".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { floating: Some(true), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "Terminal"));
+ assert!(!wm.is_floating(id));
+ }
+
+ #[test]
+ fn rule_assigns_window_to_target_workspace() {
+ let mut wm = wm_with_monitor();
+ let target = wm.add_workspace("scratch", "dynamic");
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { class: Some("scratchpad".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { workspace: Some(target), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "notes");
+ w.app_id = "scratchpad".into();
+ wm.add_window(w);
+ assert_eq!(wm.window(id).unwrap().workspace, target);
+ }
+
+ #[test]
+ fn removing_a_workspace_reassigns_its_windows() {
+ let mut wm = wm_with_monitor();
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ wm.remove_workspace(ws2);
+ assert_ne!(wm.window(a).unwrap().workspace, ws2);
+ assert!(wm.workspace(ws2).is_none());
+ }
+
+ #[test]
+ fn rename_workspace_changes_the_display_name() {
+ let mut wm = wm_with_monitor();
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.rename_workspace(ws2, "code");
+ assert_eq!(wm.workspace(ws2).unwrap().name, "code");
+ }
+
+ #[test]
+ fn auto_back_and_forth_jumps_to_the_previous_workspace_when_reselecting_the_active_one() {
+ let mut wm = wm_with_monitor();
+ wm.auto_back_and_forth = true;
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.current_workspace(), ws2);
+ // Re-selecting the already-active workspace jumps back to 0, the
+ // one that was active right before.
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.current_workspace(), 0);
+ }
+
+ #[test]
+ fn without_auto_back_and_forth_reselecting_the_active_workspace_is_a_plain_no_op() {
+ let mut wm = wm_with_monitor();
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.current_workspace(), ws2);
+ }
+
+ #[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");
+ wm.switch_workspace(ws2);
+ wm.switch_workspace(9999);
+ assert_eq!(wm.current_workspace(), ws2);
+ // The failed switch must not have overwritten `previous_workspace`
+ // either - auto_back_and_forth would otherwise jump to a
+ // workspace id that was never really visited.
+ wm.auto_back_and_forth = true;
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.current_workspace(), 0);
+ }
+
+ #[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");
+ assert!(wm.workspaces().iter().all(|w| w.name != "ghost"));
+ }
+
+ // ---- Scratchpad --------------------------------------------------------
+
+ #[test]
+ fn scratchpad_add_hides_the_window_and_marks_pool_membership() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "term"));
+ wm.scratchpad_add(a);
+ let w = wm.window(a).unwrap();
+ assert!(w.scratchpad);
+ assert!(w.minimized);
+ assert!(w.floating);
+ assert!(!wm.visible_windows().any(|w| w.id == a));
+ }
+
+ #[test]
+ fn scratchpad_show_brings_back_the_hidden_window_and_focuses_it() {
+ 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();
+ let w = wm.window(a).unwrap();
+ assert!(!w.minimized);
+ assert_eq!(wm.focused_id(), Some(a));
+ assert!(wm.visible_windows().any(|w| w.id == a));
+ }
+
+ #[test]
+ fn scratchpad_show_hides_again_when_the_shown_scratchpad_window_is_focused() {
+ 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(); // shows + focuses
+ wm.scratchpad_show(); // toggles back off
+ assert!(wm.window(a).unwrap().minimized);
+ assert!(!wm.visible_windows().any(|w| w.id == a));
+ }
+
+ #[test]
+ fn scratchpad_show_moves_the_window_onto_the_current_workspace() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "term"));
+ wm.scratchpad_add(a);
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ wm.scratchpad_show();
+ assert_eq!(wm.window(a).unwrap().workspace, ws2);
+ assert!(wm.visible_windows().any(|w| w.id == a));
+ }
+
+ #[test]
+ fn scratchpad_show_with_no_scratchpad_windows_is_a_no_op() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "normal"));
+ wm.scratchpad_show();
+ assert_eq!(wm.focused_id(), Some(a));
+ assert!(!wm.window(a).unwrap().minimized);
+ }
+
+ #[test]
+ fn scratchpad_show_picks_the_most_recently_added_hidden_window() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "old"));
+ wm.scratchpad_add(a);
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "new"));
+ wm.scratchpad_add(b);
+ wm.scratchpad_show();
+ assert_eq!(wm.focused_id(), Some(b));
+ assert!(wm.window(a).unwrap().minimized);
+ }
+
+ #[test]
+ fn scratchpad_remove_leaves_current_visibility_untouched_but_drops_pool_membership() {
+ 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_remove(a);
+ assert!(!wm.window(a).unwrap().scratchpad);
+ assert!(wm.window(a).unwrap().minimized);
+ // No longer scratchpad-managed, so a later `scratchpad_show` must
+ // not touch it.
+ wm.scratchpad_show();
+ assert!(wm.window(a).unwrap().minimized);
+ }
+
+ // ---- Monitor hotplug -------------------------------------------------
+
+ fn two_monitors() -> Vec<Monitor> {
+ let mut a = Monitor::new(0, "primary", Rect::new(0, 0, 1280, 800));
+ a.primary = true;
+ let b = Monitor::new(1, "secondary", Rect::new(1280, 0, 1920, 1080));
+ vec![a, b]
+ }
+
+ #[test]
+ fn unplugging_a_monitor_rehomes_its_windows_to_the_primary() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "on-second-monitor");
+ w.geometry = Rect::new(1500, 200, 600, 400); // inside monitor 1 only
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().monitor = 1;
+
+ // Monitor 1 goes away.
+ wm.set_monitors(vec![two_monitors().remove(0)]);
+
+ let w = wm.window(id).unwrap();
+ assert_eq!(w.monitor, 0, "window should be rehomed to the primary monitor");
+ assert!(
+ Rect::new(0, 0, 1280, 800).overlaps(&w.geometry),
+ "rehomed window should be on-screen, got {:?}",
+ w.geometry
+ );
+ }
+
+ #[test]
+ fn windows_already_on_a_surviving_monitor_are_left_alone() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "on-primary");
+ w.geometry = Rect::new(10, 20, 300, 200);
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().monitor = 0;
+ wm.window_mut(id).unwrap().geometry = Rect::new(10, 20, 300, 200);
+
+ wm.set_monitors(vec![two_monitors().remove(0)]);
+
+ let w = wm.window(id).unwrap();
+ assert_eq!(w.monitor, 0);
+ assert_eq!(w.geometry, Rect::new(10, 20, 300, 200), "untouched window must not move");
+ }
+
+ #[test]
+ fn a_window_still_overlapping_the_primary_keeps_its_geometry() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "straddling");
+ wm.add_window(w.clone());
+ // Straddles the boundary, so it still overlaps the primary.
+ w.geometry = Rect::new(1200, 100, 400, 300);
+ wm.window_mut(id).unwrap().monitor = 1;
+ wm.window_mut(id).unwrap().geometry = w.geometry;
+
+ wm.set_monitors(vec![two_monitors().remove(0)]);
+
+ let got = wm.window(id).unwrap();
+ assert_eq!(got.monitor, 0, "monitor id must still be remapped");
+ assert_eq!(got.geometry, Rect::new(1200, 100, 400, 300), "already-visible geometry should be kept");
+ }
+
+ #[test]
+ fn losing_every_monitor_leaves_windows_intact_for_when_one_returns() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "orphan");
+ w.geometry = Rect::new(1500, 200, 600, 400);
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().monitor = 1;
+ wm.window_mut(id).unwrap().geometry = Rect::new(1500, 200, 600, 400);
+
+ wm.set_monitors(Vec::new());
+
+ let got = wm.window(id).unwrap();
+ assert_eq!(got.geometry, Rect::new(1500, 200, 600, 400));
+ assert_eq!(got.monitor, 1);
+ }
+
+ #[test]
+ fn a_window_whose_monitor_field_is_stale_is_still_rescued() {
+ // Regression: `add_window` assigns `monitor` from the *primary*
+ // monitor, so a window placed on the second monitor by a rule (or
+ // dragged there) keeps `monitor == 0`. Rehoming that keyed off the
+ // field alone skipped this window entirely and left it off-screen.
+ // Reproduced live by unplugging a monitor out from under an xterm.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+
+ let id = wm.alloc_window_id();
+ let w = Window::new(id, "placed-by-rule");
+ wm.add_window(w);
+ // Geometry on monitor 1, but `monitor` still says 0 - exactly what
+ // add_window + a geometry rule produce.
+ wm.window_mut(id).unwrap().geometry = Rect::new(1500, 200, 600, 400);
+ assert_eq!(wm.window(id).unwrap().monitor, 0, "precondition: stale field");
+
+ wm.set_monitors(vec![two_monitors().remove(0)]);
+
+ let got = wm.window(id).unwrap();
+ assert!(
+ Rect::new(0, 0, 1280, 800).overlaps(&got.geometry),
+ "window must be pulled back on-screen, got {:?}",
+ got.geometry
+ );
+ }
+
+ // ---- Fullscreen ------------------------------------------------------
+
+ #[test]
+ fn fullscreen_covers_the_monitor_and_restores_the_original_geometry() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "app");
+ w.geometry = Rect::new(100, 100, 400, 300);
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().geometry = Rect::new(100, 100, 400, 300);
+
+ wm.toggle_fullscreen(id);
+ let got = wm.window(id).unwrap();
+ assert!(got.fullscreen);
+ assert_eq!(got.geometry, Rect::new(0, 0, 1280, 800), "should cover the whole monitor");
+ assert!(!got.decorated, "fullscreen must drop the titlebar");
+
+ wm.toggle_fullscreen(id);
+ let got = wm.window(id).unwrap();
+ assert!(!got.fullscreen);
+ assert_eq!(got.geometry, Rect::new(100, 100, 400, 300));
+ assert!(got.decorated);
+ }
+
+ #[test]
+ fn fullscreen_round_trip_restores_a_client_side_decorated_window_to_undecorated() {
+ // Regression test: exiting fullscreen used to hardcode
+ // `decorated = true` unconditionally, which is only correct for a
+ // window that was decorated to begin with. A window a rule sets
+ // `decorated = false` for (client-side-decorated apps like
+ // Firefox) that goes fullscreen and back used to come back
+ // permanently `decorated = true` - with nothing to ever set it
+ // back, since the client only negotiates its decoration mode once.
+ // Since border/titlebar hit-testing is keyed off `Window.decorated`
+ // directly, this made srdwm swallow every click near the top of
+ // the window as a fake titlebar hit instead of forwarding it to
+ // the client.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ 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);
+
+ wm.toggle_fullscreen(id);
+ assert!(!wm.window(id).unwrap().decorated, "fullscreen itself must still drop the titlebar");
+
+ wm.toggle_fullscreen(id);
+ assert!(!wm.window(id).unwrap().decorated, "must restore the pre-fullscreen decorated=false, not default to true");
+ }
+
+ /// A monitor whose usable `geometry` is shrunk by a bottom dock's
+ /// exclusive zone, distinct from its true `full_geometry` - the shape
+ /// every real backend reports once a bar/dock has claimed space (see
+ /// `Monitor::full_geometry`'s doc comment).
+ 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);
+ m.primary = true;
+ m
+ }
+
+ #[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.
+ 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_fullscreen(id);
+ assert_eq!(wm.window(id).unwrap().geometry, Rect::new(0, 0, 1920, 1080), "fullscreen must reach the true monitor edge, past the dock");
+ }
+
+ #[test]
+ fn maximize_still_respects_the_dock_reservation() {
+ 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");
+ }
+
+ #[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.
+ 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));
+
+ // 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]);
+
+ assert_eq!(
+ wm.window(id).unwrap().geometry,
+ Rect::new(0, 0, 1920, 1080),
+ "an already-maximized window must live-track a monitor geometry change, not just windows placed afterward"
+ );
+ }
+
+ #[test]
+ fn fullscreen_window_also_live_tracks_a_monitor_geometry_change() {
+ 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_fullscreen(id);
+ assert_eq!(wm.window(id).unwrap().geometry, Rect::new(0, 0, 1920, 1080));
+
+ let mut resized = Monitor::new(0, "primary", Rect::new(0, 0, 2560, 1420));
+ resized.full_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, 2560, 1440), "fullscreen must live-track the true full rect, not the usable one");
+ }
+
+ #[test]
+ fn a_non_maximized_window_is_left_alone_by_a_monitor_geometry_change() {
+ // set_monitors' new re-sync pass is gated on maximized/fullscreen --
+ // must not clobber an ordinary floating/tiled window's geometry just
+ // because the monitor rect changed underneath it.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(vec![monitor_with_dock()]);
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "a");
+ w.geometry = Rect::new(100, 100, 400, 300);
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().geometry = Rect::new(100, 100, 400, 300);
+
+ 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]);
+
+ assert_eq!(wm.window(id).unwrap().geometry, Rect::new(100, 100, 400, 300));
+ }
+
+ #[test]
+ fn dragging_a_window_can_cross_into_the_dock_reserved_strip() {
+ // Regression test: `update_drag`'s clamp used to also use
+ // `Monitor::geometry` (the shrunk usable area), which made it
+ // physically impossible to ever drag a floating window into the
+ // strip a dock reserves - not just discouraged, genuinely
+ // unreachable at any drag speed or angle. `full_geometry` is what
+ // makes that space reachable again; the dock still renders on top
+ // as an overlay, same as it does everywhere else.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(vec![monitor_with_dock()]);
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "a");
+ w.geometry = Rect::new(500, 500, 200, 200);
+ wm.add_window(w);
+
+ wm.start_drag(id, 600, 600);
+ // Drag far down - past the old usable-area bottom (1020) and
+ // toward the true monitor bottom (1080).
+ wm.update_drag(600, 5000);
+ let g = wm.window(id).unwrap().geometry;
+ // Old behavior (clamped to `geometry`, bottom 1020) would stop at
+ // y=980; clamped to `full_geometry` (bottom 1080), it reaches 1040.
+ assert_eq!(g.y, 1040, "must clamp against the true monitor bottom, not the dock-shrunk usable area");
+ }
+
+ #[test]
+ fn class_rule_applies_once_app_id_is_known_after_creation() {
+ // Regression test: `add_window` matches rules against whatever
+ // `app_id`/`title` the window already has - for a native Wayland
+ // client those are still empty at that moment (the real values
+ // only arrive on a later commit, well after `new_toplevel`), so
+ // every class-based rule - including `srd.rule({ class =
+ // "firefox" }, { decorated = false })`, meant to stop srdwm
+ // drawing a second titlebar over Firefox's own - silently never
+ // matched. `reapply_rules_if_pending` is the retry a backend calls
+ // once the real app_id is known.
+ let mut wm = wm_with_monitor();
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { class: Some("firefox".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { decorated: Some(false), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ // Empty app_id, exactly as a fresh native Wayland toplevel has it.
+ wm.add_window(Window::new(id, ""));
+ assert!(wm.window(id).unwrap().decorated, "no app_id yet, so no match - must not have flipped early");
+
+ let w = wm.window_mut(id).unwrap();
+ w.app_id = "firefox".into();
+ wm.reapply_rules_if_pending(id);
+ assert!(!wm.window(id).unwrap().decorated, "app_id now known - the rule must apply on retry");
+
+ // A later, unrelated title change (e.g. a browser tab switching)
+ // must not re-match and re-apply - rule actions apply once.
+ let w = wm.window_mut(id).unwrap();
+ w.decorated = true;
+ w.title = "a new tab title".into();
+ wm.reapply_rules_if_pending(id);
+ assert!(wm.window(id).unwrap().decorated, "rules_applied is already true - must not re-run the match");
+ }
+
+ #[test]
+ fn opacity_rule_applies_on_the_deferred_retry_same_as_other_actions() {
+ // Regression test: `opacity` was added to `add_window`'s own rule
+ // application but missed here, in the deferred retry
+ // `reapply_rules_if_pending` - confirmed live: a rule like
+ // `srd.rule({ class = "Alacritty" }, { opacity = 0.4 })` never took
+ // effect for any real native Wayland client, since (per the test
+ // above) that's the *only* path a class-based rule actually
+ // matches through for one of those - `add_window`'s own match
+ // attempt always fails first, against an as-yet-empty `app_id`.
+ let mut wm = wm_with_monitor();
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { class: Some("alacritty".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { opacity: Some(0.4), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, ""));
+ assert_eq!(wm.window(id).unwrap().opacity, 1.0, "no app_id yet, so no match - must not have applied early");
+
+ let w = wm.window_mut(id).unwrap();
+ w.app_id = "Alacritty".into();
+ wm.reapply_rules_if_pending(id);
+ assert_eq!(wm.window(id).unwrap().opacity, 0.4, "app_id now known - the rule must apply on retry");
+ }
+
+ #[test]
+ fn fullscreen_from_maximized_still_restores_the_pre_maximize_size() {
+ // Both share `restore_geometry`; entering fullscreen from a
+ // maximised window must not overwrite it with the monitor rect, or
+ // the window could never get its real size back.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "app");
+ w.geometry = Rect::new(50, 60, 300, 200);
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().geometry = Rect::new(50, 60, 300, 200);
+
+ wm.toggle_maximize(id);
+ wm.toggle_fullscreen(id);
+ assert!(wm.is_fullscreen(id));
+ assert!(!wm.window(id).unwrap().maximized, "the two states are mutually exclusive");
+
+ wm.toggle_fullscreen(id);
+ assert_eq!(
+ wm.window(id).unwrap().geometry,
+ Rect::new(50, 60, 300, 200),
+ "must restore the size from before maximise, not the monitor rect"
+ );
+ }
+
+ #[test]
+ fn tiling_leaves_fullscreen_windows_alone() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "tiled"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "full"));
+ wm.toggle_fullscreen(b);
+
+ let changes = wm.arrange_workspace(wm.current_workspace());
+ assert!(
+ !changes.iter().any(|(id, _)| *id == b),
+ "a fullscreen window must not be re-tiled"
+ );
+ assert_eq!(wm.window(b).unwrap().geometry, Rect::new(0, 0, 1280, 800));
+ }
+
+ // ---- Directional move ------------------------------------------------
+
+ #[test]
+ fn moving_a_window_swaps_it_with_its_neighbour() {
+ let mut wm = wm_with_monitor();
+ let left = wm.alloc_window_id();
+ let mut a = Window::new(left, "left");
+ a.geometry = Rect::new(0, 0, 400, 400);
+ wm.add_window(a);
+ wm.window_mut(left).unwrap().geometry = Rect::new(0, 0, 400, 400);
+
+ let right = wm.alloc_window_id();
+ let mut b = Window::new(right, "right");
+ b.geometry = Rect::new(600, 0, 400, 400);
+ wm.add_window(b);
+ wm.window_mut(right).unwrap().geometry = Rect::new(600, 0, 400, 400);
+
+ wm.focus_window(left);
+ let swapped = wm.move_window_direction(Direction::Right);
+
+ assert_eq!(swapped, Some(right));
+ assert_eq!(wm.window(left).unwrap().geometry, Rect::new(600, 0, 400, 400));
+ assert_eq!(wm.window(right).unwrap().geometry, Rect::new(0, 0, 400, 400));
+ }
+
+ #[test]
+ fn moving_with_no_neighbour_pushes_to_the_monitor_edge() {
+ let mut wm = wm_with_monitor();
+ let id = wm.alloc_window_id();
+ let mut w = Window::new(id, "only");
+ w.geometry = Rect::new(500, 300, 200, 150);
+ wm.add_window(w);
+ wm.window_mut(id).unwrap().geometry = Rect::new(500, 300, 200, 150);
+ wm.focus_window(id);
+
+ assert_eq!(wm.move_window_direction(Direction::Left), None);
+ assert_eq!(wm.window(id).unwrap().geometry.x, 0, "should hug the left edge");
+
+ wm.move_window_direction(Direction::Down);
+ let g = wm.window(id).unwrap().geometry;
+ let mon = wm.primary_monitor().unwrap().geometry;
+ assert_eq!(g.bottom(), mon.bottom(), "should hug the bottom edge");
+ }
+
+ #[test]
+ fn swapping_also_reorders_the_stack_so_tiling_follows() {
+ // Under tiling the layout assigns slots from `order`, so a swap that
+ // only exchanged geometry would be undone by the next arrange.
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+ wm.arrange_workspace(wm.current_workspace());
+
+ // Snapshot *after* focusing: `focus_window` raises, which reorders
+ // on its own and would otherwise mask what the move did.
+ wm.focus_window(a);
+ let order_before: Vec<_> = wm.stacking_order().map(|w| w.id).collect();
+ wm.move_window_direction(Direction::Right);
+ let order_after: Vec<_> = wm.stacking_order().map(|w| w.id).collect();
+
+ assert_ne!(order_before, order_after, "stacking order must reflect the swap");
+ assert_eq!(
+ order_after,
+ order_before.iter().rev().copied().collect::<Vec<_>>(),
+ "the two windows should have traded places in the stack"
+ );
+ }
+
+ // ---- Always on top ---------------------------------------------------
+
+ #[test]
+ fn pinned_windows_stay_above_newly_raised_ones() {
+ let mut wm = wm_with_monitor();
+ let pinned = wm.alloc_window_id();
+ wm.add_window(Window::new(pinned, "pip"));
+ let other = wm.alloc_window_id();
+ wm.add_window(Window::new(other, "normal"));
+
+ wm.toggle_always_on_top(pinned);
+ assert!(wm.is_always_on_top(pinned));
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(pinned));
+
+ // Raising a normal window must not bury the pinned one.
+ wm.raise_window(other);
+ assert_eq!(
+ wm.stacking_order().last().map(|w| w.id),
+ Some(pinned),
+ "pinned window must remain topmost after another is raised"
+ );
+ }
+
+ #[test]
+ fn a_new_window_does_not_cover_a_pinned_one() {
+ let mut wm = wm_with_monitor();
+ let pinned = wm.alloc_window_id();
+ wm.add_window(Window::new(pinned, "pip"));
+ wm.toggle_always_on_top(pinned);
+
+ let fresh = wm.alloc_window_id();
+ wm.add_window(Window::new(fresh, "just opened"));
+
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(pinned));
+ }
+
+ #[test]
+ fn unpinning_lets_a_window_fall_back_into_the_normal_stack() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+
+ wm.toggle_always_on_top(a);
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(a));
+ wm.toggle_always_on_top(a);
+ wm.raise_window(b);
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(b));
+ }
+
+ #[test]
+ fn lower_window_sends_it_to_the_back_of_the_stack() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+ let c = wm.alloc_window_id();
+ wm.add_window(Window::new(c, "c"));
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(c), "precondition: c is on top after being added last");
+
+ wm.lower_window(c);
+ let order: Vec<_> = wm.stacking_order().map(|w| w.id).collect();
+ assert_eq!(order, vec![c, a, b], "c must be at the very back, a/b unchanged relative to each other");
+ }
+
+ #[test]
+ fn lower_window_never_buries_a_pinned_window() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let pinned = wm.alloc_window_id();
+ wm.add_window(Window::new(pinned, "pinned"));
+ wm.toggle_always_on_top(pinned);
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(pinned));
+
+ wm.lower_window(a);
+ assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(pinned), "a pinned window must stay on top even after an unrelated lower_window call");
+ }