diff options
| author | srdusr <[email protected]> | 2024-04-02 00:58:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-04-02 00:58:00 +0200 |
| commit | 8110bb2773b6c841029a51eca7971f42a36f480c (patch) | |
| tree | b3b364a6c0231fe408229e94dc9e25ce4562ad02 /crates/core/src/placement.rs | |
| parent | d1f856edc516047575cf46f12fad2b59006363c8 (diff) | |
| download | srdwm-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.rs | 187 |
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()); + } +} |