atagen

monolith

public
Star0
Watch0
cabbef6c998a4bb3df73485c11df310f73adf1c0
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/tests.rsRust220 lines6.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
fn check_geometry(l: &Layout) {
132
    for (o, out) in l.outputs() {
133
        let arr = l.arrange(o);
134
        let shown: Vec<Rect> = arr
135
            .tiled
136
            .iter()
137
            .filter(|p| p.visible)
138
            .map(|p| p.rect)
139
            .collect();
140
        for r in &shown {
141
            assert!(r.w >= 0.0 && r.h >= 0.0);
142
            assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6);
143
            assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6);
144
        }
145
        for (i, a) in shown.iter().enumerate() {
146
            for b in &shown[i + 1..] {
147
                let ox = a.right().min(b.right()) - a.x.max(b.x);
148
                let oy = a.bottom().min(b.bottom()) - a.y.max(b.y);
149
                assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}");
150
            }
151
        }
152
    }
153
}
154
155
proptest! {
156
    #![proptest_config(ProptestConfig::with_cases(2000))]
157
158
    #[test]
159
    fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {
160
        let mut l = Layout::default();
161
        l.add_output("a", AREA);
162
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
163
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
164
        for op in &ops {
165
            apply(&mut l, &tags, op);
166
            l.check();
167
            check_geometry(&l);
168
        }
169
    }
170
}
171
172
/// The hidden-slot rules from planner.html, demo 11.
173
#[test]
174
fn hidden_windows_keep_their_slots() {
175
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
176
    let mut t: Tree<u32> = Tree::default();
177
    t.insert_row(a);
178
    t.insert_after(d, a);
179
    t.insert_after(b, d);
180
    t.insert_row(c);
181
    t.insert_after(e, c);
182
    t.check();
183
    let hidden = |w: u32| w != d && w != e;
184
    let all = |_| true;
185
    let rect = |t: &Tree<u32>, w| {
186
        t.arrange(AREA, 0.0, all)
187
            .into_iter()
188
            .find(|p| p.0 == w)
189
            .unwrap()
190
            .1
191
    };
192
193
    assert!(t.move_window(a, Dir::Right, hidden));
194
    assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);
195
196
    assert!(t.move_window(c, Dir::Up, hidden));
197
    t.check();
198
    assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");
199
}
200
201
/// Moving out of a row toward a lane of hidden windows joins that lane, so
202
/// they share it when shown again instead of a new lane opening between.
203
#[test]
204
fn moving_toward_a_hidden_lane_joins_it() {
205
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
206
    let mut t: Tree<u32> = Tree::default();
207
    t.insert_row(a);
208
    t.insert_after(b, a);
209
    t.insert_after(c, b);
210
    t.insert_row(d);
211
    t.insert_after(e, c);
212
    let tag_two = |w: u32| w != a && w != d;
213
    assert!(t.move_window(e, Dir::Down, tag_two));
214
    t.check();
215
    let all = t.arrange(AREA, 0.0, |_| true);
216
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
217
    assert_eq!(rect(e).y, rect(d).y);
218
    assert!(rect(d).x < rect(e).x);
219
    assert!(rect(a).y < rect(d).y);
220
}