atagen

monolith

public
Star0
Watch0
8f974106aae3cb42125335d1835db9f3c9c20941
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/tests.rsRust296 lines9.3 KB
1
use super::*;
2
use proptest::prelude::*;
3
4
const AREA: Rect = Rect::new(0.0, 0.0, 1000.0, 600.0);
5
6
#[derive(Clone, Debug)]
7
enum Op {
8
    Add(u8, bool),
9
    Remove(u8),
10
    Focus(u8),
11
    Move(Dir),
12
    Block(Dir),
13
    Merge(Dir),
14
    View(u8),
15
    ToggleView(u8),
16
    Retag(u8, u8),
17
    Float(u8),
18
    Expand(u8),
19
    Flip,
20
    Resize(bool, i8),
21
    Scratch(u8),
22
    Summon,
23
    Unplug,
24
    Replug,
25
}
26
27
fn dir() -> impl Strategy<Value = Dir> {
28
    prop_oneof![
29
        Just(Dir::Left),
30
        Just(Dir::Right),
31
        Just(Dir::Up),
32
        Just(Dir::Down)
33
    ]
34
}
35
36
fn op() -> impl Strategy<Value = Op> {
37
    prop_oneof![
38
        4 => (0u8..4, prop::bool::weighted(0.15)).prop_map(|(t, f)| Op::Add(t, f)),
39
        1 => any::<u8>().prop_map(Op::Remove),
40
        3 => any::<u8>().prop_map(Op::Focus),
41
        4 => dir().prop_map(Op::Move),
42
        2 => dir().prop_map(Op::Block),
43
        3 => dir().prop_map(Op::Merge),
44
        1 => (0u8..4).prop_map(Op::View),
45
        2 => (0u8..4).prop_map(Op::ToggleView),
46
        1 => (any::<u8>(), 0u8..4).prop_map(|(w, t)| Op::Retag(w, t)),
47
        1 => any::<u8>().prop_map(Op::Float),
48
        1 => (0u8..3).prop_map(Op::Expand),
49
        1 => Just(Op::Flip),
50
        1 => (any::<bool>(), any::<i8>()).prop_map(|(a, d)| Op::Resize(a, d)),
51
        1 => any::<u8>().prop_map(Op::Scratch),
52
        1 => Just(Op::Summon),
53
        1 => Just(Op::Unplug),
54
        1 => Just(Op::Replug),
55
    ]
56
}
57
58
fn nth(l: &Layout, i: u8) -> Option<WindowId> {
59
    let ids: Vec<WindowId> = l.windows().map(|(id, _)| id).collect();
60
    (!ids.is_empty()).then(|| ids[i as usize % ids.len()])
61
}
62
63
fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {
64
    let active = l.active_output();
65
    match *op {
66
        Op::Add(t, f) => {
67
            let w = l.add_window(
68
                None,
69
                Some(tags[t as usize]),
70
                f.then(|| Rect::new(10.0, 10.0, 200.0, 100.0)),
71
            );
72
            if l.visible(w) {
73
                l.focus(w);
74
            }
75
        }
76
        Op::Remove(i) => nth(l, i).into_iter().for_each(|w| l.remove_window(w)),
77
        Op::Focus(i) => {
78
            if let Some(w) = nth(l, i).filter(|&w| l.visible(w)) {
79
                l.focus(w);
80
            }
81
        }
82
        Op::Move(d) => {
83
            l.move_window(d);
84
        }
85
        Op::Block(d) => {
86
            l.move_container(d);
87
        }
88
        Op::Merge(d) => {
89
            l.merge_window(d);
90
        }
91
        Op::View(t) => active.into_iter().for_each(|o| l.view(o, tags[t as usize])),
92
        Op::ToggleView(t) => active
93
            .into_iter()
94
            .for_each(|o| l.toggle_view(o, tags[t as usize])),
95
        Op::Retag(i, t) => nth(l, i)
96
            .into_iter()
97
            .for_each(|w| l.set_tag(w, tags[t as usize])),
98
        Op::Float(i) => nth(l, i).into_iter().for_each(|w| l.toggle_floating(w)),
99
        Op::Expand(s) => {
100
            l.expand(
101
                [
102
                    ExpandScope::Window,
103
                    ExpandScope::Container,
104
                    ExpandScope::All,
105
                ][s as usize],
106
            );
107
        }
108
        Op::Flip => active.into_iter().for_each(|o| l.flip(o)),
109
        Op::Resize(h, d) => {
110
            let axis = if h { Axis::Horizontal } else { Axis::Vertical };
111
            l.resize(axis, f64::from(d) / 200.0);
112
        }
113
        Op::Scratch(i) => nth(l, i).into_iter().for_each(|w| l.scratchpad_add(w, "s")),
114
        Op::Summon => {
115
            l.summon("s");
116
        }
117
        Op::Unplug => {
118
            let b = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o);
119
            if let Some(o) = b {
120
                l.remove_output(o);
121
            }
122
        }
123
        Op::Replug => {
124
            if !l.outputs().any(|(_, out)| out.name == "b") {
125
                l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
126
            }
127
        }
128
    }
129
}
130
131
fn check_geometry(l: &Layout) {
132
    for (o, out) in l.outputs() {
133
        let arr = l.arrange(o);
134
        let mut drawn: Vec<WindowId> = arr
135
            .tiled
136
            .iter()
137
            .filter(|p| p.visible)
138
            .map(|p| p.window)
139
            .chain(arr.floating.iter().map(|&(w, _)| w))
140
            .chain(arr.fullscreen)
141
            .collect();
142
        drawn.sort();
143
        drawn.dedup();
144
        let mut visible: Vec<WindowId> = l
145
            .windows()
146
            .filter(|&(w, win)| win.output == Some(o) && l.visible(w))
147
            .map(|(w, _)| w)
148
            .collect();
149
        visible.sort();
150
        assert_eq!(drawn, visible, "arrangement disagrees with visible()");
151
        let shown: Vec<Rect> = arr
152
            .tiled
153
            .iter()
154
            .filter(|p| p.visible)
155
            .map(|p| p.rect)
156
            .collect();
157
        for p in arr.tiled.iter().filter(|p| !p.visible) {
158
            assert!(p.rect.w * p.rect.h <= 1e-6, "hidden {:?} has area", p.rect);
159
        }
160
        for r in &shown {
161
            assert!(r.w >= 0.0 && r.h >= 0.0);
162
            assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6);
163
            assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6);
164
        }
165
        for (i, a) in shown.iter().enumerate() {
166
            for b in &shown[i + 1..] {
167
                let ox = a.right().min(b.right()) - a.x.max(b.x);
168
                let oy = a.bottom().min(b.bottom()) - a.y.max(b.y);
169
                assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}");
170
            }
171
        }
172
    }
173
}
174
175
proptest! {
176
    #![proptest_config(ProptestConfig::with_cases(2000))]
177
178
    #[test]
179
    fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {
180
        let mut l = Layout::default();
181
        l.add_output("a", AREA);
182
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
183
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
184
        for op in &ops {
185
            apply(&mut l, &tags, op);
186
            l.check();
187
            check_geometry(&l);
188
        }
189
    }
190
191
    #[test]
192
    fn replugging_restores_what_unplugging_moved(ops in prop::collection::vec(op(), 1..60)) {
193
        let mut l = Layout::default();
194
        l.add_output("a", AREA);
195
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
196
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
197
        for op in &ops {
198
            apply(&mut l, &tags, op);
199
        }
200
        let Some(b) = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o) else {
201
            return Ok(());
202
        };
203
        let before = |l: &Layout, o| {
204
            let out = l.output(o).unwrap();
205
            (out.tree.windows().collect::<Vec<_>>(), out.floating.clone(), out.viewed.clone())
206
        };
207
        let was = before(&l, b);
208
        l.remove_output(b);
209
        let b = l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
210
        l.check();
211
        prop_assert_eq!(before(&l, b), was);
212
    }
213
}
214
215
/// The hidden-slot rules from planner.html, demo 11.
216
#[test]
217
fn hidden_windows_keep_their_slots() {
218
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
219
    let mut t: Tree<u32> = Tree::default();
220
    t.insert_row(a);
221
    t.insert_after(d, a);
222
    t.insert_after(b, d);
223
    t.insert_row(c);
224
    t.insert_after(e, c);
225
    t.check();
226
    let hidden = |w: u32| w != d && w != e;
227
    let all = |_| true;
228
    let rect = |t: &Tree<u32>, w| {
229
        t.arrange(AREA, 0.0, all)
230
            .into_iter()
231
            .find(|p| p.0 == w)
232
            .unwrap()
233
            .1
234
    };
235
236
    assert!(t.move_window(a, Dir::Right, hidden));
237
    assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);
238
239
    assert!(t.move_window(c, Dir::Up, hidden));
240
    t.check();
241
    assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");
242
}
243
244
/// Moving out of a row toward a lane of hidden windows joins that lane, so
245
/// they share it when shown again instead of a new lane opening between.
246
#[test]
247
fn moving_toward_a_hidden_lane_joins_it() {
248
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
249
    let mut t: Tree<u32> = Tree::default();
250
    t.insert_row(a);
251
    t.insert_after(b, a);
252
    t.insert_after(c, b);
253
    t.insert_row(d);
254
    t.insert_after(e, c);
255
    let tag_two = |w: u32| w != a && w != d;
256
    assert!(t.move_window(e, Dir::Down, tag_two));
257
    t.check();
258
    let all = t.arrange(AREA, 0.0, |_| true);
259
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
260
    assert_eq!(rect(e).y, rect(d).y);
261
    assert!(rect(d).x < rect(e).x);
262
    assert!(rect(a).y < rect(d).y);
263
}
264
265
#[test]
266
fn merging_steps_out_of_a_stack_before_joining_another() {
267
    let (a, b, c) = (1, 2, 3);
268
    let stacked = || {
269
        let mut t: Tree<u32> = Tree::default();
270
        t.insert_row(a);
271
        t.insert_after(b, a);
272
        t.insert_after(c, b);
273
        assert!(t.merge_window(b, Dir::Left, |_| true));
274
        t
275
    };
276
    let rects = |t: &Tree<u32>| {
277
        t.check();
278
        let all = t.arrange(AREA, 0.0, |_| true);
279
        move |w| all.iter().find(|p: &&(u32, Rect)| p.0 == w).unwrap().1
280
    };
281
282
    let mut t = stacked();
283
    assert!(t.merge_window(a, Dir::Right, |_| true));
284
    let rect = rects(&t);
285
    assert!(rect(b).x < rect(a).x && rect(a).x < rect(c).x);
286
    assert_eq!(rect(a).h, AREA.h);
287
    assert!(t.merge_window(a, Dir::Right, |_| true));
288
    let rect = rects(&t);
289
    assert_eq!(rect(a).x, rect(c).x);
290
291
    let mut t = stacked();
292
    assert!(t.merge_window(a, Dir::Up, |_| true));
293
    let rect = rects(&t);
294
    assert_eq!(rect(a).w, AREA.w);
295
    assert!(rect(a).bottom() <= rect(b).y && rect(b).y == rect(c).y);
296
}