srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/placement.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-04-02 00:58:00 +0200
committersrdusr <[email protected]>2024-04-02 00:58:00 +0200
commit8110bb2773b6c841029a51eca7971f42a36f480c (patch)
treeb3b364a6c0231fe408229e94dc9e25ce4562ad02 /crates/core/src/placement.rs
parentd1f856edc516047575cf46f12fad2b59006363c8 (diff)
downloadsrdwm-8110bb2773b6c841029a51eca7971f42a36f480c.tar.gz
srdwm-8110bb2773b6c841029a51eca7971f42a36f480c.zip
Rewrite srdwm in Rust: working X11 and Wayland backends, Lua config
The C++ prototype (moved to legacy-cpp/) was mostly a design skeleton: X11 and Windows backends were partially real, Wayland created the wlroots object graph but never wired a single event listener, macOS was stub except monitor enumeration, and the Lua engine's srd.bind() stored a key-combo string but never the actual closure. See docs/PRIOR_ART.md for the full audit. This replaces it with a Cargo workspace: - srdwm-core: platform-independent window/workspace/monitor state, a real master-stack tiling layout, and SmartPlacement grid/cascade/ snap-to-edge placement - fixing several bugs in the C++ version (hardcoded 2-column grid, cascade that never cascaded, snap-to-edge that always returned a fixed rect). 35 unit tests. - srdwm-config: the srd Lua API via mlua, implementing the surface docs/DEFAULTS.md always documented but the C++ engine never actually built (srd.window.close()/focus(direction), srd.workspace.next(), real keybinding closures, require("srd") support). 10 unit tests. - srdwm-x11: a real reparenting WM with a drawn title bar (buttons, drag, resize), verified live under Xephyr - frame placement and client offset match srdwm-core's computed geometry exactly, and the decoration renders correctly on screen. - srdwm-wayland: a from-scratch smithay compositor (the C++ version had nothing working to port from) - runs via the winit backend, tracks xdg-shell toplevels through the same WindowManager and hit-testing code X11 uses, verified to start/render/run without crashing. Decorations are solid-color (no text yet); see docs/IMPLEMENTATION_STATUS.md for exact scope. - srdwm-windows / srdwm-macos: structured, cfg-gated designs informed by komorebi/glazewm and yabai/AeroSpace respectively (see docs/PRIOR_ART.md), honestly marked as unbuilt/unverified since this sandbox has no Windows or macOS target.
Diffstat (limited to 'crates/core/src/placement.rs')
-rw-r--r--crates/core/src/placement.rs187
1 files changed, 187 insertions, 0 deletions
diff --git a/crates/core/src/placement.rs b/crates/core/src/placement.rs
new file mode 100644
index 0000000..ed9e410
--- /dev/null
+++ b/crates/core/src/placement.rs
@@ -0,0 +1,187 @@
+//! Smart window placement: Windows-11-style grid placement, cascade fallback,
+//! and Windows-Snap-style edge magnetism for drags.
+//!
+//! This reimplements the intent of the legacy C++ `SmartPlacement` class, but
+//! fixes several bugs found in the original (see docs/PRIOR_ART.md):
+//! - grid placement used a `static` round-robin counter that hardcoded a
+//! 2-column layout and never tracked real cell occupancy; here we scan the
+//! actual grid for the first cell that doesn't overlap an existing window.
+//! - cascade placement didn't cascade at all (it reused the first free-space
+//! sample); here new windows step diagonally by `cascade_offset` and wrap.
+//! - snap-to-edge always returned a fixed centered rect; here it computes a
+//! real Windows-Snap-style half/quarter/maximize zone from drag position.
+
+use crate::geometry::Rect;
+use crate::monitor::Monitor;
+
+pub const MIN_WINDOW_WIDTH: u32 = 200;
+pub const MIN_WINDOW_HEIGHT: u32 = 150;
+
+#[derive(Debug, Clone, Copy)]
+pub struct PlacementConfig {
+ pub grid_margin: u32,
+ pub cascade_offset: i32,
+ pub snap_threshold: i32,
+ pub max_grid: u32,
+}
+
+impl Default for PlacementConfig {
+ fn default() -> Self {
+ Self { grid_margin: 10, cascade_offset: 30, snap_threshold: 50, max_grid: 4 }
+ }
+}
+
+pub struct SmartPlacement;
+
+impl SmartPlacement {
+ /// Place a new window of `size` given the geometries of windows already
+ /// occupying `monitor`. Tries a grid cell first, falling back to cascade.
+ pub fn place(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Rect {
+ Self::grid(monitor, existing, size, cfg).unwrap_or_else(|| Self::cascade(monitor, existing, size, cfg))
+ }
+
+ fn grid(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Option<Rect> {
+ let count = existing.len() + 1;
+ let grid_size = (count as f64).sqrt().ceil() as u32;
+ let grid_size = grid_size.clamp(1, cfg.max_grid);
+ let area = monitor.geometry;
+
+ let margins = cfg.grid_margin * (grid_size + 1);
+ if area.width <= margins || area.height <= margins {
+ return None;
+ }
+ let cell_w = (area.width - margins) / grid_size;
+ let cell_h = (area.height - margins) / grid_size;
+ if cell_w < MIN_WINDOW_WIDTH || cell_h < MIN_WINDOW_HEIGHT {
+ return None;
+ }
+
+ for gy in 0..grid_size {
+ for gx in 0..grid_size {
+ let x = area.x + cfg.grid_margin as i32 + (gx * (cell_w + cfg.grid_margin)) as i32;
+ let y = area.y + cfg.grid_margin as i32 + (gy * (cell_h + cfg.grid_margin)) as i32;
+ let candidate = Rect::new(x, y, cell_w, cell_h);
+ if !existing.iter().any(|w| w.overlaps(&candidate)) {
+ return Some(Rect::new(x, y, size.0.min(cell_w), size.1.min(cell_h)));
+ }
+ }
+ }
+ None
+ }
+
+ /// Diagonal cascade, stepping by `cascade_offset` per already-placed
+ /// window and wrapping back to the origin once it would run off the
+ /// monitor.
+ fn cascade(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Rect {
+ let area = monitor.geometry;
+ let width = size.0.min(area.width);
+ let height = size.1.min(area.height);
+
+ let max_steps_x = ((area.width as i32 - width as i32) / cfg.cascade_offset.max(1)).max(1);
+ let max_steps_y = ((area.height as i32 - height as i32) / cfg.cascade_offset.max(1)).max(1);
+ let max_steps = max_steps_x.min(max_steps_y).max(1);
+
+ let step = existing.len() as i32 % max_steps;
+ let x = (area.x + cfg.cascade_offset + step * cfg.cascade_offset).min(area.right() - width as i32).max(area.x);
+ let y = (area.y + cfg.cascade_offset + step * cfg.cascade_offset).min(area.bottom() - height as i32).max(area.y);
+ Rect::new(x, y, width, height)
+ }
+
+ /// Given a window being dragged (its live geometry) and the monitor it's
+ /// on, returns the Windows-Snap zone it should resize to if it's within
+ /// `snap_threshold` pixels of a screen edge or corner, or `None` if it's
+ /// not near any snap zone.
+ pub fn snap_zone(dragged: Rect, monitor: &Monitor, cfg: &PlacementConfig) -> Option<Rect> {
+ let area = monitor.geometry;
+ let t = cfg.snap_threshold;
+ let near_left = (dragged.x - area.x).abs() <= t;
+ let near_right = (area.right() - dragged.right()).abs() <= t;
+ let near_top = (dragged.y - area.y).abs() <= t;
+ let near_bottom = (area.bottom() - dragged.bottom()).abs() <= t;
+
+ let half_w = area.width / 2;
+ let half_h = area.height / 2;
+
+ Some(match (near_left, near_right, near_top, near_bottom) {
+ (true, false, true, false) => Rect::new(area.x, area.y, half_w, half_h),
+ (false, true, true, false) => Rect::new(area.x + half_w as i32, area.y, half_w, half_h),
+ (true, false, false, true) => Rect::new(area.x, area.y + half_h as i32, half_w, half_h),
+ (false, true, false, true) => Rect::new(area.x + half_w as i32, area.y + half_h as i32, half_w, half_h),
+ (true, false, false, false) => Rect::new(area.x, area.y, half_w, area.height),
+ (false, true, false, false) => Rect::new(area.x + half_w as i32, area.y, half_w, area.height),
+ (false, false, true, false) => area,
+ _ => return None,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn monitor() -> Monitor {
+ Monitor::new(0, "test", Rect::new(0, 0, 1920, 1080))
+ }
+
+ #[test]
+ fn first_window_goes_in_top_left_grid_cell() {
+ let cfg = PlacementConfig::default();
+ let r = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg);
+ assert_eq!(r.x, cfg.grid_margin as i32);
+ assert_eq!(r.y, cfg.grid_margin as i32);
+ }
+
+ #[test]
+ fn grid_avoids_occupied_cells() {
+ let cfg = PlacementConfig::default();
+ let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg);
+ let second = SmartPlacement::place(&monitor(), &[first], (400, 300), &cfg);
+ assert!(!first.overlaps(&second), "second window must not overlap the first: {first:?} vs {second:?}");
+ }
+
+ #[test]
+ fn cascade_kicks_in_once_grid_is_full() {
+ let cfg = PlacementConfig { max_grid: 1, ..Default::default() };
+ // max_grid=1 means the grid is always a single cell, so a second
+ // window can never find a free grid cell and must cascade.
+ let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg);
+ let second = SmartPlacement::place(&monitor(), &[first], (400, 300), &cfg);
+ assert_ne!(first, second);
+ // First window is grid-placed (offset by grid_margin); the second no
+ // longer fits any grid cell and falls back to cascade, which steps
+ // from the monitor origin by `cascade_offset` per already-placed window.
+ assert_eq!(second.x, cfg.cascade_offset * 2);
+ assert_eq!(second.y, cfg.cascade_offset * 2);
+ }
+
+ #[test]
+ fn snap_left_edge_yields_left_half() {
+ let cfg = PlacementConfig::default();
+ let dragged = Rect::new(2, 100, 400, 300); // x=2 is within threshold of left edge
+ let zone = SmartPlacement::snap_zone(dragged, &monitor(), &cfg).unwrap();
+ assert_eq!(zone, Rect::new(0, 0, 960, 1080));
+ }
+
+ #[test]
+ fn snap_top_edge_yields_maximize() {
+ let cfg = PlacementConfig::default();
+ let dragged = Rect::new(500, 1, 400, 300);
+ let zone = SmartPlacement::snap_zone(dragged, &monitor(), &cfg).unwrap();
+ assert_eq!(zone, monitor().geometry);
+ }
+
+ #[test]
+ fn snap_top_left_corner_yields_quarter() {
+ let cfg = PlacementConfig::default();
+ let dragged = Rect::new(1, 1, 400, 300);
+ let zone = SmartPlacement::snap_zone(dragged, &monitor(), &cfg).unwrap();
+ assert_eq!(zone, Rect::new(0, 0, 960, 540));
+ }
+
+ #[test]
+ fn no_snap_away_from_edges() {
+ let cfg = PlacementConfig::default();
+ let dragged = Rect::new(700, 400, 400, 300);
+ assert!(SmartPlacement::snap_zone(dragged, &monitor(), &cfg).is_none());
+ }
+}