atagen

monolith

public
Star0
Watch0
9944e8618a62a996bdd1b8ec56a43ae0f9946e3f
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/tests.rsRust399 lines13.0 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
/// Going in and out of a stack repeatedly leaves every window the size it was.
334
#[test]
335
fn stacking_in_and_out_does_not_drift() {
336
    for out in [Dir::Up, Dir::Down, Dir::Right] {
337
        let (a, b, c) = (1, 2, 3);
338
        let mut t: Tree<u32> = Tree::default();
339
        t.insert_row(a);
340
        t.insert_after(b, a);
341
        t.insert_after(c, b);
342
        assert!(t.merge_window(c, Dir::Left, |_| true));
343
        assert!(t.merge_window(c, out, |_| true));
344
        let once = t.arrange(AREA, 0.0, |_| true);
345
        for _ in 0..8 {
346
            t.merge_window(c, Dir::Left, |_| true);
347
            t.merge_window(c, out, |_| true);
348
        }
349
        t.check();
350
        assert_eq!(t.arrange(AREA, 0.0, |_| true), once, "stepping out {out:?}");
351
    }
352
}
353
354
/// Windows of other tags in the rows around a window must not change what
355
/// the user sees as it moves through them, nor drift over round trips.
356
#[test]
357
fn moving_past_hidden_windows_does_not_drift() {
358
    let (a, b, h, h2) = (1, 2, 3, 4);
359
    let mut t: Tree<u32> = Tree::default();
360
    t.insert_row(h);
361
    t.insert_row(a);
362
    t.insert_after(h2, a);
363
    t.insert_after(b, h2);
364
    let vis = |w: u32| w != h && w != h2;
365
    let once = t.arrange(AREA, 0.0, |_| true);
366
    for _ in 0..4 {
367
        for d in [Dir::Up, Dir::Up, Dir::Down] {
368
            t.move_window(b, d, vis);
369
            let shown = t.arrange(AREA, 0.0, vis);
370
            let r = shown.iter().find(|p| p.0 == b).unwrap().1;
371
            assert!(
372
                (r.w * r.h - AREA.w * AREA.h / 2.0).abs() < 1e-6,
373
                "{d:?} left {r:?}"
374
            );
375
        }
376
        t.check();
377
    }
378
    assert_eq!(t.arrange(AREA, 0.0, |_| true), once);
379
}
380
381
/// Monitors sleeping together unplug one after another, so windows already
382
/// moved off the first must still go home to it, not to the second.
383
#[test]
384
fn windows_return_to_where_they_were_first_evicted_from() {
385
    let mut l = Layout::default();
386
    let a = l.add_output("a", AREA);
387
    l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
388
    let c = l.add_output("c", Rect::new(1800.0, 0.0, 800.0, 600.0));
389
    let w = l.add_window(Some(a), None, None);
390
    l.remove_output(a);
391
    let b = l.outputs().find(|(_, out)| out.name == "b").unwrap().0;
392
    l.remove_output(b);
393
    assert_eq!(l.window(w).unwrap().output, Some(c));
394
    l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
395
    let a = l.add_output("a", AREA);
396
    l.check();
397
    assert_eq!(l.window(w).unwrap().output, Some(a));
398
    assert!(l.output(a).unwrap().tree.contains(w));
399
}