atagen

monolith

public
Star0
Watch0

Commit 8f1f743a2f6b

tarball

atagen <boss@atagen.co>2026-10-19 03:49 UTC

2 files changed+102-16
layout: repeat a move until it shows, or undo it

Steps around hidden windows, like stepping out beside a lane of another tag or escaping to the root next to nothing visible, changed the tree without changing the screen. The keypress looked dropped and rearranged windows the user could not see. move_window, merge_window and move_container now keep stepping until the visible layout changes and restore the tree when it never does.
Modified file: crates/layout/src/tests.rs+41 additions
@@ -417,3 +417,44 @@ fn moving_toward_a_stack_showing_one_window_swaps() {
417417 assert!(rect(b).x < rect(a).x);
418418 assert_eq!(rect(a).h, AREA.h);
419419 }
420+
421+/// A move either changes what is on screen or changes nothing at all. Steps
422+/// around hidden windows used to succeed invisibly and eat the keypress.
423+#[test]
424+fn moves_are_seen_or_undone() {
425+ let dirs = [Dir::Left, Dir::Right, Dir::Up, Dir::Down];
426+ let mut runner = proptest::test_runner::TestRunner::deterministic();
427+ let ops = prop::collection::vec((0u8..4, 0u8..4, 0u8..3, any::<bool>()), 1..14);
428+ for _ in 0..2000 {
429+ let seq = proptest::strategy::ValueTree::current(&ops.new_tree(&mut runner).unwrap());
430+ let mut t: Tree<u32> = Tree::default();
431+ t.insert_row(0);
432+ let mut n = 1;
433+ for &(w, d, kind, hide) in &seq {
434+ let vis = |x: u32| !(hide && x == 0);
435+ if kind == 0 && n < 5 {
436+ t.insert_after(n, u32::from(w) % n);
437+ n += 1;
438+ continue;
439+ }
440+ let w = u32::from(w) % n;
441+ let shown = |t: &Tree<u32>| {
442+ let mut v: Vec<_> = t
443+ .arrange(AREA, 0.0, vis)
444+ .into_iter()
445+ .filter(|p| vis(p.0))
446+ .collect();
447+ v.sort_by_key(|p| p.0);
448+ v
449+ };
450+ let before = shown(&t);
451+ let moved = if kind == 1 {
452+ t.merge_window(w, dirs[usize::from(d)], vis)
453+ } else {
454+ t.move_window(w, dirs[usize::from(d)], vis)
455+ };
456+ t.check();
457+ assert_eq!(moved, shown(&t) != before, "{seq:?}");
458+ }
459+ }
460+}
Modified file: crates/layout/src/tree.rs+61 additions−16 deletions
@@ -190,8 +190,10 @@ impl<W: Copy + Eq + Hash> Tree<W> {
190190 let Some(&n) = self.leaves.get(&w) else {
191191 return false;
192192 };
193- let moved = self.move_node(n, dir, &vis, true);
194- self.reveal(n, moved)
193+ self.until_seen(&vis, |t| {
194+ let moved = t.move_node(n, dir, &vis, true);
195+ t.reveal(n, moved)
196+ })
195197 }
196198
197199 /// Moves the container holding `w` as a rigid block. A window sitting
@@ -200,12 +202,14 @@ impl<W: Copy + Eq + Hash> Tree<W> {
200202 let Some(&n) = self.leaves.get(&w) else {
201203 return false;
202204 };
203- let block = self.nodes[n]
204- .parent
205- .filter(|&p| Some(p) != self.root)
206- .unwrap_or(n);
207- let moved = self.move_node(block, dir, &vis, false);
208- self.reveal(n, moved)
205+ self.until_seen(&vis, |t| {
206+ let block = t.nodes[n]
207+ .parent
208+ .filter(|&p| Some(p) != t.root)
209+ .unwrap_or(n);
210+ let moved = t.move_node(block, dir, &vis, false);
211+ t.reveal(n, moved)
212+ })
209213 }
210214
211215 /// Moves a window to the end of the next visible lane in `dir` among its
@@ -216,15 +220,56 @@ impl<W: Copy + Eq + Hash> Tree<W> {
216220 let Some(&n) = self.leaves.get(&w) else {
217221 return false;
218222 };
219- if let Some((p, i)) = self.position(n)
220- && self.axis(p) == Some(dir.axis())
221- && let Some(j) = self.next_shown(p, i, dir, &vis)
222- {
223- self.join_lane(n, p, j);
224- return self.reveal(n, true);
223+ self.until_seen(&vis, |t| {
224+ if let Some((p, i)) = t.position(n)
225+ && t.axis(p) == Some(dir.axis())
226+ && let Some(j) = t.next_shown(p, i, dir, &vis)
227+ {
228+ t.join_lane(n, p, j);
229+ return t.reveal(n, true);
230+ }
231+ let moved = t.move_node(n, dir, &vis, true);
232+ t.reveal(n, moved)
233+ })
234+ }
235+
236+ /// Repeats `step` until the visible layout changes, or undoes it all when
237+ /// it never does. Steps around hidden windows otherwise ate keypresses
238+ /// and rearranged windows the user could not see.
239+ fn until_seen(
240+ &mut self,
241+ vis: &impl Fn(W) -> bool,
242+ mut step: impl FnMut(&mut Self) -> bool,
243+ ) -> bool {
244+ let seen = |t: &Self| -> Vec<(W, Rect)> {
245+ t.arrange(Rect::new(0.0, 0.0, 1000.0, 1000.0), 0.0, vis)
246+ .into_iter()
247+ .filter(|p| vis(p.0))
248+ .collect()
249+ };
250+ let same = |a: &[(W, Rect)], b: &[(W, Rect)]| {
251+ a.len() == b.len()
252+ && a.iter().all(|(v, r)| {
253+ b.iter().any(|(w, s)| {
254+ v == w
255+ && [r.x - s.x, r.y - s.y, r.w - s.w, r.h - s.h]
256+ .iter()
257+ .all(|d| d.abs() < 1e-6)
258+ })
259+ })
260+ };
261+ let before = self.clone();
262+ let start = seen(self);
263+ for _ in 0..=before.nodes.len() {
264+ if !step(self) {
265+ break;
266+ }
267+ if !same(&start, &seen(self)) {
268+ return true;
269+ }
225270 }
226- let moved = self.move_node(n, dir, &vis, true);
227- self.reveal(n, moved)
271+ *self = before;
272+ false
228273 }
229274
230275 /// Undoes any expansion along `leaf`'s ancestry that favours another