atagen

monolith

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