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/layout.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/layout.rs')
| -rw-r--r-- | crates/core/src/layout.rs | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/crates/core/src/layout.rs b/crates/core/src/layout.rs new file mode 100644 index 0000000..fc6c0bc --- /dev/null +++ b/crates/core/src/layout.rs @@ -0,0 +1,150 @@ +use crate::geometry::Rect; +use crate::monitor::Monitor; +use crate::window::WindowId; +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy)] +pub struct TilingConfig { + pub master_ratio: f32, + pub master_count: usize, + pub gap_inner: u32, + pub gap_outer: u32, +} + +impl Default for TilingConfig { + fn default() -> Self { + Self { master_ratio: 0.6, master_count: 1, gap_inner: 8, gap_outer: 16 } + } +} + +/// Arranges a set of windows on a monitor. Implementations are pure +/// functions of (window order, monitor, config) -> new geometries, which +/// keeps them trivially unit-testable and free of platform coupling. +pub trait Layout: Send + Sync { + fn name(&self) -> &'static str; + fn arrange(&self, windows: &[WindowId], monitor: &Monitor, cfg: &TilingConfig) -> HashMap<WindowId, Rect>; +} + +/// Master-stack tiling (dwm/i3-style): the first `master_count` windows take +/// a resizable master column on the left; the rest are split evenly in a +/// stack column on the right. This replaces the legacy C++ placeholder, +/// which only ever split windows into equal-width columns. +pub struct MasterStackLayout; + +impl Layout for MasterStackLayout { + fn name(&self) -> &'static str { + "tiling" + } + + fn arrange(&self, windows: &[WindowId], monitor: &Monitor, cfg: &TilingConfig) -> HashMap<WindowId, Rect> { + let mut out = HashMap::new(); + if windows.is_empty() { + return out; + } + let area = monitor.geometry.inset(cfg.gap_outer); + if windows.len() == 1 { + out.insert(windows[0], area); + return out; + } + + let master_count = cfg.master_count.max(1).min(windows.len()); + let has_stack = windows.len() > master_count; + let master_width = if has_stack { + ((area.width as f32) * cfg.master_ratio) as u32 + } else { + area.width + }; + + let half_gap = cfg.gap_inner / 2; + let master_h = area.height / master_count as u32; + for (i, &id) in windows[..master_count].iter().enumerate() { + let r = Rect { + x: area.x, + y: area.y + (i as u32 * master_h) as i32, + width: master_width.saturating_sub(half_gap), + height: master_h.saturating_sub(cfg.gap_inner), + }; + out.insert(id, r); + } + + if has_stack { + let stack = &windows[master_count..]; + let stack_x = area.x + master_width as i32 + half_gap as i32; + let stack_width = area.width.saturating_sub(master_width + half_gap); + let stack_h = area.height / stack.len() as u32; + for (i, &id) in stack.iter().enumerate() { + let r = Rect { + x: stack_x, + y: area.y + (i as u32 * stack_h) as i32, + width: stack_width, + height: stack_h.saturating_sub(cfg.gap_inner), + }; + out.insert(id, r); + } + } + out + } +} + +/// "Dynamic" layout: windows keep whatever geometry they already have. +/// Placement for *new* windows is handled separately by [`crate::placement::SmartPlacement`] +/// at creation time; this layout never repositions existing windows. This +/// mirrors the legacy design intent (see docs/PRIOR_ART.md) but, unlike the +/// C++ version, is a deliberate no-op rather than an accidental one. +pub struct NoOpLayout(pub &'static str); + +impl Layout for NoOpLayout { + fn name(&self) -> &'static str { + self.0 + } + + fn arrange(&self, _windows: &[WindowId], _monitor: &Monitor, _cfg: &TilingConfig) -> HashMap<WindowId, Rect> { + HashMap::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn monitor() -> Monitor { + Monitor::new(0, "test", Rect::new(0, 0, 1920, 1080)) + } + + #[test] + fn single_window_fills_monitor_minus_outer_gap() { + let layout = MasterStackLayout; + let cfg = TilingConfig { gap_outer: 20, ..Default::default() }; + let result = layout.arrange(&[1], &monitor(), &cfg); + assert_eq!(result[&1], Rect::new(20, 20, 1880, 1040)); + } + + #[test] + fn two_windows_split_master_and_stack() { + let layout = MasterStackLayout; + let cfg = TilingConfig { gap_outer: 0, gap_inner: 0, master_ratio: 0.6, master_count: 1 }; + let result = layout.arrange(&[1, 2], &monitor(), &cfg); + assert_eq!(result[&1].width, 1152); // 60% of 1920 + assert_eq!(result[&2].width, 768); + assert_eq!(result[&1].height, 1080); + assert_eq!(result[&2].height, 1080); + } + + #[test] + fn three_windows_stack_splits_remaining_height_evenly() { + let layout = MasterStackLayout; + let cfg = TilingConfig { gap_outer: 0, gap_inner: 0, master_ratio: 0.5, master_count: 1 }; + let result = layout.arrange(&[1, 2, 3], &monitor(), &cfg); + assert_eq!(result[&2].height, 540); + assert_eq!(result[&3].height, 540); + assert_eq!(result[&2].y, 0); + assert_eq!(result[&3].y, 540); + } + + #[test] + fn no_op_layout_never_moves_windows() { + let layout = NoOpLayout("dynamic"); + let result = layout.arrange(&[1, 2, 3], &monitor(), &TilingConfig::default()); + assert!(result.is_empty()); + } +} |