srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/layout.rs
diff options
context:
space:
mode:
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());
+ }
+}