srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/layout.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/layout.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/layout.rs')
-rw-r--r--crates/core/src/layout.rs150
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());
+ }
+}