atagen

monolith

public
Star0
Watch0
0b68ce424a32e597159d75536d1b9efbe6fe39d6
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/tests.rsRust353 lines11.4 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
/// The area a tiled window gets, zero for floating or unplaced ones.
132
fn tiled_area(l: &Layout, w: WindowId) -> f64 {
133
    let Some(o) = l.window(w).and_then(|x| x.output) else {
134
        return 0.0;
135
    };
136
    l.arrange(o)
137
        .tiled
138
        .iter()
139
        .find(|p| p.window == w && p.visible)
140
        .map_or(0.0, |p| p.rect.w * p.rect.h)
141
}
142
143
fn check_geometry(l: &Layout) {
144
    for (o, out) in l.outputs() {
145
        let arr = l.arrange(o);
146
        let mut drawn: Vec<WindowId> = arr
147
            .tiled
148
            .iter()
149
            .filter(|p| p.visible)
150
            .map(|p| p.window)
151
            .chain(arr.floating.iter().map(|&(w, _)| w))
152
            .chain(arr.fullscreen)
153
            .collect();
154
        drawn.sort();
155
        drawn.dedup();
156
        let mut visible: Vec<WindowId> = l
157
            .windows()
158
            .filter(|&(w, win)| win.output == Some(o) && l.visible(w))
159
            .map(|(w, _)| w)
160
            .collect();
161
        visible.sort();
162
        assert_eq!(drawn, visible, "arrangement disagrees with visible()");
163
        let shown: Vec<Rect> = arr
164
            .tiled
165
            .iter()
166
            .filter(|p| p.visible)
167
            .map(|p| p.rect)
168
            .collect();
169
        for p in arr.tiled.iter().filter(|p| !p.visible) {
170
            assert!(p.rect.w * p.rect.h <= 1e-6, "hidden {:?} has area", p.rect);
171
        }
172
        for r in &shown {
173
            assert!(r.w >= 0.0 && r.h >= 0.0);
174
            assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6);
175
            assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6);
176
        }
177
        for (i, a) in shown.iter().enumerate() {
178
            for b in &shown[i + 1..] {
179
                let ox = a.right().min(b.right()) - a.x.max(b.x);
180
                let oy = a.bottom().min(b.bottom()) - a.y.max(b.y);
181
                assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}");
182
            }
183
        }
184
    }
185
}
186
187
proptest! {
188
    #![proptest_config(ProptestConfig::with_cases(2000))]
189
190
    #[test]
191
    fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {
192
        let mut l = Layout::default();
193
        l.add_output("a", AREA);
194
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
195
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
196
        for op in &ops {
197
            let moving = matches!(op, Op::Move(_) | Op::Merge(_) | Op::Block(_));
198
            let shown_before = l.focused().filter(|&w| tiled_area(&l, w) > 1.0);
199
            apply(&mut l, &tags, op);
200
            l.check();
201
            check_geometry(&l);
202
            if moving && let Some(w) = shown_before {
203
                prop_assert!(tiled_area(&l, w) > 1.0, "{w:?} vanished after {op:?}");
204
            }
205
        }
206
    }
207
208
    #[test]
209
    fn replugging_restores_what_unplugging_moved(ops in prop::collection::vec(op(), 1..60)) {
210
        let mut l = Layout::default();
211
        l.add_output("a", AREA);
212
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
213
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
214
        for op in &ops {
215
            apply(&mut l, &tags, op);
216
        }
217
        let Some(b) = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o) else {
218
            return Ok(());
219
        };
220
        let before = |l: &Layout, o| {
221
            let out = l.output(o).unwrap();
222
            (out.tree.windows().collect::<Vec<_>>(), out.floating.clone(), out.viewed.clone())
223
        };
224
        let was = before(&l, b);
225
        l.remove_output(b);
226
        let b = l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
227
        l.check();
228
        prop_assert_eq!(before(&l, b), was);
229
    }
230
}
231
232
/// The hidden-slot rules from planner.html, demo 11.
233
#[test]
234
fn hidden_windows_keep_their_slots() {
235
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
236
    let mut t: Tree<u32> = Tree::default();
237
    t.insert_row(a);
238
    t.insert_after(d, a);
239
    t.insert_after(b, d);
240
    t.insert_row(c);
241
    t.insert_after(e, c);
242
    t.check();
243
    let hidden = |w: u32| w != d && w != e;
244
    let all = |_| true;
245
    let rect = |t: &Tree<u32>, w| {
246
        t.arrange(AREA, 0.0, all)
247
            .into_iter()
248
            .find(|p| p.0 == w)
249
            .unwrap()
250
            .1
251
    };
252
253
    assert!(t.move_window(a, Dir::Right, hidden));
254
    assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);
255
256
    assert!(t.move_window(c, Dir::Up, hidden));
257
    t.check();
258
    assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");
259
}
260
261
/// Moving out of a row toward a lane of hidden windows joins that lane, so
262
/// they share it when shown again instead of a new lane opening between.
263
#[test]
264
fn moving_toward_a_hidden_lane_joins_it() {
265
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
266
    let mut t: Tree<u32> = Tree::default();
267
    t.insert_row(a);
268
    t.insert_after(b, a);
269
    t.insert_after(c, b);
270
    t.insert_row(d);
271
    t.insert_after(e, c);
272
    let tag_two = |w: u32| w != a && w != d;
273
    assert!(t.move_window(e, Dir::Down, tag_two));
274
    t.check();
275
    let all = t.arrange(AREA, 0.0, |_| true);
276
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
277
    assert_eq!(rect(e).y, rect(d).y);
278
    assert!(rect(d).x < rect(e).x);
279
    assert!(rect(a).y < rect(d).y);
280
}
281
282
#[test]
283
fn merging_steps_out_of_a_stack_before_joining_another() {
284
    let (a, b, c) = (1, 2, 3);
285
    let stacked = || {
286
        let mut t: Tree<u32> = Tree::default();
287
        t.insert_row(a);
288
        t.insert_after(b, a);
289
        t.insert_after(c, b);
290
        assert!(t.merge_window(b, Dir::Left, |_| true));
291
        t
292
    };
293
    let rects = |t: &Tree<u32>| {
294
        t.check();
295
        let all = t.arrange(AREA, 0.0, |_| true);
296
        move |w| all.iter().find(|p: &&(u32, Rect)| p.0 == w).unwrap().1
297
    };
298
299
    let mut t = stacked();
300
    assert!(t.merge_window(a, Dir::Right, |_| true));
301
    let rect = rects(&t);
302
    assert!(rect(b).x < rect(a).x && rect(a).x < rect(c).x);
303
    assert_eq!(rect(a).h, AREA.h);
304
    assert!(t.merge_window(a, Dir::Right, |_| true));
305
    let rect = rects(&t);
306
    assert_eq!(rect(a).x, rect(c).x);
307
308
    let mut t = stacked();
309
    assert!(t.merge_window(a, Dir::Up, |_| true));
310
    let rect = rects(&t);
311
    assert_eq!(rect(a).w, AREA.w);
312
    assert!(rect(a).bottom() <= rect(b).y && rect(b).y == rect(c).y);
313
}
314
315
/// A window alone on screen in a container of hidden ones expands among the
316
/// windows it is shown beside, not inside the container nobody can see.
317
#[test]
318
fn expand_skips_containers_of_hidden_windows() {
319
    let (a, b, c) = (1, 2, 3);
320
    let mut t: Tree<u32> = Tree::default();
321
    t.insert_row(a);
322
    t.insert_after(b, a);
323
    t.insert_after(c, b);
324
    assert!(t.merge_window(a, Dir::Right, |_| true));
325
    let shown = |w: u32| w != a;
326
    assert!(t.toggle_expand(b, 0, 1, shown));
327
    assert!(t.maximized(b, shown));
328
    let all = t.arrange(AREA, 0.0, shown);
329
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
330
    assert_eq!(rect(b).w, AREA.w);
331
}
332
333
/// Joining a stack hands the window's weight to it and leaving hands it back,
334
/// so going in and out repeatedly leaves every window the size it was.
335
#[test]
336
fn stacking_in_and_out_does_not_drift() {
337
    for out in [Dir::Up, Dir::Down, Dir::Right] {
338
        let (a, b, c) = (1, 2, 3);
339
        let mut t: Tree<u32> = Tree::default();
340
        t.insert_row(a);
341
        t.insert_after(b, a);
342
        t.insert_after(c, b);
343
        assert!(t.merge_window(c, Dir::Left, |_| true));
344
        assert!(t.merge_window(c, out, |_| true));
345
        let once = t.arrange(AREA, 0.0, |_| true);
346
        for _ in 0..8 {
347
            t.merge_window(c, Dir::Left, |_| true);
348
            t.merge_window(c, out, |_| true);
349
        }
350
        t.check();
351
        assert_eq!(t.arrange(AREA, 0.0, |_| true), once, "stepping out {out:?}");
352
    }
353
}