srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/layout.rs
blob: fc6c0bc4bfc990bac8ae8717a7b942f7bf9aa683 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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());
    }
}