srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-14 00:42:00 +0200
committersrdusr <[email protected]>2024-05-14 00:42:00 +0200
commitdd31bf5ac2a692617d478478d36c733586b93cf8 (patch)
tree2adb42ad8b24172cbbea3bf362a0d284acd54439 /crates/core/src/manager.rs
parenta9dd8a6d4947cb537f7919c5a66ba4624af61800 (diff)
downloadsrdwm-dd31bf5ac2a692617d478478d36c733586b93cf8.tar.gz
srdwm-dd31bf5ac2a692617d478478d36c733586b93cf8.zip
Draw a cursor, handle the lid, and everything a real config needs
Groundwork for actually daily-driving this: porting the user's Hyprland config exposed what srdwm couldn't yet express, and testing on a bare TTY exposed something worse. A visible mouse cursor. Nothing drew a pointer at all - on a bare TTY the mouse was simply invisible. It hid because the nested backend runs inside another compositor, which draws a cursor over srdwm's window; only the DRM backend, i.e. the actual session path, was affected. A built-in arrow is now composited above everything on the output the pointer is on. It's a reviewable ASCII bitmap rather than an XCursor theme: a cursor that is always present beats a prettier one that sometimes isn't, the same reasoning as decoration.rs's font fallback. Client-set cursor surfaces and named shapes are still not rendered, so an app asking for an I-beam gets the arrow. Lid switch. libinput switch events are handled and surfaced to config as srd.on("lid_closed"/"lid_open", fn), so closing the lid can lock and suspend instead of doing nothing. Config-driven additions, each needed by a binding in the ported config and none of which existed: fullscreen (Window.fullscreen was a dead field -- declared, never read or written), directional window move that swaps with the neighbour and reorders the stack so tiling follows, focus cycling, modifier+drag to move/resize anywhere in a window rather than only by the titlebar, modifier+scroll to change workspace, and 8 XF86 media/power keysyms taken from the system's own XF86keysym.h. The keysym tables are hand-maintained in both directions and a key missing from either fails silently, so a round-trip test now covers every one the configs bind. Verified in the QEMU VM: a bare-TTY screendump shows a recognisable arrow at the pointer position (113 white fill + 58 black outline pixels at screen centre, where the pointer starts).
Diffstat (limited to 'crates/core/src/manager.rs')
-rw-r--r--crates/core/src/manager.rs268
1 files changed, 262 insertions, 6 deletions
diff --git a/crates/core/src/manager.rs b/crates/core/src/manager.rs
index d261c13..23d94be 100644
--- a/crates/core/src/manager.rs
+++ b/crates/core/src/manager.rs
@@ -296,7 +296,15 @@ impl WindowManager {
/// Vim-style directional focus: picks the nearest window whose center
/// lies in `dir` relative to the focused window's center, on the same
/// workspace. Returns the newly focused window, if any.
- pub fn focus_direction(&mut self, dir: Direction) -> Option<WindowId> {
+ /// Nearest window to the focused one in `dir`, by a distance biased
+ /// toward the requested axis so a window that's mostly to the left
+ /// (small |dy|) beats a diagonally-placed one - matching how
+ /// i3/sway-style directional focus feels.
+ ///
+ /// Shared by [`Self::focus_direction`] and [`Self::move_window_direction`]
+ /// so "the window to the left" means the same thing whether you're
+ /// focusing it or swapping with it.
+ pub fn neighbour_in(&self, dir: Direction) -> Option<WindowId> {
let (fx, fy, fid) = {
let focused = self.focused_window()?;
let (fx, fy) = focused.geometry.center();
@@ -316,9 +324,6 @@ impl WindowManager {
if !matches {
continue;
}
- // Distance biased toward the requested axis so a window that's
- // mostly to the left (small |dy|) beats one that's diagonally
- // placed, matching how i3/sway-style directional focus feels.
let (primary, secondary) = match dir {
Direction::Left | Direction::Right => (dx, dy),
Direction::Up | Direction::Down => (dy, dx),
@@ -328,13 +333,61 @@ impl WindowManager {
best = Some((w.id, dist));
}
}
- let target = best.map(|(id, _)| id);
+ best.map(|(id, _)| id)
+ }
+
+ pub fn focus_direction(&mut self, dir: Direction) -> Option<WindowId> {
+ let target = self.neighbour_in(dir);
if let Some(id) = target {
self.focus_window(id);
}
target
}
+ /// Moves the focused window in `dir` by swapping places with its
+ /// neighbour there - the `movewindow l/r/u/d` gesture.
+ ///
+ /// Swapping (rather than nudging by a fixed step) is what makes this
+ /// useful in both of srdwm's modes: under tiling it reorders the layout,
+ /// and in dynamic/floating mode two windows trade positions, which is
+ /// predictable either way. With no neighbour in that direction the
+ /// window is pushed to the corresponding edge of its monitor instead, so
+ /// the key still does something sensible.
+ pub fn move_window_direction(&mut self, dir: Direction) -> Option<WindowId> {
+ let focused = self.focused_id()?;
+ match self.neighbour_in(dir) {
+ Some(other) => {
+ let a = self.windows.get(&focused)?.geometry;
+ let b = self.windows.get(&other)?.geometry;
+ if let Some(w) = self.windows.get_mut(&focused) {
+ w.geometry = b;
+ }
+ if let Some(w) = self.windows.get_mut(&other) {
+ w.geometry = a;
+ }
+ // Keep stacking order in step so a tiling layout, which
+ // assigns slots from `order`, actually reflects the swap.
+ let (ia, ib) = (
+ self.order.iter().position(|&id| id == focused)?,
+ self.order.iter().position(|&id| id == other)?,
+ );
+ self.order.swap(ia, ib);
+ Some(other)
+ }
+ None => {
+ let mon = self.windows.get(&focused).and_then(|w| self.monitor_for(w.monitor))?.geometry;
+ let w = self.windows.get_mut(&focused)?;
+ match dir {
+ Direction::Left => w.geometry.x = mon.x,
+ Direction::Right => w.geometry.x = mon.right() - w.geometry.width as i32,
+ Direction::Up => w.geometry.y = mon.y,
+ Direction::Down => w.geometry.y = mon.bottom() - w.geometry.height as i32,
+ }
+ None
+ }
+ }
+ }
+
// ---- Window operations ----------------------------------------------
pub fn close_window(&mut self, id: WindowId) {
@@ -371,6 +424,40 @@ impl WindowManager {
}
}
+ /// Fullscreen: the window covers its whole monitor with no decoration.
+ ///
+ /// Distinct from [`Self::toggle_maximize`], which keeps the titlebar (and
+ /// is what a maximise button does). Both share `restore_geometry`, so
+ /// they are mutually exclusive - toggling one off restores whatever the
+ /// window's geometry was before *either* was applied, and entering
+ /// fullscreen from a maximised window doesn't lose the original size.
+ pub fn toggle_fullscreen(&mut self, id: WindowId) {
+ let monitor_geom = self.windows.get(&id).and_then(|w| self.monitor_for(w.monitor)).map(|m| m.geometry);
+ let Some(w) = self.windows.get_mut(&id) else { return };
+ if w.fullscreen {
+ if let Some(restore) = w.restore_geometry.take() {
+ w.geometry = restore;
+ }
+ w.fullscreen = false;
+ w.decorated = true;
+ } else if let Some(geom) = monitor_geom {
+ // Only remember the pre-fullscreen geometry if we aren't already
+ // maximised, otherwise the monitor rect would overwrite the real
+ // restore point and the window could never get its size back.
+ if !w.maximized {
+ w.restore_geometry = Some(w.geometry);
+ }
+ w.maximized = false;
+ w.geometry = geom;
+ w.fullscreen = true;
+ w.decorated = false;
+ }
+ }
+
+ pub fn is_fullscreen(&self, id: WindowId) -> bool {
+ self.windows.get(&id).map(|w| w.fullscreen).unwrap_or(false)
+ }
+
pub fn toggle_floating(&mut self, id: WindowId) {
if let Some(w) = self.windows.get_mut(&id) {
w.floating = !w.floating;
@@ -411,6 +498,32 @@ impl WindowManager {
None
}
+ /// Topmost non-minimised window containing a point, ignoring
+ /// decorations. Used for modifier+drag, where the grab applies anywhere
+ /// in the window rather than only on the titlebar (`hit_test`).
+ pub fn window_at(&self, x: i32, y: i32) -> Option<WindowId> {
+ self.order
+ .iter()
+ .rev()
+ .filter_map(|id| self.windows.get(id))
+ .find(|w| !w.minimized && w.geometry.contains_point(x, y))
+ .map(|w| w.id)
+ }
+
+ /// The corner of `id` nearest a point, for modifier+right-drag resize:
+ /// grabbing the closest corner is what makes the gesture feel like it
+ /// pulls the edge you aimed at (matching Hyprland's `resizewindow`).
+ pub fn nearest_corner(&self, id: WindowId, x: i32, y: i32) -> ResizeEdge {
+ let Some(w) = self.windows.get(&id) else { return ResizeEdge::BottomRight };
+ let (cx, cy) = w.geometry.center();
+ match (x < cx, y < cy) {
+ (true, true) => ResizeEdge::TopLeft,
+ (false, true) => ResizeEdge::TopRight,
+ (true, false) => ResizeEdge::BottomLeft,
+ (false, false) => ResizeEdge::BottomRight,
+ }
+ }
+
// ---- Drag / resize ------------------------------------------------------
pub fn start_drag(&mut self, id: WindowId, x: i32, y: i32) {
@@ -566,7 +679,9 @@ impl WindowManager {
let mut by_monitor: HashMap<MonitorId, Vec<WindowId>> = HashMap::new();
for &id in &self.order {
let Some(w) = self.windows.get(&id) else { continue };
- if w.workspace == workspace && !w.minimized && !w.floating {
+ // Fullscreen windows own their whole monitor, so tiling must
+ // leave them alone, exactly as it does floating ones.
+ if w.workspace == workspace && !w.minimized && !w.floating && !w.fullscreen {
by_monitor.entry(w.monitor).or_default().push(id);
}
}
@@ -960,4 +1075,145 @@ mod tests {
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_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"
+ );
+ }
}