atagen

monolith

public
Star0
Watch0
3215aa6779fcb44b5b0bf6c5dc22cea57659a079
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/tests.rsRust957 lines32.2 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, bool),
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(), any::<bool>()).prop_map(|(d, m)| Op::Block(d, m)),
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, m) => {
86
            l.move_container(d, m);
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(
192
        ops in prop::collection::vec(op(), 1..80),
193
        autotile in any::<bool>(),
194
    ) {
195
        let mut l = Layout {
196
            autotile,
197
            ..Layout::default()
198
        };
199
        l.add_output("a", AREA);
200
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
201
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
202
        for op in &ops {
203
            let moving = matches!(op, Op::Move(_) | Op::Merge(_) | Op::Block(..));
204
            let shown_before = l.focused().filter(|&w| tiled_area(&l, w) > 1.0);
205
            apply(&mut l, &tags, op);
206
            l.check();
207
            check_geometry(&l);
208
            if moving && let Some(w) = shown_before {
209
                prop_assert!(tiled_area(&l, w) > 1.0, "{w:?} vanished after {op:?}");
210
            }
211
        }
212
    }
213
214
    #[test]
215
    fn replugging_restores_what_unplugging_moved(ops in prop::collection::vec(op(), 1..60)) {
216
        let mut l = Layout::default();
217
        l.add_output("a", AREA);
218
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
219
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
220
        for op in &ops {
221
            apply(&mut l, &tags, op);
222
        }
223
        let Some(b) = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o) else {
224
            return Ok(());
225
        };
226
        let before = |l: &Layout, o| {
227
            let out = l.output(o).unwrap();
228
            (out.tree.windows().collect::<Vec<_>>(), out.floating.clone(), out.viewed.clone())
229
        };
230
        let was = before(&l, b);
231
        l.remove_output(b);
232
        let b = l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
233
        l.check();
234
        prop_assert_eq!(before(&l, b), was);
235
    }
236
}
237
238
/// The hidden-slot rules from planner.html, demo 11.
239
#[test]
240
fn hidden_windows_keep_their_slots() {
241
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
242
    let mut t: Tree<u32> = Tree::default();
243
    t.insert_row(a);
244
    t.insert_after(d, a);
245
    t.insert_after(b, d);
246
    t.insert_row(c);
247
    t.insert_after(e, c);
248
    t.check();
249
    let hidden = |w: u32| w != d && w != e;
250
    let all = |_| true;
251
    let rect = |t: &Tree<u32>, w| {
252
        t.arrange(AREA, 0.0, all)
253
            .into_iter()
254
            .find(|p| p.0 == w)
255
            .unwrap()
256
            .1
257
    };
258
259
    assert!(t.move_window(a, Dir::Right, hidden));
260
    assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);
261
262
    assert!(t.move_window(c, Dir::Up, hidden));
263
    t.check();
264
    assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");
265
}
266
267
/// Moving out of a row toward a lane of hidden windows joins that lane, so
268
/// they share it when shown again instead of a new lane opening between.
269
#[test]
270
fn moving_toward_a_hidden_lane_joins_it() {
271
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
272
    let mut t: Tree<u32> = Tree::default();
273
    t.insert_row(a);
274
    t.insert_after(b, a);
275
    t.insert_after(c, b);
276
    t.insert_row(d);
277
    t.insert_after(e, c);
278
    let tag_two = |w: u32| w != a && w != d;
279
    assert!(t.move_window(e, Dir::Down, tag_two));
280
    t.check();
281
    let all = t.arrange(AREA, 0.0, |_| true);
282
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
283
    assert_eq!(rect(e).y, rect(d).y);
284
    assert!(rect(d).x < rect(e).x);
285
    assert!(rect(a).y < rect(d).y);
286
}
287
288
#[test]
289
fn merging_steps_out_of_a_stack_before_joining_another() {
290
    let (a, b, c) = (1, 2, 3);
291
    let stacked = || {
292
        let mut t: Tree<u32> = Tree::default();
293
        t.insert_row(a);
294
        t.insert_after(b, a);
295
        t.insert_after(c, b);
296
        assert!(t.merge_window(b, Dir::Left, |_| true));
297
        t
298
    };
299
    let rects = |t: &Tree<u32>| {
300
        t.check();
301
        let all = t.arrange(AREA, 0.0, |_| true);
302
        move |w| all.iter().find(|p: &&(u32, Rect)| p.0 == w).unwrap().1
303
    };
304
305
    let mut t = stacked();
306
    assert!(t.merge_window(a, Dir::Right, |_| true));
307
    let rect = rects(&t);
308
    assert!(rect(b).x < rect(a).x && rect(a).x < rect(c).x);
309
    assert_eq!(rect(a).h, AREA.h);
310
    assert!(t.merge_window(a, Dir::Right, |_| true));
311
    let rect = rects(&t);
312
    assert_eq!(rect(a).x, rect(c).x);
313
314
    let mut t = stacked();
315
    assert!(t.merge_window(a, Dir::Up, |_| true));
316
    let rect = rects(&t);
317
    assert_eq!(rect(a).w, AREA.w);
318
    assert!(rect(a).bottom() <= rect(b).y && rect(b).y == rect(c).y);
319
}
320
321
/// An extended window's group moves whole, even into a container running
322
/// its own way, where it would otherwise dissolve and show what it covers.
323
#[test]
324
fn extended_windows_move_their_group() {
325
    let (a, b, c) = (1, 2, 3);
326
    let all = |_| true;
327
    let mut t: Tree<u32> = Tree::default();
328
    t.insert_row(a);
329
    t.insert_after(b, a);
330
    t.insert_after(c, b);
331
    assert!(t.merge_window(b, Dir::Left, all));
332
    assert!(t.toggle_expand(b, 0, 1, all));
333
    let rects = |t: &Tree<u32>| {
334
        t.check();
335
        assert!(t.grouped(b, all) && t.eclipsed(a, all));
336
        let placed = t.arrange(AREA, 0.0, all);
337
        move |w| placed.iter().find(|p: &&(u32, Rect)| p.0 == w).unwrap().1
338
    };
339
340
    assert!(t.move_window(b, Dir::Right, all));
341
    let rect = rects(&t);
342
    assert!(rect(c).x < rect(b).x);
343
344
    assert!(t.move_window(b, Dir::Down, all));
345
    let rect = rects(&t);
346
    assert!(rect(c).bottom() <= rect(b).y && rect(b).w == AREA.w);
347
348
    assert!(t.merge_window(b, Dir::Up, all));
349
    let rect = rects(&t);
350
    assert!(rect(c).right() <= rect(b).x && rect(b).y == rect(c).y);
351
}
352
353
#[test]
354
fn retagging_an_extended_window_leaves_hidden_members() {
355
    let (a, b, c, d) = (1, 2, 3, 4);
356
    let shown = |w| w != d;
357
    let mut t: Tree<u32> = Tree::default();
358
    t.insert_row(a);
359
    t.insert_after(b, a);
360
    t.insert_after(c, b);
361
    t.insert_after(d, c);
362
    assert!(t.toggle_expand(b, 0, 1, shown));
363
    let mut group = t.group_windows(b, shown);
364
    group.sort();
365
    assert_eq!(group, [a, b, c]);
366
    assert_eq!(t.group_windows(a, shown), [a]);
367
}
368
369
/// A window alone on screen in a container of hidden ones expands among the
370
/// windows it is shown beside, not inside the container nobody can see.
371
#[test]
372
fn expand_skips_containers_of_hidden_windows() {
373
    let (a, b, c) = (1, 2, 3);
374
    let mut t: Tree<u32> = Tree::default();
375
    t.insert_row(a);
376
    t.insert_after(b, a);
377
    t.insert_after(c, b);
378
    assert!(t.merge_window(a, Dir::Right, |_| true));
379
    let shown = |w: u32| w != a;
380
    assert!(t.toggle_expand(b, 0, 1, shown));
381
    assert!(t.maximized(b, shown));
382
    let all = t.arrange(AREA, 0.0, shown);
383
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
384
    assert_eq!(rect(b).w, AREA.w);
385
}
386
387
/// Going in and out of a stack repeatedly leaves every window the size it was.
388
#[test]
389
fn stacking_in_and_out_does_not_drift() {
390
    for out in [Dir::Up, Dir::Down, Dir::Right] {
391
        let (a, b, c) = (1, 2, 3);
392
        let mut t: Tree<u32> = Tree::default();
393
        t.insert_row(a);
394
        t.insert_after(b, a);
395
        t.insert_after(c, b);
396
        assert!(t.merge_window(c, Dir::Left, |_| true));
397
        assert!(t.merge_window(c, out, |_| true));
398
        let once = t.arrange(AREA, 0.0, |_| true);
399
        for _ in 0..8 {
400
            t.merge_window(c, Dir::Left, |_| true);
401
            t.merge_window(c, out, |_| true);
402
        }
403
        t.check();
404
        assert_eq!(t.arrange(AREA, 0.0, |_| true), once, "stepping out {out:?}");
405
    }
406
}
407
408
/// Windows of other tags in the rows around a window must not change what
409
/// the user sees as it moves through them, nor drift over round trips.
410
#[test]
411
fn moving_past_hidden_windows_does_not_drift() {
412
    let (a, b, h, h2) = (1, 2, 3, 4);
413
    let mut t: Tree<u32> = Tree::default();
414
    t.insert_row(h);
415
    t.insert_row(a);
416
    t.insert_after(h2, a);
417
    t.insert_after(b, h2);
418
    let vis = |w: u32| w != h && w != h2;
419
    let mut once = None;
420
    for _ in 0..4 {
421
        for d in [Dir::Up, Dir::Up, Dir::Down] {
422
            t.move_window(b, d, vis);
423
            let shown = t.arrange(AREA, 0.0, vis);
424
            let r = shown.iter().find(|p| p.0 == b).unwrap().1;
425
            assert!(
426
                (r.w * r.h - AREA.w * AREA.h / 2.0).abs() < 1e-6,
427
                "{d:?} left {r:?}"
428
            );
429
        }
430
        t.check();
431
        let all = t.arrange(AREA, 0.0, |_| true);
432
        assert_eq!(*once.get_or_insert_with(|| all.clone()), all);
433
    }
434
}
435
436
/// Monitors sleeping together unplug one after another, so windows already
437
/// moved off the first must still go home to it, not to the second.
438
#[test]
439
fn windows_return_to_where_they_were_first_evicted_from() {
440
    let mut l = Layout::default();
441
    let a = l.add_output("a", AREA);
442
    l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
443
    let c = l.add_output("c", Rect::new(1800.0, 0.0, 800.0, 600.0));
444
    let w = l.add_window(Some(a), None, None);
445
    l.remove_output(a);
446
    let b = l.outputs().find(|(_, out)| out.name == "b").unwrap().0;
447
    l.remove_output(b);
448
    assert_eq!(l.window(w).unwrap().output, Some(c));
449
    l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
450
    let a = l.add_output("a", AREA);
451
    l.check();
452
    assert_eq!(l.window(w).unwrap().output, Some(a));
453
    assert!(l.output(a).unwrap().tree.contains(w));
454
}
455
456
/// A stack showing one window looks like a lone window, so moving toward it
457
/// swaps places instead of joining a stack the user cannot see.
458
#[test]
459
fn moving_toward_a_stack_showing_one_window_swaps() {
460
    let (a, b, h) = (1, 2, 3);
461
    let mut t: Tree<u32> = Tree::default();
462
    t.insert_row(a);
463
    t.insert_after(b, a);
464
    t.insert_after(h, b);
465
    assert!(t.merge_window(h, Dir::Left, |_| true));
466
    let vis = |w: u32| w != h;
467
    assert!(t.move_window(a, Dir::Right, vis));
468
    t.check();
469
    let shown = t.arrange(AREA, 0.0, vis);
470
    let rect = |w| shown.iter().find(|p| p.0 == w).unwrap().1;
471
    assert!(rect(b).x < rect(a).x);
472
    assert_eq!(rect(a).h, AREA.h);
473
}
474
475
/// A move either changes what is on screen or changes nothing at all, and a
476
/// window move never rearranges hidden windows among themselves. Steps around
477
/// hidden windows used to succeed invisibly and eat the keypress.
478
#[test]
479
fn moves_are_seen_or_undone() {
480
    let dirs = [Dir::Left, Dir::Right, Dir::Up, Dir::Down];
481
    let mut runner = proptest::test_runner::TestRunner::deterministic();
482
    let ops = prop::collection::vec((0u8..4, 0u8..4, 0u8..4, 0u8..32), 1..14);
483
    for _ in 0..2000 {
484
        let seq = proptest::strategy::ValueTree::current(&ops.new_tree(&mut runner).unwrap());
485
        let mut t: Tree<u32> = Tree::default();
486
        t.insert_row(0);
487
        let mut n = 1;
488
        for &(w, d, kind, hide) in &seq {
489
            let vis = |x: u32| hide & (1 << x) == 0;
490
            if kind == 0 && n < 5 {
491
                t.insert_after(n, u32::from(w) % n);
492
                n += 1;
493
                continue;
494
            }
495
            let w = u32::from(w) % n;
496
            let shown = |t: &Tree<u32>| {
497
                let mut v: Vec<_> = t
498
                    .arrange(AREA, 0.0, vis)
499
                    .into_iter()
500
                    .filter(|p| vis(p.0))
501
                    .collect();
502
                v.sort_by_key(|p| p.0);
503
                v
504
            };
505
            let before = shown(&t);
506
            let hidden = |t: &Tree<u32>| {
507
                let mut t = t.clone();
508
                t.retain(|x| !vis(x));
509
                t.shape()
510
            };
511
            let rest = hidden(&t);
512
            let d = dirs[usize::from(d)];
513
            let moved = match kind {
514
                1 => t.merge_window(w, d, vis),
515
                2 => t.move_window(w, d, vis),
516
                3 => t.move_container(w, d, false, vis),
517
                _ => continue,
518
            };
519
            t.check();
520
            assert_eq!(moved, shown(&t) != before, "{seq:?}");
521
            if kind != 3 {
522
                assert_eq!(hidden(&t), rest, "{seq:?}");
523
            }
524
        }
525
    }
526
}
527
528
/// A fullscreen window hides everything but its own transients, which stay
529
/// reachable by focus and are not hidden when they map.
530
#[test]
531
fn fullscreen_leaves_its_transients_reachable() {
532
    let mut l = Layout::default();
533
    let o = l.add_output("a", AREA);
534
    let beside = l.add_window(None, None, None);
535
    l.focus(beside);
536
    let game = l.add_window(None, None, None);
537
    l.focus(game);
538
    l.toggle_fullscreen(game);
539
    let stranger = l.add_window(None, None, None);
540
    assert!(l.covered(stranger));
541
    let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));
542
    l.set_parent(dialog, Some(game));
543
    assert!(!l.covered(dialog));
544
    l.focus(dialog);
545
546
    let arr = l.arrange(o);
547
    assert_eq!(arr.fullscreen, Some(game));
548
    assert_eq!(arr.over, vec![dialog]);
549
    for dir in [Dir::Left, Dir::Right, Dir::Up, Dir::Down] {
550
        if let Some(w) = l.focus_dir(dir) {
551
            assert!(w == game || w == dialog, "focus walked to a covered window");
552
        }
553
    }
554
555
    l.remove_window(game);
556
    assert!(!l.covered(stranger));
557
    assert_eq!(l.window(dialog).and_then(|w| w.parent), None);
558
}
559
560
fn rect_of(t: &Tree<u32>, w: u32, vis: impl Fn(u32) -> bool) -> Rect {
561
    t.arrange(AREA, 0.0, vis)
562
        .into_iter()
563
        .find(|p| p.0 == w)
564
        .unwrap()
565
        .1
566
}
567
568
#[test]
569
fn splitting_a_lone_window_shares_its_space() {
570
    let (a, b, c) = (1, 2, 3);
571
    let mut t: Tree<u32> = Tree::default();
572
    t.insert_row(a);
573
    t.insert_split(b, a, Axis::Horizontal);
574
    t.check();
575
    let (ra, rb) = (rect_of(&t, a, |_| true), rect_of(&t, b, |_| true));
576
    assert_eq!((ra.w, rb.w, ra.h), (AREA.w / 2.0, AREA.w / 2.0, AREA.h));
577
    assert!(ra.x < rb.x);
578
579
    t.insert_split(c, b, Axis::Vertical);
580
    t.check();
581
    let (rb, rc) = (rect_of(&t, b, |_| true), rect_of(&t, c, |_| true));
582
    assert_eq!(
583
        (rb.h, rc.h, rb.w),
584
        (AREA.h / 2.0, AREA.h / 2.0, AREA.w / 2.0)
585
    );
586
    assert!(rb.y < rc.y);
587
    assert_eq!(rect_of(&t, a, |_| true), ra, "the sibling is undisturbed");
588
}
589
590
#[test]
591
fn splitting_along_the_parents_axis_appends_a_sibling() {
592
    let (a, b, c, d) = (1, 2, 3, 4);
593
    let mut t: Tree<u32> = Tree::default();
594
    t.insert_row(a);
595
    t.insert_split(b, a, Axis::Horizontal);
596
    t.insert_split(c, b, Axis::Vertical);
597
    let before = rect_of(&t, a, |_| true);
598
    t.insert_split(d, a, Axis::Horizontal);
599
    t.check();
600
    let all = |_| true;
601
    let (ra, rd, rb) = (
602
        rect_of(&t, a, all),
603
        rect_of(&t, d, all),
604
        rect_of(&t, b, all),
605
    );
606
    assert_eq!((ra.w, rd.w), (before.w / 2.0, before.w / 2.0));
607
    assert!(ra.x < rd.x && rd.x < rb.x);
608
    assert_eq!(rb.w, AREA.w / 2.0, "the neighbour keeps its share");
609
}
610
611
#[test]
612
fn splitting_beside_hidden_windows_leaves_their_slots_alone() {
613
    let (a, b, c, d) = (1, 2, 3, 4);
614
    let mut t: Tree<u32> = Tree::default();
615
    t.insert_row(a);
616
    t.insert_after(b, a);
617
    t.insert_after(c, a);
618
    let tag_one = |w: u32| w == a || w == d;
619
    assert_eq!(rect_of(&t, a, tag_one).h, AREA.h);
620
621
    t.insert_split(d, a, Axis::Vertical);
622
    t.check();
623
    let (ra, rd) = (rect_of(&t, a, tag_one), rect_of(&t, d, tag_one));
624
    assert_eq!((ra.h, rd.h, ra.w), (AREA.h / 2.0, AREA.h / 2.0, AREA.w));
625
    assert!(ra.y < rd.y);
626
627
    t.insert_split(d + 1, a, Axis::Horizontal);
628
    t.check();
629
    let shown = |w: u32| w == a || w == d + 1;
630
    let (ra, re) = (rect_of(&t, a, shown), rect_of(&t, d + 1, shown));
631
    assert_eq!((ra.w, re.w), (AREA.w / 2.0, AREA.w / 2.0));
632
    assert!(ra.x < re.x);
633
    assert_eq!(t.windows().count(), 5, "hidden windows stay in the tree");
634
}
635
636
#[test]
637
fn autotile_dwindles_by_the_anchors_shape() {
638
    let mut l = Layout {
639
        autotile: true,
640
        ..Layout::default()
641
    };
642
    let o = l.add_output("a", AREA);
643
    let tag = l.tag("1");
644
    l.view(o, tag);
645
    let mut ids = Vec::new();
646
    for _ in 0..4 {
647
        let w = l.add_window(Some(o), Some(tag), None);
648
        l.focus(w);
649
        ids.push(w);
650
        l.check();
651
    }
652
    let arr = l.arrange(o);
653
    let r = |w| arr.tiled.iter().find(|p| p.window == w).unwrap().rect;
654
    let [a, b, c, d] = [ids[0], ids[1], ids[2], ids[3]];
655
    assert!(
656
        r(a).x < r(b).x && r(a).h > r(b).h * 1.5,
657
        "{a:?} left, {b:?} right"
658
    );
659
    assert!(r(b).y < r(c).y && r(b).x == r(c).x);
660
    assert!(r(c).x < r(d).x && r(c).y == r(d).y);
661
}
662
663
#[test]
664
fn urgency_skips_the_focused_window_and_ends_with_focus() {
665
    let mut l = Layout::default();
666
    let o = l.add_output("a", AREA);
667
    let tag = l.tag("1");
668
    l.view(o, tag);
669
    let a = l.add_window(Some(o), Some(tag), None);
670
    let b = l.add_window(Some(o), Some(tag), None);
671
    l.focus(a);
672
    l.mark_urgent(a);
673
    l.mark_urgent(b);
674
    assert!(!l.window(a).unwrap().urgent);
675
    assert!(l.window(b).unwrap().urgent);
676
    assert!(!l.tags().any(|(_, t)| t.urgent));
677
    l.focus(b);
678
    assert!(!l.window(b).unwrap().urgent);
679
}
680
681
#[test]
682
fn oversized_floats_keep_their_top_edge_and_a_strip() {
683
    let bounds = Rect::new(0.0, 0.0, 1000.0, 600.0);
684
    let huge = Rect::new(-5000.0, -300.0, 3000.0, 2000.0);
685
    let r = clamp_float(huge, bounds);
686
    assert_eq!((r.x, r.y), (-2936.0, 0.0));
687
    let r = clamp_float(Rect::new(5000.0, 5000.0, 3000.0, 2000.0), bounds);
688
    assert_eq!((r.x, r.y), (936.0, 536.0));
689
    let fits = clamp_float(Rect::new(900.0, 500.0, 300.0, 200.0), bounds);
690
    assert_eq!((fits.x, fits.y), (700.0, 400.0));
691
}
692
693
#[test]
694
fn floats_follow_their_output_area_and_panels() {
695
    let mut l = Layout::default();
696
    let o = l.add_output("a", AREA);
697
    let w = l.add_window(Some(o), None, Some(Rect::new(500.0, 300.0, 400.0, 300.0)));
698
    l.set_output_area(o, Rect::new(100.0, 50.0, 500.0, 300.0));
699
    let r = l.window(w).unwrap().float_rect.unwrap();
700
    assert_eq!((r.x, r.y, r.w, r.h), (350.0, 200.0, 200.0, 150.0));
701
702
    l.set_reserved(
703
        o,
704
        Insets {
705
            top: 100.0,
706
            ..Insets::default()
707
        },
708
    );
709
    let r = l.window(w).unwrap().float_rect.unwrap();
710
    let usable = l.output(o).unwrap().usable();
711
    assert!(r.y >= usable.y && r.bottom() <= usable.bottom());
712
    assert_eq!((r.w, r.h), (200.0, 150.0), "a panel never shrinks a float");
713
}
714
715
#[test]
716
fn floats_shrink_into_a_smaller_output_they_are_evicted_to() {
717
    let mut l = Layout::default();
718
    let a = l.add_output("a", AREA);
719
    let b = l.add_output("b", Rect::new(1000.0, 0.0, 500.0, 300.0));
720
    let w = l.add_window(Some(a), None, Some(Rect::new(800.0, 400.0, 200.0, 200.0)));
721
    l.remove_output(a);
722
    assert_eq!(l.window(w).unwrap().output, Some(b));
723
    let r = l.window(w).unwrap().float_rect.unwrap();
724
    let usable = l.output(b).unwrap().usable();
725
    assert_eq!((r.w, r.h), (100.0, 100.0));
726
    assert!(r.x >= usable.x && r.right() <= usable.right());
727
    assert!(r.y >= usable.y && r.bottom() <= usable.bottom());
728
}
729
730
/// Dragging a split moves only the two tiles that meet at it, whichever of
731
/// them the drag started from, and stops where one would vanish.
732
#[test]
733
fn dragging_a_split_moves_just_that_split() {
734
    let mut l = Layout::default();
735
    let o = l.add_output("a", AREA);
736
    let a = l.add_window(Some(o), None, None);
737
    let b = l.add_window(Some(o), None, None);
738
    let c = l.add_window(Some(o), None, None);
739
    let rect = |l: &Layout, w| {
740
        l.arrange(o)
741
            .tiled
742
            .iter()
743
            .find(|p| p.window == w)
744
            .unwrap()
745
            .rect
746
    };
747
    let before = [rect(&l, a), rect(&l, b), rect(&l, c)];
748
    assert!(!l.has_split(a, Dir::Up));
749
    assert!(!l.move_split(a, Dir::Up, 50.0));
750
    assert!(l.has_split(a, Dir::Down));
751
752
    assert!(l.move_split(a, Dir::Down, 40.0));
753
    let (ra, rb) = (rect(&l, a), rect(&l, b));
754
    assert!((ra.h - before[0].h - 40.0).abs() < 1e-6);
755
    assert!((rb.h - before[1].h + 40.0).abs() < 1e-6);
756
    assert_eq!(rect(&l, c).h, before[2].h);
757
758
    assert!(l.move_split(b, Dir::Up, -40.0));
759
    assert!((rect(&l, a).h - before[0].h).abs() < 1e-6);
760
761
    l.move_split(a, Dir::Down, 1e6);
762
    assert!(rect(&l, b).h > 1.0);
763
    l.check();
764
}
765
766
#[test]
767
fn scratchpads_stay_centred_whatever_their_rect() {
768
    let mut l = Layout::default();
769
    let o = l.add_output("a", AREA);
770
    let w = l.add_window(Some(o), None, Some(Rect::new(10.0, 20.0, 300.0, 200.0)));
771
    l.scratchpad_add(w, "s");
772
    l.summon("s");
773
    l.set_float_rect(w, Rect::new(0.0, 0.0, 500.0, 5000.0));
774
    let r = l.arrange(o).floating.iter().find(|p| p.0 == w).unwrap().1;
775
    let area = l.output(o).unwrap().usable();
776
    assert_eq!((r.w, r.h), (500.0, area.h));
777
    assert_eq!(r.x + r.w / 2.0, area.x + area.w / 2.0);
778
    assert_eq!(r.y, area.y);
779
}
780
781
/// Focus aimed at a window with a modal dialog lands on the deepest dialog
782
/// from every path, and falls back to the parent once the dialog is gone.
783
#[test]
784
fn modal_dialogs_keep_focus_over_their_parent() {
785
    let mut l = Layout::default();
786
    l.add_output("a", AREA);
787
    let parent = l.add_window(None, None, None);
788
    let other = l.add_window(None, None, None);
789
    l.focus(parent);
790
    let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));
791
    let nested = l.add_window(None, None, Some(Rect::new(350.0, 250.0, 100.0, 50.0)));
792
    l.set_parent(dialog, Some(parent));
793
    l.set_parent(nested, Some(dialog));
794
    l.set_modal(dialog, true);
795
    assert_eq!(l.focused(), Some(dialog));
796
    l.set_modal(nested, true);
797
    assert_eq!(l.focused(), Some(nested));
798
799
    l.focus(other);
800
    assert_eq!(l.focused(), Some(other));
801
    assert_eq!(l.focus(parent), Some(nested));
802
    assert_eq!(l.focused(), Some(nested));
803
    assert!(l.is_focused(parent));
804
805
    l.focus(other);
806
    for dir in [Dir::Left, Dir::Right, Dir::Up, Dir::Down] {
807
        l.focus(other);
808
        if let Some(w) = l.focus_dir(dir) {
809
            assert_ne!(w, parent, "directional focus stopped on a blocked parent");
810
        }
811
    }
812
813
    l.focus(parent);
814
    l.remove_window(nested);
815
    assert_eq!(l.focused(), Some(dialog));
816
    l.remove_window(dialog);
817
    assert_eq!(l.focused(), Some(parent));
818
    l.check();
819
}
820
821
/// A fullscreen window nobody ever focused, as a rule with `focus = false`
822
/// makes, still takes the output once its tag is in view.
823
#[test]
824
fn fullscreen_does_not_need_focus_history() {
825
    let mut l = Layout::default();
826
    let o = l.add_output("a", AREA);
827
    let (main, other) = (l.tag("main"), l.tag("other"));
828
    l.view(o, main);
829
    let shown = l.add_window(None, Some(main), None);
830
    let player = l.add_window(None, Some(main), None);
831
    l.toggle_fullscreen(player);
832
    assert_eq!(l.arrange(o).fullscreen, Some(player));
833
    assert!(l.covered(shown));
834
835
    let hidden = l.add_window(None, Some(other), None);
836
    l.toggle_fullscreen(hidden);
837
    assert_eq!(l.arrange(o).fullscreen, Some(player));
838
    l.focus(shown);
839
    l.view(o, other);
840
    assert_eq!(l.arrange(o).fullscreen, Some(hidden));
841
}
842
843
/// A request to focus a covered window flags it instead, but its own
844
/// fullscreen window's transients and a summoned scratchpad take the focus.
845
#[test]
846
fn covered_windows_are_flagged_not_focused() {
847
    let mut l = Layout::default();
848
    let o = l.add_output("a", AREA);
849
    let game = l.add_window(None, None, None);
850
    l.focus(game);
851
    l.toggle_fullscreen(game);
852
    let stranger = l.add_window(None, None, None);
853
    assert!(!l.focus_unless_covered(stranger));
854
    assert_eq!(l.focused(), Some(game));
855
    assert!(l.window(stranger).unwrap().urgent);
856
857
    let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));
858
    l.set_parent(dialog, Some(game));
859
    assert!(l.focus_unless_covered(dialog));
860
    assert_eq!(l.focused(), Some(dialog));
861
862
    let pad = l.add_window(None, None, None);
863
    l.scratchpad_add(pad, "s");
864
    assert_eq!(l.summon("s"), Some(pad));
865
    assert_eq!(l.focused(), Some(pad));
866
    assert!(l.arrange(o).over.contains(&pad));
867
    l.check();
868
}
869
870
/// A window its client hid keeps its place, floating state, parent links and
871
/// scratchpad, and comes back as it was when shown again.
872
#[test]
873
fn hidden_windows_come_back_as_they_were() {
874
    let mut l = Layout::default();
875
    let o = l.add_output("a", AREA);
876
    let tag = l.tag("work");
877
    l.view(o, tag);
878
    let tray = l.add_window(None, Some(tag), Some(Rect::new(40.0, 50.0, 300.0, 200.0)));
879
    let child = l.add_window(None, Some(tag), Some(Rect::new(60.0, 70.0, 100.0, 80.0)));
880
    let tiled = l.add_window(None, Some(tag), None);
881
    l.set_parent(child, Some(tray));
882
    l.toggle_sticky(tray);
883
    l.focus(tray);
884
    l.focus(tiled);
885
886
    l.set_hidden(tray, true);
887
    assert!(!l.visible(tray));
888
    assert!(l.arrange(o).floating.iter().all(|f| f.0 != tray));
889
    assert!(!l.focus_unless_covered(tray));
890
    assert!(!l.window(tray).unwrap().urgent);
891
    assert_eq!(l.focused(), Some(tiled));
892
    assert_eq!(l.window(child).and_then(|w| w.parent), Some(tray));
893
894
    l.set_hidden(tray, false);
895
    let w = l.window(tray).unwrap();
896
    assert!(l.visible(tray));
897
    assert!(w.floating && w.sticky && w.tag == tag);
898
    assert_eq!(w.float_rect, Some(Rect::new(40.0, 50.0, 300.0, 200.0)));
899
    assert!(l.arrange(o).floating.iter().any(|f| f.0 == tray));
900
    l.check();
901
}
902
903
/// Summoning skips a scratchpad window that is hidden to a tray.
904
#[test]
905
fn summoning_skips_hidden_scratchpad_windows() {
906
    let mut l = Layout::default();
907
    let o = l.add_output("a", AREA);
908
    let a = l.add_window(None, None, None);
909
    let b = l.add_window(None, None, None);
910
    l.scratchpad_add(a, "s");
911
    l.scratchpad_add(b, "s");
912
    l.set_hidden(a, true);
913
    assert_eq!(l.summon("s"), Some(b));
914
    assert!(l.arrange(o).floating.iter().all(|f| f.0 != a));
915
    assert_eq!(l.summon("s"), None);
916
}
917
918
/// A modal dialog that takes focus on mapping puts its parent just beneath
919
/// it, so closing it does not land on whatever was focused before the parent.
920
#[test]
921
fn a_modal_brings_its_parent_up_beneath_it() {
922
    let mut l = Layout::default();
923
    l.add_output("a", AREA);
924
    let parent = l.add_window(None, None, None);
925
    let other = l.add_window(None, None, None);
926
    l.focus(parent);
927
    l.focus(other);
928
    let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));
929
    l.set_parent(dialog, Some(parent));
930
    l.set_modal(dialog, true);
931
    l.focus(dialog);
932
    l.remove_window(dialog);
933
    assert_eq!(l.focused(), Some(parent));
934
}
935
936
/// Floats start inside what the bars leave of their output, whether they came
937
/// with a rect hanging off its edge or take the default one.
938
#[test]
939
fn new_floats_land_inside_the_bars() {
940
    let mut l = Layout::default();
941
    let o = l.add_output("a", AREA);
942
    let bar = Insets {
943
        top: 40.0,
944
        ..Insets::default()
945
    };
946
    l.set_reserved(o, bar);
947
    let usable = l.output(o).unwrap().usable();
948
949
    let hanging = l.add_window(None, None, Some(Rect::new(900.0, -30.0, 300.0, 200.0)));
950
    let r = l.window(hanging).and_then(|w| w.float_rect).unwrap();
951
    assert!(r.x >= usable.x && r.right() <= usable.right() && r.y >= usable.y);
952
953
    let plain = l.add_window(None, None, None);
954
    l.toggle_floating(plain);
955
    let r = l.window(plain).and_then(|w| w.float_rect).unwrap();
956
    assert_eq!(r.y + r.h / 2.0, usable.y + usable.h / 2.0);
957
}