atagen

monolith

public
Star0
Watch0

Commit 3b1cf5ee2ea2

tarball

atagen <boss@atagen.co>2026-10-17 13:59 UTC

3 files changed+103-13
layout: keep moved windows visible and stack weights stable

A move into a container expanded toward another child gave the moved window no room, so it vanished while staying focused. Moves and new windows now clear expansions that favour someone else. Joining a stack also hands the window's weight to the stack and leaving hands it back, where before each round trip split a slot's weight again and shrank the windows around it.
Deleted file: crates/layout/proptest-regressions/tests.txt−8 deletions
@@ -1,8 +0,0 @@
1-# Seeds for failure cases proptest has generated in the past. It is
2-# automatically read and these particular cases re-run before any
3-# novel cases are generated.
4-#
5-# It is recommended to check this file in to source control so that
6-# everyone who runs the test benefits from these saved cases.
7-cc feaaec708a35beac0ab9749dd260c423ed4177da099bd85a2041f38172334f65 # shrinks to ops = [Add(0, false), Add(0, false), Add(0, false), View(1), Scratch(0), Add(0, false), Add(0, false), Add(0, false), Summon, Summon, Flip, Add(1, false), Move(Left), Add(0, false), Add(0, false), Focus(114), Flip, Move(Left), Focus(0), Add(0, false), View(0), View(0), View(0), Add(0, false), Expand(0)]
8-cc 796d3b6a182db230a58f8fcf320dde6b4ffd5ff3128b07a4f4c75737b25d9e88 # shrinks to ops = [Add(0, false), Focus(0), Add(0, false), Focus(30), View(3), Focus(0), Add(3, false), Move(Up), Add(0, false), Move(Left), Add(0, false), View(0), Add(0, false), Block(Right), Add(0, false), Move(Right), Add(0, false), View(3), Flip, Add(3, false), Expand(0), View(0), Move(Right)]
Modified file: crates/layout/src/tests.rs+39 additions
@@ -128,6 +128,18 @@ fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {
128128 }
129129 }
130130
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+
131143 fn check_geometry(l: &Layout) {
132144 for (o, out) in l.outputs() {
133145 let arr = l.arrange(o);
@@ -182,9 +194,14 @@ proptest! {
182194 l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
183195 let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
184196 for op in &ops {
197+ let moving = matches!(op, Op::Move(_) | Op::Merge(_) | Op::Block(_));
198+ let shown_before = l.focused().filter(|&w| tiled_area(&l, w) > 1.0);
185199 apply(&mut l, &tags, op);
186200 l.check();
187201 check_geometry(&l);
202+ if moving && let Some(w) = shown_before {
203+ prop_assert!(tiled_area(&l, w) > 1.0, "{w:?} vanished after {op:?}");
204+ }
188205 }
189206 }
190207
@@ -312,3 +329,25 @@ fn expand_skips_containers_of_hidden_windows() {
312329 let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
313330 assert_eq!(rect(b).w, AREA.w);
314331 }
332+
333+/// Joining a stack hands the window's weight to it and leaving hands it back,
334+/// so going in and out repeatedly leaves every window the size it was.
335+#[test]
336+fn stacking_in_and_out_does_not_drift() {
337+ for out in [Dir::Up, Dir::Down, Dir::Right] {
338+ let (a, b, c) = (1, 2, 3);
339+ let mut t: Tree<u32> = Tree::default();
340+ t.insert_row(a);
341+ t.insert_after(b, a);
342+ t.insert_after(c, b);
343+ assert!(t.merge_window(c, Dir::Left, |_| true));
344+ assert!(t.merge_window(c, out, |_| true));
345+ let once = t.arrange(AREA, 0.0, |_| true);
346+ for _ in 0..8 {
347+ t.merge_window(c, Dir::Left, |_| true);
348+ t.merge_window(c, out, |_| true);
349+ }
350+ t.check();
351+ assert_eq!(t.arrange(AREA, 0.0, |_| true), once, "stepping out {out:?}");
352+ }
353+}
Modified file: crates/layout/src/tree.rs+64 additions−5 deletions
@@ -138,6 +138,7 @@ impl<W: Copy + Eq + Hash> Tree<W> {
138138 self.root = Some(c);
139139 }
140140 }
141+ self.reveal(n, true);
141142 }
142143
143144 /// Appends `w` as a row of its own along the stacking axis.
@@ -157,6 +158,7 @@ impl<W: Copy + Eq + Hash> Tree<W> {
157158 self.root = Some(c);
158159 }
159160 }
161+ self.reveal(n, true);
160162 }
161163
162164 pub fn remove(&mut self, w: W) -> bool {
@@ -182,7 +184,8 @@ impl<W: Copy + Eq + Hash> Tree<W> {
182184 let Some(&n) = self.leaves.get(&w) else {
183185 return false;
184186 };
185- self.move_node(n, dir, &vis, true)
187+ let moved = self.move_node(n, dir, &vis, true);
188+ self.reveal(n, moved)
186189 }
187190
188191 /// Moves the container holding `w` as a rigid block. A window sitting
@@ -195,7 +198,8 @@ impl<W: Copy + Eq + Hash> Tree<W> {
195198 .parent
196199 .filter(|&p| Some(p) != self.root)
197200 .unwrap_or(n);
198- self.move_node(block, dir, &vis, false)
201+ let moved = self.move_node(block, dir, &vis, false);
202+ self.reveal(n, moved)
199203 }
200204
201205 /// Moves a window to the end of the next visible lane in `dir` among its
@@ -211,9 +215,27 @@ impl<W: Copy + Eq + Hash> Tree<W> {
211215 && let Some(j) = self.next_shown(p, i, dir, &vis)
212216 {
213217 self.join_lane(n, p, j);
214- return true;
218+ return self.reveal(n, true);
215219 }
216- self.move_node(n, dir, &vis, true)
220+ let moved = self.move_node(n, dir, &vis, true);
221+ self.reveal(n, moved)
222+ }
223+
224+ /// Undoes any expansion along `leaf`'s ancestry that favours another
225+ /// child, once it has `moved` or arrived. An expanded container gives its
226+ /// other children no room, so a window moved into one vanished while
227+ /// staying focused.
228+ fn reveal(&mut self, leaf: NodeId, moved: bool) -> bool {
229+ let mut cur = leaf;
230+ while moved && let Some(p) = self.nodes[cur].parent {
231+ if let Kind::Container { expanded, .. } = &mut self.nodes[p].kind
232+ && expanded.is_some_and(|e| e != cur)
233+ {
234+ *expanded = None;
235+ }
236+ cur = p;
237+ }
238+ moved
217239 }
218240
219241 /// Whether every container above `w` is expanded towards it.
@@ -405,7 +427,7 @@ impl<W: Copy + Eq + Hash> Tree<W> {
405427 self.join_lane(n, p, j);
406428 } else {
407429 let at = if dir.forward() { i + 1 } else { i };
408- self.relocate(n, p, at);
430+ self.step_out(n, p, i, at);
409431 }
410432 return true;
411433 }
@@ -432,6 +454,7 @@ impl<W: Copy + Eq + Hash> Tree<W> {
432454 return false;
433455 }
434456 let (old, i) = self.position(n).expect("n is below the root");
457+ self.release_share(n);
435458 self.unlink(old, i);
436459 self.normalize(old);
437460 let r = self.root.expect("the rest of the tree survives");
@@ -494,7 +517,43 @@ impl<W: Copy + Eq + Hash> Tree<W> {
494517 c
495518 };
496519 let end = self.children(lane).len();
520+ let from_p = self.position(n).filter(|&(old, _)| old == p);
521+ let weight = from_p.map(|(_, i)| self.children(p)[i].weight);
497522 self.relocate(n, lane, end);
523+ if let Some(weight) = weight
524+ && self.nodes.contains_key(lane)
525+ && let Some((q, k)) = self.position(lane)
526+ && q == p
527+ {
528+ self.children_mut(p)[k].weight += weight;
529+ }
530+ }
531+
532+ /// Moves `n` out of `p`'s child `i` to sit beside it at `at`, taking
533+ /// along the share of that child's weight it had.
534+ fn step_out(&mut self, n: NodeId, p: NodeId, i: usize, at: usize) {
535+ let cur = self.children(p)[i].node;
536+ let Some((parent, k)) = self.position(n).filter(|&(q, _)| q == cur) else {
537+ self.relocate(n, p, at);
538+ return;
539+ };
540+ let share = self.release_share(n).unwrap_or_else(|| self.mean_weight(p));
541+ self.unlink(parent, k);
542+ self.attach(p, at, n, share);
543+ self.normalize(parent);
544+ }
545+
546+ /// Takes `n`'s share of its container's weight away from the container,
547+ /// for `n` to carry out. Joining a lane hands a window's weight to the
548+ /// lane, so leaving gives it back and windows going in and out of
549+ /// stacks do not shrink their neighbours a little more each time.
550+ fn release_share(&mut self, n: NodeId) -> Option<f64> {
551+ let (old, i) = self.position(n)?;
552+ let (q, k) = self.position(old)?;
553+ let total: f64 = self.children(old).iter().map(|c| c.weight).sum();
554+ let share = self.children(q)[k].weight * self.children(old)[i].weight / total;
555+ self.children_mut(q)[k].weight -= share;
556+ Some(share)
498557 }
499558
500559 fn relocate(&mut self, n: NodeId, p: NodeId, at: usize) {