| 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 mut drawn: Vec<WindowId> = arr |
| 135 | .tiled |
| 136 | .iter() |
| 137 | .filter(|p| p.visible) |
| 138 | .map(|p| p.window) |
| 139 | .chain(arr.floating.iter().map(|&(w, _)| w)) |
| 140 | .chain(arr.fullscreen) |
| 141 | .collect(); |
| 142 | drawn.sort(); |
| 143 | drawn.dedup(); |
| 144 | let mut visible: Vec<WindowId> = l |
| 145 | .windows() |
| 146 | .filter(|&(w, win)| win.output == Some(o) && l.visible(w)) |
| 147 | .map(|(w, _)| w) |
| 148 | .collect(); |
| 149 | visible.sort(); |
| 150 | assert_eq!(drawn, visible, "arrangement disagrees with visible()"); |
| 151 | let shown: Vec<Rect> = arr |
| 152 | .tiled |
| 153 | .iter() |
| 154 | .filter(|p| p.visible) |
| 155 | .map(|p| p.rect) |
| 156 | .collect(); |
| 157 | for p in arr.tiled.iter().filter(|p| !p.visible) { |
| 158 | assert!(p.rect.w * p.rect.h <= 1e-6, "hidden {:?} has area", p.rect); |
| 159 | } |
| 160 | for r in &shown { |
| 161 | assert!(r.w >= 0.0 && r.h >= 0.0); |
| 162 | assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6); |
| 163 | assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6); |
| 164 | } |
| 165 | for (i, a) in shown.iter().enumerate() { |
| 166 | for b in &shown[i + 1..] { |
| 167 | let ox = a.right().min(b.right()) - a.x.max(b.x); |
| 168 | let oy = a.bottom().min(b.bottom()) - a.y.max(b.y); |
| 169 | assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}"); |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | proptest! { |
| 176 | #![proptest_config(ProptestConfig::with_cases(2000))] |
| 177 | |
| 178 | #[test] |
| 179 | fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) { |
| 180 | let mut l = Layout::default(); |
| 181 | l.add_output("a", AREA); |
| 182 | l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0)); |
| 183 | let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")]; |
| 184 | for op in &ops { |
| 185 | apply(&mut l, &tags, op); |
| 186 | l.check(); |
| 187 | check_geometry(&l); |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | #[test] |
| 192 | fn replugging_restores_what_unplugging_moved(ops in prop::collection::vec(op(), 1..60)) { |
| 193 | let mut l = Layout::default(); |
| 194 | l.add_output("a", AREA); |
| 195 | l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0)); |
| 196 | let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")]; |
| 197 | for op in &ops { |
| 198 | apply(&mut l, &tags, op); |
| 199 | } |
| 200 | let Some(b) = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o) else { |
| 201 | return Ok(()); |
| 202 | }; |
| 203 | let before = |l: &Layout, o| { |
| 204 | let out = l.output(o).unwrap(); |
| 205 | (out.tree.windows().collect::<Vec<_>>(), out.floating.clone(), out.viewed.clone()) |
| 206 | }; |
| 207 | let was = before(&l, b); |
| 208 | l.remove_output(b); |
| 209 | let b = l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0)); |
| 210 | l.check(); |
| 211 | prop_assert_eq!(before(&l, b), was); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | /// The hidden-slot rules from planner.html, demo 11. |
| 216 | #[test] |
| 217 | fn hidden_windows_keep_their_slots() { |
| 218 | let (a, b, c, d, e) = (1, 2, 3, 4, 5); |
| 219 | let mut t: Tree<u32> = Tree::default(); |
| 220 | t.insert_row(a); |
| 221 | t.insert_after(d, a); |
| 222 | t.insert_after(b, d); |
| 223 | t.insert_row(c); |
| 224 | t.insert_after(e, c); |
| 225 | t.check(); |
| 226 | let hidden = |w: u32| w != d && w != e; |
| 227 | let all = |_| true; |
| 228 | let rect = |t: &Tree<u32>, w| { |
| 229 | t.arrange(AREA, 0.0, all) |
| 230 | .into_iter() |
| 231 | .find(|p| p.0 == w) |
| 232 | .unwrap() |
| 233 | .1 |
| 234 | }; |
| 235 | |
| 236 | assert!(t.move_window(a, Dir::Right, hidden)); |
| 237 | assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x); |
| 238 | |
| 239 | assert!(t.move_window(c, Dir::Up, hidden)); |
| 240 | t.check(); |
| 241 | assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own"); |
| 242 | } |
| 243 | |
| 244 | /// Moving out of a row toward a lane of hidden windows joins that lane, so |
| 245 | /// they share it when shown again instead of a new lane opening between. |
| 246 | #[test] |
| 247 | fn moving_toward_a_hidden_lane_joins_it() { |
| 248 | let (a, b, c, d, e) = (1, 2, 3, 4, 5); |
| 249 | let mut t: Tree<u32> = Tree::default(); |
| 250 | t.insert_row(a); |
| 251 | t.insert_after(b, a); |
| 252 | t.insert_after(c, b); |
| 253 | t.insert_row(d); |
| 254 | t.insert_after(e, c); |
| 255 | let tag_two = |w: u32| w != a && w != d; |
| 256 | assert!(t.move_window(e, Dir::Down, tag_two)); |
| 257 | t.check(); |
| 258 | let all = t.arrange(AREA, 0.0, |_| true); |
| 259 | let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1; |
| 260 | assert_eq!(rect(e).y, rect(d).y); |
| 261 | assert!(rect(d).x < rect(e).x); |
| 262 | assert!(rect(a).y < rect(d).y); |
| 263 | } |
| 264 | |
| 265 | #[test] |
| 266 | fn merging_steps_out_of_a_stack_before_joining_another() { |
| 267 | let (a, b, c) = (1, 2, 3); |
| 268 | let stacked = || { |
| 269 | let mut t: Tree<u32> = Tree::default(); |
| 270 | t.insert_row(a); |
| 271 | t.insert_after(b, a); |
| 272 | t.insert_after(c, b); |
| 273 | assert!(t.merge_window(b, Dir::Left, |_| true)); |
| 274 | t |
| 275 | }; |
| 276 | let rects = |t: &Tree<u32>| { |
| 277 | t.check(); |
| 278 | let all = t.arrange(AREA, 0.0, |_| true); |
| 279 | move |w| all.iter().find(|p: &&(u32, Rect)| p.0 == w).unwrap().1 |
| 280 | }; |
| 281 | |
| 282 | let mut t = stacked(); |
| 283 | assert!(t.merge_window(a, Dir::Right, |_| true)); |
| 284 | let rect = rects(&t); |
| 285 | assert!(rect(b).x < rect(a).x && rect(a).x < rect(c).x); |
| 286 | assert_eq!(rect(a).h, AREA.h); |
| 287 | assert!(t.merge_window(a, Dir::Right, |_| true)); |
| 288 | let rect = rects(&t); |
| 289 | assert_eq!(rect(a).x, rect(c).x); |
| 290 | |
| 291 | let mut t = stacked(); |
| 292 | assert!(t.merge_window(a, Dir::Up, |_| true)); |
| 293 | let rect = rects(&t); |
| 294 | assert_eq!(rect(a).w, AREA.w); |
| 295 | assert!(rect(a).bottom() <= rect(b).y && rect(b).y == rect(c).y); |
| 296 | } |
| 297 | |
| 298 | /// A window alone on screen in a container of hidden ones expands among the |
| 299 | /// windows it is shown beside, not inside the container nobody can see. |
| 300 | #[test] |
| 301 | fn expand_skips_containers_of_hidden_windows() { |
| 302 | let (a, b, c) = (1, 2, 3); |
| 303 | let mut t: Tree<u32> = Tree::default(); |
| 304 | t.insert_row(a); |
| 305 | t.insert_after(b, a); |
| 306 | t.insert_after(c, b); |
| 307 | assert!(t.merge_window(a, Dir::Right, |_| true)); |
| 308 | let shown = |w: u32| w != a; |
| 309 | assert!(t.toggle_expand(b, 0, 1, shown)); |
| 310 | assert!(t.maximized(b, shown)); |
| 311 | let all = t.arrange(AREA, 0.0, shown); |
| 312 | let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1; |
| 313 | assert_eq!(rect(b).w, AREA.w); |
| 314 | } |