atagen

monolith

public
Star0
Watch0

Commit ab9ba6872885

tarball

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

2 files changed+73-3
planner: finish vanishing windows before appearing ones grow

A closing window collapses to a seed inside its own full rect, and a window
spawned in the same retarget grows from a seed on that same line. They overlap
for real throughout any single set of phases, so Direct, AxisOrder, Lanes and
Grid all failed validation and plan() fell through to Pinch for the whole
output. When a plan would pinch and some window appears from an empty rect,
plan() now plans everything else first and grows the appearing windows
afterwards, which keeps the no-overlap check exact for visible windows.
Modified file: crates/planner/src/plan.rs+37 additions−3 deletions
@@ -87,18 +87,52 @@ impl GroupPlan {
8787 }
8888
8989 /// Plans the transition from `from` to `to`. Both slices index the same windows.
90+/// When some windows appear while others vanish in the same place, no single
91+/// set of phases keeps them apart, so the vanishing ones finish first.
92+pub fn plan(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan {
93+ let direct = plan_once(from, to, cfg);
94+ if !direct.groups.iter().any(|g| g.strategy == Strategy::Pinch) {
95+ return direct;
96+ }
97+ staged(from, to, cfg).unwrap_or(direct)
98+}
99+
100+/// Plans everything but the windows that appear, then lets those grow.
101+/// A window that appears from a seed sits inside the space a vanishing one
102+/// still fills, and the exact overlap check rejects growing into it.
103+fn staged(from: &[Rect], to: &[Rect], cfg: &Config) -> Option<Plan> {
104+ let appears = |i: usize| is_empty(&from[i], cfg) && !is_empty(&to[i], cfg);
105+ if !(0..from.len()).any(appears) {
106+ return None;
107+ }
108+ let mid: Vec<Rect> = (0..from.len())
109+ .map(|i| if appears(i) { from[i] } else { to[i] })
110+ .collect();
111+ let (first, second) = (plan_once(from, &mid, cfg), plan_once(&mid, to, cfg));
112+ let groups: Vec<Group> = first.groups.into_iter().chain(second.groups).collect();
113+ if groups.iter().any(|g| g.strategy == Strategy::Pinch) {
114+ return None;
115+ }
116+ let mut keyframes = first.keyframes;
117+ keyframes.extend(second.keyframes.into_iter().skip(1));
118+ Some(Plan { keyframes, groups })
119+}
120+
121+fn is_empty(r: &Rect, cfg: &Config) -> bool {
122+ r.w <= cfg.eps || r.h <= cfg.eps
123+}
124+
90125 /// Windows with no area at either end, like hidden ones held at their seed,
91126 /// jump straight to `to`. Planning them would let an invisible point inside
92127 /// a visible window join its group, or block every strategy and pinch.
93-pub fn plan(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan {
128+fn plan_once(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan {
94129 assert_eq!(
95130 from.len(),
96131 to.len(),
97132 "layouts must describe the same windows"
98133 );
99- let empty = |r: &Rect| r.w <= cfg.eps || r.h <= cfg.eps;
100134 let live: Vec<usize> = (0..from.len())
101- .filter(|&i| !(empty(&from[i]) && empty(&to[i])))
135+ .filter(|&i| !(is_empty(&from[i], cfg) && is_empty(&to[i], cfg)))
102136 .collect();
103137 if live.len() == from.len() {
104138 return plan_live(from, to, cfg);
Modified file: crates/planner/src/tests.rs+36 additions
@@ -236,6 +236,24 @@ proptest! {
236236 }
237237
238238 #[test]
239+ fn closing_while_spawning_never_pinches(seed in 1u64.., n in 2usize..9, edge in 0usize..3, direct: bool) {
240+ let mut rng = Rng(seed);
241+ let c = cfg(if direct { Style::Direct } else { Style::Orthogonal });
242+ let a = random_layout(&mut rng, n);
243+ let v = rng.below(n);
244+ let at = [a[v].x - G / 2.0, a[v].center_x(), a[v].right() + G / 2.0][edge];
245+ let line = Rect::new(at, a[v].y, 0.0, a[v].h);
246+ let mut from = a.clone();
247+ from.push(line);
248+ let mut to = a.clone();
249+ to[v] = Rect::new(a[v].center_x(), a[v].y, 0.0, a[v].h);
250+ to.push(a[v]);
251+ let p = plan(&from, &to, &c);
252+ prop_assert!(validate(&p, &c).is_empty(), "{:?}", p);
253+ prop_assert!(p.groups.iter().all(|g| g.strategy != Strategy::Pinch), "{:?}", p);
254+ }
255+
256+ #[test]
239257 fn interrupts_replan_cleanly(seed in 1u64.., n in 2usize..9, at in 0.0f64..1.0, s in 0.0f64..1.0) {
240258 let mut rng = Rng(seed);
241259 let c = cfg(Style::Orthogonal);
@@ -266,3 +284,21 @@ fn hidden_seeds_stay_out_of_the_plan() {
266284 assert!(p.groups.iter().all(|g| !g.members.contains(&2)));
267285 assert!(p.keyframes.iter().all(|k| k[2] == to[2]));
268286 }
287+
288+/// A window closes in the slot where another opens. The ghost shrinks to a
289+/// seed inside its own full rect while the newcomer grows from a seed on the
290+/// same line, so no single set of phases keeps them apart and every strategy
291+/// used to fail until the plan pinched the whole output.
292+#[test]
293+fn closing_and_spawning_in_one_slot_plan_in_stages() {
294+ let c = cfg(Style::Orthogonal);
295+ let t = cols(region(), 3);
296+ let seed = Rect::new(600.0, 10.0, 0.0, 680.0);
297+ let from = vec![t[0], t[1], t[2], seed];
298+ let to = vec![t[0], seed, t[2], t[1]];
299+ let p = plan(&from, &to, &c);
300+ assert!(validate(&p, &c).is_empty(), "{p:?}");
301+ assert!(p.groups.iter().all(|g| g.strategy != Strategy::Pinch));
302+ assert_eq!(p.phases(), 2);
303+ assert_eq!(p.keyframes[1], vec![t[0], seed, t[2], seed]);
304+}