atagen

monolith

public
Star0
Watch0

Commit 9baaafa4b167

tarball

atagen <boss@atagen.co>2026-10-16 10:04 UTC

6 files changed+1772-0
layout: add the tag and tree model

One tree per output holds every tiled window, and per-output tag views filter it, so viewing or hiding a tag never rearranges anything. A container squeezed by an expanded sibling shrinks its gaps rather than spilling its children past the output edge. A new window next to a full-width row grows that row instead of stacking under it.
Modified file: Cargo.lock+20 additions
@@ -409,6 +409,11 @@ version = "0.0.0"
409409 [[package]]
410410 name = "monolith-layout"
411411 version = "0.0.0"
412+dependencies = [
413+ "monolith-planner",
414+ "proptest",
415+ "slotmap",
416+]
412417
413418 [[package]]
414419 name = "monolith-motion"
@@ -743,6 +748,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
743748 checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
744749
745750 [[package]]
751+name = "slotmap"
752+version = "1.1.1"
753+source = "registry+https://github.com/rust-lang/crates.io-index"
754+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
755+dependencies = [
756+ "version_check",
757+]
758+
759+[[package]]
746760 name = "smallvec"
747761 version = "1.16.3"
748762 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -996,6 +1010,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
9961010 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
9971011
9981012 [[package]]
1013+name = "version_check"
1014+version = "0.9.5"
1015+source = "registry+https://github.com/rust-lang/crates.io-index"
1016+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1017+
1018+[[package]]
9991019 name = "wait-timeout"
10001020 version = "0.2.1"
10011021 source = "registry+https://github.com/rust-lang/crates.io-index"
Modified file: crates/layout/Cargo.toml+6 additions
@@ -8,3 +8,9 @@ publish.workspace = true
88 [lints]
99 workspace = true
1010
11+[dependencies]
12+monolith-planner.workspace = true
13+slotmap = "1"
14+
15+[dev-dependencies]
16+proptest.workspace = true
Added file: crates/layout/proptest-regressions/tests.txt+7 additions
@@ -0,0 +1,7 @@
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)]
Modified file: crates/layout/src/lib.rs+907 additions
@@ -1 +1,908 @@
1+//! Window organisation without smithay. Tags with per-output views, one
2+//! tiling tree per output filtered by those views, floating windows,
3+//! scratchpads, focus, and restoring windows when an output comes back.
14
5+mod tree;
6+
7+#[cfg(test)]
8+mod tests;
9+
10+pub use monolith_planner::Rect;
11+pub use tree::{Axis, Dir, Tree};
12+
13+use slotmap::{SlotMap, new_key_type};
14+use std::collections::HashMap;
15+
16+new_key_type! {
17+ pub struct WindowId;
18+ pub struct TagId;
19+ pub struct OutputId;
20+}
21+
22+pub const HOMELESS: &str = "homeless";
23+
24+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25+pub enum ExpandScope {
26+ Window,
27+ Block,
28+ All,
29+}
30+
31+#[derive(Clone, Copy, Debug, PartialEq)]
32+pub struct Gaps {
33+ pub inner: f64,
34+ pub outer: f64,
35+}
36+
37+impl Default for Gaps {
38+ fn default() -> Self {
39+ Self {
40+ inner: 8.0,
41+ outer: 8.0,
42+ }
43+ }
44+}
45+
46+#[derive(Clone, Debug)]
47+pub struct Window {
48+ pub output: Option<OutputId>,
49+ pub tag: TagId,
50+ pub floating: bool,
51+ pub float_rect: Option<Rect>,
52+ pub fullscreen: bool,
53+ pub sticky: bool,
54+ pub scratchpad: Option<usize>,
55+ evicted_from: Option<String>,
56+}
57+
58+#[derive(Clone, Debug)]
59+pub struct Tag {
60+ pub name: String,
61+ pub urgent: bool,
62+}
63+
64+#[derive(Clone, Debug)]
65+pub struct Output {
66+ pub name: String,
67+ pub area: Rect,
68+ pub tree: Tree<WindowId>,
69+ /// Viewed tags besides the homeless tag, which is always viewed.
70+ pub viewed: Vec<TagId>,
71+ /// Floating windows, bottom to top.
72+ pub floating: Vec<WindowId>,
73+}
74+
75+#[derive(Clone, Debug)]
76+pub struct Scratchpad {
77+ pub name: String,
78+ pub windows: Vec<WindowId>,
79+ pub shown: Option<usize>,
80+ pub output: Option<OutputId>,
81+}
82+
83+#[derive(Clone, Debug)]
84+struct Parked {
85+ tree: Tree<WindowId>,
86+ viewed: Vec<TagId>,
87+ floating: Vec<WindowId>,
88+ area: Rect,
89+}
90+
91+#[derive(Clone, Debug, Default)]
92+pub struct Arrangement {
93+ /// Every tiled window on the output. Hidden ones are zero-size seeds.
94+ pub tiled: Vec<Placed>,
95+ /// Visible floating windows and shown scratchpads, bottom to top.
96+ pub floating: Vec<(WindowId, Rect)>,
97+ pub fullscreen: Option<WindowId>,
98+}
99+
100+#[derive(Clone, Copy, Debug)]
101+pub struct Placed {
102+ pub window: WindowId,
103+ pub rect: Rect,
104+ pub visible: bool,
105+}
106+
107+#[derive(Clone, Debug)]
108+pub struct Layout {
109+ windows: SlotMap<WindowId, Window>,
110+ tags: SlotMap<TagId, Tag>,
111+ tag_order: Vec<TagId>,
112+ homeless: TagId,
113+ outputs: SlotMap<OutputId, Output>,
114+ active: Option<OutputId>,
115+ /// Focus history, most recent last.
116+ history: Vec<WindowId>,
117+ scratchpads: Vec<Scratchpad>,
118+ last_scratchpad: Option<usize>,
119+ parked: HashMap<String, Parked>,
120+ pub gaps: Gaps,
121+}
122+
123+impl Default for Layout {
124+ fn default() -> Self {
125+ let mut tags = SlotMap::with_key();
126+ let homeless = tags.insert(Tag {
127+ name: HOMELESS.into(),
128+ urgent: false,
129+ });
130+ Self {
131+ windows: SlotMap::with_key(),
132+ tags,
133+ tag_order: vec![homeless],
134+ homeless,
135+ outputs: SlotMap::with_key(),
136+ active: None,
137+ history: Vec::new(),
138+ scratchpads: Vec::new(),
139+ last_scratchpad: None,
140+ parked: HashMap::new(),
141+ gaps: Gaps::default(),
142+ }
143+ }
144+}
145+
146+impl Layout {
147+ pub fn homeless(&self) -> TagId {
148+ self.homeless
149+ }
150+
151+ /// Looks a tag up by name, creating it at the end of the order.
152+ pub fn tag(&mut self, name: &str) -> TagId {
153+ if let Some(id) = self.tag_id(name) {
154+ return id;
155+ }
156+ let id = self.tags.insert(Tag {
157+ name: name.into(),
158+ urgent: false,
159+ });
160+ self.tag_order.push(id);
161+ id
162+ }
163+
164+ pub fn tag_id(&self, name: &str) -> Option<TagId> {
165+ self.tag_order
166+ .iter()
167+ .copied()
168+ .find(|&t| self.tags[t].name == name)
169+ }
170+
171+ pub fn tags(&self) -> impl Iterator<Item = (TagId, &Tag)> {
172+ self.tag_order.iter().map(|&t| (t, &self.tags[t]))
173+ }
174+
175+ pub fn reorder_tag(&mut self, tag: TagId, index: usize) {
176+ if let Some(i) = self.tag_order.iter().position(|&t| t == tag) {
177+ self.tag_order.remove(i);
178+ let index = index.min(self.tag_order.len());
179+ self.tag_order.insert(index, tag);
180+ }
181+ }
182+
183+ pub fn window(&self, w: WindowId) -> Option<&Window> {
184+ self.windows.get(w)
185+ }
186+
187+ pub fn windows(&self) -> impl Iterator<Item = (WindowId, &Window)> {
188+ self.windows.iter()
189+ }
190+
191+ pub fn output(&self, o: OutputId) -> Option<&Output> {
192+ self.outputs.get(o)
193+ }
194+
195+ pub fn outputs(&self) -> impl Iterator<Item = (OutputId, &Output)> {
196+ self.outputs.iter()
197+ }
198+
199+ pub fn scratchpads(&self) -> &[Scratchpad] {
200+ &self.scratchpads
201+ }
202+
203+ pub fn active_output(&self) -> Option<OutputId> {
204+ self.active
205+ }
206+
207+ pub fn set_active_output(&mut self, o: OutputId) {
208+ if self.outputs.contains_key(o) {
209+ self.active = Some(o);
210+ }
211+ }
212+
213+ /// Adds an output. One that was unplugged before gets back its tree,
214+ /// views and the windows that had to leave it.
215+ pub fn add_output(&mut self, name: &str, area: Rect) -> OutputId {
216+ let id = self.outputs.insert(Output {
217+ name: name.into(),
218+ area,
219+ tree: Tree::default(),
220+ viewed: Vec::new(),
221+ floating: Vec::new(),
222+ });
223+ if let Some(parked) = self.parked.remove(name) {
224+ self.restore(id, parked);
225+ }
226+ self.active.get_or_insert(id);
227+ let detached: Vec<WindowId> = self
228+ .windows
229+ .iter()
230+ .filter(|(_, w)| w.output.is_none())
231+ .map(|(id, _)| id)
232+ .collect();
233+ for w in detached {
234+ self.windows[w].output = Some(id);
235+ self.place(w);
236+ }
237+ id
238+ }
239+
240+ fn restore(&mut self, id: OutputId, parked: Parked) {
241+ let name = self.outputs[id].name.clone();
242+ let ours = |l: &Self, w: WindowId| {
243+ l.windows
244+ .get(w)
245+ .is_some_and(|x| x.evicted_from.as_deref() == Some(&name) && x.scratchpad.is_none())
246+ };
247+ let mut tree = parked.tree;
248+ tree.retain(|w| ours(self, w) && !self.windows[w].floating);
249+ let floating: Vec<WindowId> = parked
250+ .floating
251+ .into_iter()
252+ .filter(|&w| ours(self, w) && self.windows[w].floating)
253+ .collect();
254+ for w in tree
255+ .windows()
256+ .chain(floating.iter().copied())
257+ .collect::<Vec<_>>()
258+ {
259+ self.unplace(w);
260+ let win = &mut self.windows[w];
261+ if let (Some(from), Some(rect)) = (win.output, win.float_rect) {
262+ win.float_rect = Some(translate(rect, self.outputs[from].area, parked.area));
263+ }
264+ win.output = Some(id);
265+ win.evicted_from = None;
266+ }
267+ let out = &mut self.outputs[id];
268+ out.tree = tree;
269+ out.floating = floating;
270+ out.viewed = parked
271+ .viewed
272+ .into_iter()
273+ .filter(|&t| self.tags.contains_key(t))
274+ .collect();
275+ }
276+
277+ /// Removes an output. Its windows move to an output viewing their tag,
278+ /// or the active one, and remember where they came from.
279+ pub fn remove_output(&mut self, o: OutputId) {
280+ let Some(out) = self.outputs.remove(o) else {
281+ return;
282+ };
283+ self.parked.insert(
284+ out.name.clone(),
285+ Parked {
286+ tree: out.tree,
287+ viewed: out.viewed,
288+ floating: out.floating,
289+ area: out.area,
290+ },
291+ );
292+ if self.active == Some(o) {
293+ self.active = self.outputs.keys().next();
294+ }
295+ for sp in &mut self.scratchpads {
296+ if sp.output == Some(o) {
297+ sp.output = self.active;
298+ }
299+ }
300+ let leaving: Vec<WindowId> = self
301+ .windows
302+ .iter()
303+ .filter(|(_, w)| w.output == Some(o))
304+ .map(|(id, _)| id)
305+ .collect();
306+ for w in leaving {
307+ let tag = self.windows[w].tag;
308+ let target = self
309+ .outputs
310+ .keys()
311+ .find(|&t| self.is_viewed(t, tag))
312+ .or(self.active);
313+ let win = &mut self.windows[w];
314+ if let (Some(t), Some(rect)) = (target, win.float_rect) {
315+ win.float_rect = Some(translate(rect, out.area, self.outputs[t].area));
316+ }
317+ win.output = target;
318+ if win.scratchpad.is_some() {
319+ continue;
320+ }
321+ win.evicted_from = Some(out.name.clone());
322+ if target.is_some() {
323+ self.place(w);
324+ }
325+ }
326+ }
327+
328+ pub fn set_output_area(&mut self, o: OutputId, area: Rect) {
329+ if let Some(out) = self.outputs.get_mut(o) {
330+ out.area = area;
331+ }
332+ }
333+
334+ pub fn is_viewed(&self, o: OutputId, tag: TagId) -> bool {
335+ tag == self.homeless
336+ || self
337+ .outputs
338+ .get(o)
339+ .is_some_and(|out| out.viewed.contains(&tag))
340+ }
341+
342+ /// Views `tag` alone, besides the homeless tag.
343+ pub fn view(&mut self, o: OutputId, tag: TagId) {
344+ let Some(out) = self.outputs.get_mut(o) else {
345+ return;
346+ };
347+ out.viewed.clear();
348+ if tag != self.homeless {
349+ out.viewed.push(tag);
350+ }
351+ self.tags[tag].urgent = false;
352+ }
353+
354+ pub fn toggle_view(&mut self, o: OutputId, tag: TagId) {
355+ if tag == self.homeless {
356+ return;
357+ }
358+ let Some(out) = self.outputs.get_mut(o) else {
359+ return;
360+ };
361+ if let Some(i) = out.viewed.iter().position(|&t| t == tag) {
362+ out.viewed.remove(i);
363+ } else {
364+ out.viewed.push(tag);
365+ self.tags[tag].urgent = false;
366+ }
367+ }
368+
369+ pub fn view_everywhere(&mut self, tag: TagId) {
370+ for o in self.outputs.keys().collect::<Vec<_>>() {
371+ self.view(o, tag);
372+ }
373+ }
374+
375+ pub fn visible(&self, w: WindowId) -> bool {
376+ let Some(win) = self.windows.get(w) else {
377+ return false;
378+ };
379+ let Some(o) = win.output else {
380+ return false;
381+ };
382+ match win.scratchpad {
383+ Some(s) => {
384+ let sp = &self.scratchpads[s];
385+ sp.output == Some(o) && sp.shown.map(|i| sp.windows[i]) == Some(w)
386+ }
387+ None => win.sticky || self.is_viewed(o, win.tag),
388+ }
389+ }
390+
391+ /// Adds a window on `output` (default the active one) and `tag` (default
392+ /// homeless). A window landing on a tag its output is not viewing marks
393+ /// the tag urgent.
394+ pub fn add_window(
395+ &mut self,
396+ output: Option<OutputId>,
397+ tag: Option<TagId>,
398+ floating: Option<Rect>,
399+ ) -> WindowId {
400+ let output = output
401+ .filter(|&o| self.outputs.contains_key(o))
402+ .or(self.active);
403+ let tag = tag
404+ .filter(|&t| self.tags.contains_key(t))
405+ .unwrap_or(self.homeless);
406+ let w = self.windows.insert(Window {
407+ output,
408+ tag,
409+ floating: floating.is_some(),
410+ float_rect: floating,
411+ fullscreen: false,
412+ sticky: false,
413+ scratchpad: None,
414+ evicted_from: None,
415+ });
416+ if let Some(o) = output {
417+ if !self.is_viewed(o, tag) {
418+ self.tags[tag].urgent = true;
419+ }
420+ self.place(w);
421+ }
422+ w
423+ }
424+
425+ pub fn remove_window(&mut self, w: WindowId) {
426+ if !self.windows.contains_key(w) {
427+ return;
428+ }
429+ self.unplace(w);
430+ self.history.retain(|&h| h != w);
431+ if let Some(s) = self.windows[w].scratchpad {
432+ self.leave_scratchpad(s, w);
433+ }
434+ self.windows.remove(w);
435+ }
436+
437+ /// Puts a window into its output's tree or floating stack. Tiled windows
438+ /// go next to the focused window if both are visible, else next to the
439+ /// latest window of their tag, else into a new row.
440+ fn place(&mut self, w: WindowId) {
441+ let win = &self.windows[w];
442+ let Some(o) = win.output else { return };
443+ if win.scratchpad.is_some() {
444+ return;
445+ }
446+ if win.floating {
447+ if win.float_rect.is_none() {
448+ let rect = centred(self.outputs[o].area, 0.6);
449+ self.windows[w].float_rect = Some(rect);
450+ }
451+ self.outputs[o].floating.push(w);
452+ return;
453+ }
454+ let tag = win.tag;
455+ let tree = &self.outputs[o].tree;
456+ let focused = self
457+ .focused()
458+ .filter(|&f| tree.contains(f) && self.is_viewed(o, tag));
459+ let anchor = focused.or_else(|| {
460+ self.history
461+ .iter()
462+ .rev()
463+ .copied()
464+ .find(|&h| tree.contains(h) && self.windows[h].tag == tag)
465+ });
466+ let tree = &mut self.outputs[o].tree;
467+ match anchor {
468+ Some(a) => tree.insert_after(w, a),
469+ None => tree.insert_row(w),
470+ }
471+ }
472+
473+ fn unplace(&mut self, w: WindowId) {
474+ let Some(o) = self.windows[w].output else {
475+ return;
476+ };
477+ let out = &mut self.outputs[o];
478+ out.tree.remove(w);
479+ out.floating.retain(|&f| f != w);
480+ }
481+
482+ pub fn focus(&mut self, w: WindowId) {
483+ let Some(win) = self.windows.get(w) else {
484+ return;
485+ };
486+ self.active = win.output.or(self.active);
487+ if let Some(o) = win.output {
488+ let stack = &mut self.outputs[o].floating;
489+ if let Some(i) = stack.iter().position(|&f| f == w) {
490+ stack.remove(i);
491+ stack.push(w);
492+ }
493+ }
494+ self.history.retain(|&h| h != w);
495+ self.history.push(w);
496+ }
497+
498+ /// The most recently focused visible window.
499+ pub fn focused(&self) -> Option<WindowId> {
500+ self.history
501+ .iter()
502+ .rev()
503+ .copied()
504+ .find(|&w| self.visible(w))
505+ }
506+
507+ /// The most recently focused visible window on `o`.
508+ pub fn focused_on(&self, o: OutputId) -> Option<WindowId> {
509+ self.history
510+ .iter()
511+ .rev()
512+ .copied()
513+ .find(|&w| self.windows[w].output == Some(o) && self.visible(w))
514+ }
515+
516+ /// Focuses the nearest visible window in `dir`, crossing to the next
517+ /// output when there is none on this one.
518+ pub fn focus_dir(&mut self, dir: Dir) -> Option<WindowId> {
519+ let o = self.active?;
520+ let from = self.focused_on(o).and_then(|f| self.rect_of(f));
521+ let target = from.and_then(|from| {
522+ let arr = self.arrange(o);
523+ let candidates = arr
524+ .tiled
525+ .iter()
526+ .filter(|p| p.visible)
527+ .map(|p| (p.window, p.rect))
528+ .chain(arr.floating.iter().copied())
529+ .filter(|&(w, _)| Some(w) != self.focused_on(o));
530+ nearest(from, dir, candidates)
531+ });
532+ if let Some(w) = target {
533+ self.focus(w);
534+ return Some(w);
535+ }
536+ let here = self.outputs[o].area;
537+ let next = nearest(
538+ here,
539+ dir,
540+ self.outputs.iter().map(|(id, out)| (id, out.area)),
541+ )?;
542+ self.active = Some(next);
543+ let w = self.focused_on(next);
544+ if let Some(w) = w {
545+ self.focus(w);
546+ }
547+ w
548+ }
549+
550+ fn rect_of(&self, w: WindowId) -> Option<Rect> {
551+ let o = self.windows.get(w)?.output?;
552+ let arr = self.arrange(o);
553+ arr.tiled
554+ .iter()
555+ .find(|p| p.window == w)
556+ .map(|p| p.rect)
557+ .or_else(|| arr.floating.iter().find(|f| f.0 == w).map(|f| f.1))
558+ }
559+
560+ fn vis_on(&self, o: OutputId) -> impl Fn(WindowId) -> bool + '_ {
561+ move |w| self.windows[w].output == Some(o) && self.visible(w)
562+ }
563+
564+ fn focused_tiled(&self) -> Option<(WindowId, OutputId)> {
565+ let w = self.focused()?;
566+ let o = self.windows[w].output?;
567+ self.outputs[o].tree.contains(w).then_some((w, o))
568+ }
569+
570+ pub fn move_window(&mut self, dir: Dir) -> bool {
571+ let Some((w, o)) = self.focused_tiled() else {
572+ return false;
573+ };
574+ let mut tree = std::mem::take(&mut self.outputs[o].tree);
575+ let moved = tree.move_window(w, dir, self.vis_on(o));
576+ self.outputs[o].tree = tree;
577+ moved
578+ }
579+
580+ pub fn move_block(&mut self, dir: Dir) -> bool {
581+ let Some((w, o)) = self.focused_tiled() else {
582+ return false;
583+ };
584+ let mut tree = std::mem::take(&mut self.outputs[o].tree);
585+ let moved = tree.move_block(w, dir, self.vis_on(o));
586+ self.outputs[o].tree = tree;
587+ moved
588+ }
589+
590+ pub fn resize(&mut self, axis: Axis, delta: f64) -> bool {
591+ let Some((w, o)) = self.focused_tiled() else {
592+ return false;
593+ };
594+ let mut tree = std::mem::take(&mut self.outputs[o].tree);
595+ let done = tree.resize(w, axis, delta, self.vis_on(o));
596+ self.outputs[o].tree = tree;
597+ done
598+ }
599+
600+ pub fn expand(&mut self, scope: ExpandScope) -> bool {
601+ let Some((w, o)) = self.focused_tiled() else {
602+ return false;
603+ };
604+ let (skip, count) = match scope {
605+ ExpandScope::Window => (0, 1),
606+ ExpandScope::Block => (1, 1),
607+ ExpandScope::All => (0, usize::MAX),
608+ };
609+ self.outputs[o].tree.toggle_expand(w, skip, count)
610+ }
611+
612+ pub fn flip(&mut self, o: OutputId) {
613+ if let Some(out) = self.outputs.get_mut(o) {
614+ out.tree.flip();
615+ }
616+ }
617+
618+ /// Retags a window. It keeps its slot and disappears if the new tag is
619+ /// not viewed.
620+ pub fn set_tag(&mut self, w: WindowId, tag: TagId) {
621+ if self.tags.contains_key(tag)
622+ && let Some(win) = self.windows.get_mut(w)
623+ {
624+ win.tag = tag;
625+ }
626+ }
627+
628+ pub fn send_to_output(&mut self, w: WindowId, o: OutputId) {
629+ let Some(win) = self.windows.get(w) else {
630+ return;
631+ };
632+ if !self.outputs.contains_key(o) || win.output == Some(o) || win.scratchpad.is_some() {
633+ return;
634+ }
635+ let from = win.output.map(|f| self.outputs[f].area);
636+ self.unplace(w);
637+ let win = &mut self.windows[w];
638+ if let (Some(from), Some(rect)) = (from, win.float_rect) {
639+ win.float_rect = Some(translate(rect, from, self.outputs[o].area));
640+ }
641+ win.output = Some(o);
642+ win.evicted_from = None;
643+ self.place(w);
644+ }
645+
646+ pub fn toggle_floating(&mut self, w: WindowId) {
647+ let Some(win) = self.windows.get(w) else {
648+ return;
649+ };
650+ if win.scratchpad.is_some() {
651+ return;
652+ }
653+ self.unplace(w);
654+ let win = &mut self.windows[w];
655+ win.floating = !win.floating;
656+ self.place(w);
657+ }
658+
659+ pub fn set_float_rect(&mut self, w: WindowId, rect: Rect) {
660+ if let Some(win) = self.windows.get_mut(w) {
661+ win.float_rect = Some(rect);
662+ }
663+ }
664+
665+ pub fn toggle_fullscreen(&mut self, w: WindowId) {
666+ if let Some(win) = self.windows.get_mut(w) {
667+ win.fullscreen = !win.fullscreen;
668+ }
669+ }
670+
671+ pub fn toggle_sticky(&mut self, w: WindowId) {
672+ if let Some(win) = self.windows.get_mut(w) {
673+ win.sticky = !win.sticky;
674+ }
675+ }
676+
677+ /// Adds a window to a named scratchpad, creating it. Scratchpad windows
678+ /// float and are shown only by summoning.
679+ pub fn scratchpad_add(&mut self, w: WindowId, name: &str) {
680+ let Some(win) = self.windows.get(w) else {
681+ return;
682+ };
683+ if let Some(s) = win.scratchpad {
684+ self.leave_scratchpad(s, w);
685+ }
686+ self.unplace(w);
687+ let s = match self.scratchpads.iter().position(|sp| sp.name == name) {
688+ Some(s) => s,
689+ None => {
690+ self.scratchpads.push(Scratchpad {
691+ name: name.into(),
692+ windows: Vec::new(),
693+ shown: None,
694+ output: None,
695+ });
696+ self.scratchpads.len() - 1
697+ }
698+ };
699+ self.scratchpads[s].windows.push(w);
700+ let area = self.windows[w].output.map(|o| self.outputs[o].area);
701+ let win = &mut self.windows[w];
702+ win.scratchpad = Some(s);
703+ win.floating = true;
704+ if win.float_rect.is_none() {
705+ win.float_rect = area.map(|a| centred(a, 0.6));
706+ }
707+ }
708+
709+ /// Cycles a scratchpad through off, each window, and off again, on the
710+ /// active output.
711+ pub fn summon(&mut self, name: &str) -> Option<WindowId> {
712+ let s = self.scratchpads.iter().position(|sp| sp.name == name)?;
713+ let o = self.active?;
714+ let sp = &mut self.scratchpads[s];
715+ let next = match sp.shown {
716+ None => 0,
717+ Some(i) => i + 1,
718+ };
719+ if next >= sp.windows.len() {
720+ sp.shown = None;
721+ return None;
722+ }
723+ let from = sp.output.map(|f| self.outputs[f].area);
724+ sp.shown = Some(next);
725+ sp.output = Some(o);
726+ let w = sp.windows[next];
727+ self.last_scratchpad = Some(s);
728+ let area = self.outputs[o].area;
729+ let win = &mut self.windows[w];
730+ if win.output != Some(o) {
731+ win.float_rect = match (from, win.float_rect) {
732+ (Some(from), Some(r)) => Some(translate(r, from, area)),
733+ _ => Some(centred(area, 0.6)),
734+ };
735+ win.output = Some(o);
736+ }
737+ self.focus(w);
738+ Some(w)
739+ }
740+
741+ /// Hides a scratchpad, by default the last one summoned.
742+ pub fn dismiss(&mut self, name: Option<&str>) {
743+ let s = match name {
744+ Some(name) => self.scratchpads.iter().position(|sp| sp.name == name),
745+ None => self.last_scratchpad,
746+ };
747+ if let Some(sp) = s.and_then(|s| self.scratchpads.get_mut(s)) {
748+ sp.shown = None;
749+ }
750+ }
751+
752+ fn leave_scratchpad(&mut self, s: usize, w: WindowId) {
753+ let sp = &mut self.scratchpads[s];
754+ let Some(i) = sp.windows.iter().position(|&x| x == w) else {
755+ return;
756+ };
757+ sp.windows.remove(i);
758+ sp.shown = match sp.shown {
759+ Some(j) if j == i => None,
760+ Some(j) if j > i => Some(j - 1),
761+ other => other,
762+ };
763+ self.windows[w].scratchpad = None;
764+ }
765+
766+ /// Target geometry for everything on `o`, in output-local logical
767+ /// coordinates offset by the output's area.
768+ pub fn arrange(&self, o: OutputId) -> Arrangement {
769+ let Some(out) = self.outputs.get(o) else {
770+ return Arrangement::default();
771+ };
772+ let area = inset(out.area, self.gaps.outer);
773+ let vis = self.vis_on(o);
774+ let tiled = out
775+ .tree
776+ .arrange(area, self.gaps.inner, &vis)
777+ .into_iter()
778+ .map(|(window, rect)| Placed {
779+ window,
780+ rect,
781+ visible: vis(window),
782+ })
783+ .collect();
784+ let mut floating: Vec<(WindowId, Rect)> = out
785+ .floating
786+ .iter()
787+ .copied()
788+ .filter(|&w| vis(w))
789+ .filter_map(|w| Some((w, self.windows[w].float_rect?)))
790+ .collect();
791+ for sp in &self.scratchpads {
792+ if let Some(i) = sp.shown
793+ && sp.output == Some(o)
794+ && let Some(r) = self.windows[sp.windows[i]].float_rect
795+ {
796+ floating.push((sp.windows[i], r));
797+ }
798+ }
799+ let fullscreen = self
800+ .history
801+ .iter()
802+ .rev()
803+ .copied()
804+ .find(|&w| self.windows[w].fullscreen && vis(w));
805+ Arrangement {
806+ tiled,
807+ floating,
808+ fullscreen,
809+ }
810+ }
811+
812+ #[cfg(test)]
813+ pub(crate) fn check(&self) {
814+ for (o, out) in &self.outputs {
815+ out.tree.check();
816+ for w in out.tree.windows() {
817+ let win = &self.windows[w];
818+ assert_eq!(win.output, Some(o));
819+ assert!(!win.floating && win.scratchpad.is_none());
820+ }
821+ for &w in &out.floating {
822+ assert_eq!(self.windows[w].output, Some(o));
823+ assert!(self.windows[w].floating);
824+ }
825+ }
826+ for (w, win) in &self.windows {
827+ let homes = self
828+ .outputs
829+ .values()
830+ .filter(|out| out.tree.contains(w) || out.floating.contains(&w))
831+ .count();
832+ let expected = usize::from(win.output.is_some() && win.scratchpad.is_none());
833+ assert_eq!(homes, expected, "window placed {homes} times");
834+ }
835+ }
836+}
837+
838+fn nearest<K: Copy>(
839+ from: Rect,
840+ dir: Dir,
841+ candidates: impl Iterator<Item = (K, Rect)>,
842+) -> Option<K> {
843+ const TOL: f64 = 0.5;
844+ let score = |r: Rect| -> Option<(bool, f64, f64)> {
845+ let (gap, overlap, offset) = match dir {
846+ Dir::Right => (
847+ r.x - from.right(),
848+ span_overlap(from.y, from.h, r.y, r.h),
849+ r.center_y() - from.center_y(),
850+ ),
851+ Dir::Left => (
852+ from.x - r.right(),
853+ span_overlap(from.y, from.h, r.y, r.h),
854+ r.center_y() - from.center_y(),
855+ ),
856+ Dir::Down => (
857+ r.y - from.bottom(),
858+ span_overlap(from.x, from.w, r.x, r.w),
859+ r.center_x() - from.center_x(),
860+ ),
861+ Dir::Up => (
862+ from.y - r.bottom(),
863+ span_overlap(from.x, from.w, r.x, r.w),
864+ r.center_x() - from.center_x(),
865+ ),
866+ };
867+ (gap > -TOL).then_some((overlap <= 0.0, gap.max(0.0), offset.abs()))
868+ };
869+ candidates
870+ .filter_map(|(k, r)| score(r).map(|s| (k, s)))
871+ .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
872+ .map(|(k, _)| k)
873+}
874+
875+fn span_overlap(a: f64, al: f64, b: f64, bl: f64) -> f64 {
876+ (a + al).min(b + bl) - a.max(b)
877+}
878+
879+fn inset(r: Rect, by: f64) -> Rect {
880+ Rect::new(
881+ r.x + by,
882+ r.y + by,
883+ (r.w - 2.0 * by).max(0.0),
884+ (r.h - 2.0 * by).max(0.0),
885+ )
886+}
887+
888+fn centred(area: Rect, frac: f64) -> Rect {
889+ let (w, h) = (area.w * frac, area.h * frac);
890+ Rect::new(
891+ area.x + (area.w - w) / 2.0,
892+ area.y + (area.h - h) / 2.0,
893+ w,
894+ h,
895+ )
896+}
897+
898+/// Keeps a rect at the same relative spot when it changes output.
899+fn translate(r: Rect, from: Rect, to: Rect) -> Rect {
900+ let sx = if from.w > 0.0 { to.w / from.w } else { 1.0 };
901+ let sy = if from.h > 0.0 { to.h / from.h } else { 1.0 };
902+ Rect::new(
903+ to.x + (r.x - from.x) * sx,
904+ to.y + (r.y - from.y) * sy,
905+ r.w.min(to.w),
906+ r.h.min(to.h),
907+ )
908+}
Added file: crates/layout/src/tests.rs+188 additions
@@ -0,0 +1,188 @@
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+ View(u8),
14+ ToggleView(u8),
15+ Retag(u8, u8),
16+ Float(u8),
17+ Expand(u8),
18+ Flip,
19+ Resize(bool, i8),
20+ Scratch(u8),
21+ Summon,
22+ Unplug,
23+ Replug,
24+}
25+
26+fn dir() -> impl Strategy<Value = Dir> {
27+ prop_oneof![
28+ Just(Dir::Left),
29+ Just(Dir::Right),
30+ Just(Dir::Up),
31+ Just(Dir::Down)
32+ ]
33+}
34+
35+fn op() -> impl Strategy<Value = Op> {
36+ prop_oneof![
37+ 4 => (0u8..4, prop::bool::weighted(0.15)).prop_map(|(t, f)| Op::Add(t, f)),
38+ 1 => any::<u8>().prop_map(Op::Remove),
39+ 3 => any::<u8>().prop_map(Op::Focus),
40+ 4 => dir().prop_map(Op::Move),
41+ 2 => dir().prop_map(Op::Block),
42+ 1 => (0u8..4).prop_map(Op::View),
43+ 2 => (0u8..4).prop_map(Op::ToggleView),
44+ 1 => (any::<u8>(), 0u8..4).prop_map(|(w, t)| Op::Retag(w, t)),
45+ 1 => any::<u8>().prop_map(Op::Float),
46+ 1 => (0u8..3).prop_map(Op::Expand),
47+ 1 => Just(Op::Flip),
48+ 1 => (any::<bool>(), any::<i8>()).prop_map(|(a, d)| Op::Resize(a, d)),
49+ 1 => any::<u8>().prop_map(Op::Scratch),
50+ 1 => Just(Op::Summon),
51+ 1 => Just(Op::Unplug),
52+ 1 => Just(Op::Replug),
53+ ]
54+}
55+
56+fn nth(l: &Layout, i: u8) -> Option<WindowId> {
57+ let ids: Vec<WindowId> = l.windows().map(|(id, _)| id).collect();
58+ (!ids.is_empty()).then(|| ids[i as usize % ids.len()])
59+}
60+
61+fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {
62+ let active = l.active_output();
63+ match *op {
64+ Op::Add(t, f) => {
65+ let w = l.add_window(
66+ None,
67+ Some(tags[t as usize]),
68+ f.then(|| Rect::new(10.0, 10.0, 200.0, 100.0)),
69+ );
70+ if l.visible(w) {
71+ l.focus(w);
72+ }
73+ }
74+ Op::Remove(i) => nth(l, i).into_iter().for_each(|w| l.remove_window(w)),
75+ Op::Focus(i) => {
76+ if let Some(w) = nth(l, i).filter(|&w| l.visible(w)) {
77+ l.focus(w);
78+ }
79+ }
80+ Op::Move(d) => {
81+ l.move_window(d);
82+ }
83+ Op::Block(d) => {
84+ l.move_block(d);
85+ }
86+ Op::View(t) => active.into_iter().for_each(|o| l.view(o, tags[t as usize])),
87+ Op::ToggleView(t) => active
88+ .into_iter()
89+ .for_each(|o| l.toggle_view(o, tags[t as usize])),
90+ Op::Retag(i, t) => nth(l, i)
91+ .into_iter()
92+ .for_each(|w| l.set_tag(w, tags[t as usize])),
93+ Op::Float(i) => nth(l, i).into_iter().for_each(|w| l.toggle_floating(w)),
94+ Op::Expand(s) => {
95+ l.expand([ExpandScope::Window, ExpandScope::Block, ExpandScope::All][s as usize]);
96+ }
97+ Op::Flip => active.into_iter().for_each(|o| l.flip(o)),
98+ Op::Resize(h, d) => {
99+ let axis = if h { Axis::Horizontal } else { Axis::Vertical };
100+ l.resize(axis, f64::from(d) / 200.0);
101+ }
102+ Op::Scratch(i) => nth(l, i).into_iter().for_each(|w| l.scratchpad_add(w, "s")),
103+ Op::Summon => {
104+ l.summon("s");
105+ }
106+ Op::Unplug => {
107+ let b = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o);
108+ if let Some(o) = b {
109+ l.remove_output(o);
110+ }
111+ }
112+ Op::Replug => {
113+ if !l.outputs().any(|(_, out)| out.name == "b") {
114+ l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
115+ }
116+ }
117+ }
118+}
119+
120+fn check_geometry(l: &Layout) {
121+ for (o, out) in l.outputs() {
122+ let arr = l.arrange(o);
123+ let shown: Vec<Rect> = arr
124+ .tiled
125+ .iter()
126+ .filter(|p| p.visible)
127+ .map(|p| p.rect)
128+ .collect();
129+ for r in &shown {
130+ assert!(r.w >= 0.0 && r.h >= 0.0);
131+ assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6);
132+ assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6);
133+ }
134+ for (i, a) in shown.iter().enumerate() {
135+ for b in &shown[i + 1..] {
136+ let ox = a.right().min(b.right()) - a.x.max(b.x);
137+ let oy = a.bottom().min(b.bottom()) - a.y.max(b.y);
138+ assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}");
139+ }
140+ }
141+ }
142+}
143+
144+proptest! {
145+ #![proptest_config(ProptestConfig::with_cases(2000))]
146+
147+ #[test]
148+ fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {
149+ let mut l = Layout::default();
150+ l.add_output("a", AREA);
151+ l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
152+ let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
153+ for op in &ops {
154+ apply(&mut l, &tags, op);
155+ l.check();
156+ check_geometry(&l);
157+ }
158+ }
159+}
160+
161+/// The hidden-slot rules from planner.html, demo 11.
162+#[test]
163+fn hidden_windows_keep_their_slots() {
164+ let (a, b, c, d, e) = (1, 2, 3, 4, 5);
165+ let mut t: Tree<u32> = Tree::default();
166+ t.insert_row(a);
167+ t.insert_after(d, a);
168+ t.insert_after(b, d);
169+ t.insert_row(c);
170+ t.insert_after(e, c);
171+ t.check();
172+ let hidden = |w: u32| w != d && w != e;
173+ let all = |_| true;
174+ let rect = |t: &Tree<u32>, w| {
175+ t.arrange(AREA, 0.0, all)
176+ .into_iter()
177+ .find(|p| p.0 == w)
178+ .unwrap()
179+ .1
180+ };
181+
182+ assert!(t.move_window(a, Dir::Right, hidden));
183+ assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);
184+
185+ assert!(t.move_window(c, Dir::Up, hidden));
186+ t.check();
187+ assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");
188+}
Added file: crates/layout/src/tree.rs+644 additions
@@ -0,0 +1,644 @@
1+use monolith_planner::Rect;
2+use slotmap::{SlotMap, new_key_type};
3+use std::collections::HashMap;
4+use std::hash::Hash;
5+
6+new_key_type! { pub struct NodeId; }
7+
8+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9+pub enum Axis {
10+ Horizontal,
11+ Vertical,
12+}
13+
14+impl Axis {
15+ pub fn other(self) -> Self {
16+ match self {
17+ Self::Horizontal => Self::Vertical,
18+ Self::Vertical => Self::Horizontal,
19+ }
20+ }
21+}
22+
23+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24+pub enum Dir {
25+ Left,
26+ Right,
27+ Up,
28+ Down,
29+}
30+
31+impl Dir {
32+ pub fn axis(self) -> Axis {
33+ match self {
34+ Self::Left | Self::Right => Axis::Horizontal,
35+ Self::Up | Self::Down => Axis::Vertical,
36+ }
37+ }
38+
39+ pub fn forward(self) -> bool {
40+ matches!(self, Self::Right | Self::Down)
41+ }
42+}
43+
44+const MIN_SHARE: f64 = 0.05;
45+
46+#[derive(Clone, Debug)]
47+struct Child {
48+ node: NodeId,
49+ weight: f64,
50+}
51+
52+#[derive(Clone, Debug)]
53+enum Kind<W> {
54+ Leaf(W),
55+ Container {
56+ axis: Axis,
57+ children: Vec<Child>,
58+ expanded: Option<NodeId>,
59+ },
60+}
61+
62+#[derive(Clone, Debug)]
63+struct Node<W> {
64+ parent: Option<NodeId>,
65+ kind: Kind<W>,
66+}
67+
68+/// One output's tiling tree. Every tiled window of every tag lives here, the
69+/// caller's visibility filter decides which ones take space.
70+///
71+/// Children run on the axis opposite their parent. Containers hold at least
72+/// two children, hidden ones included, and a container left with one child
73+/// is replaced by it.
74+#[derive(Clone, Debug)]
75+pub struct Tree<W> {
76+ nodes: SlotMap<NodeId, Node<W>>,
77+ root: Option<NodeId>,
78+ leaves: HashMap<W, NodeId>,
79+ stack: Axis,
80+}
81+
82+impl<W: Copy + Eq + Hash> Default for Tree<W> {
83+ fn default() -> Self {
84+ Self {
85+ nodes: SlotMap::with_key(),
86+ root: None,
87+ leaves: HashMap::new(),
88+ stack: Axis::Vertical,
89+ }
90+ }
91+}
92+
93+impl<W: Copy + Eq + Hash> Tree<W> {
94+ /// The axis tag rows stack along.
95+ pub fn stack_axis(&self) -> Axis {
96+ self.stack
97+ }
98+
99+ pub fn contains(&self, w: W) -> bool {
100+ self.leaves.contains_key(&w)
101+ }
102+
103+ pub fn is_empty(&self) -> bool {
104+ self.root.is_none()
105+ }
106+
107+ pub fn windows(&self) -> impl Iterator<Item = W> + '_ {
108+ self.leaves.keys().copied()
109+ }
110+
111+ /// Inserts `w` as the next sibling of `anchor`, or as a new row if
112+ /// `anchor` is not in the tree.
113+ pub fn insert_after(&mut self, w: W, anchor: W) {
114+ let Some(&a) = self.leaves.get(&anchor) else {
115+ return self.insert_row(w);
116+ };
117+ let n = self.new_leaf(w);
118+ match self.position(a) {
119+ Some((p, i)) if Some(p) == self.root && self.axis(p) == Some(self.stack) => {
120+ let weight = self.unlink(p, i).weight;
121+ let c = self.new_container(self.stack.other());
122+ self.attach(c, 0, a, 1.0);
123+ self.attach(c, 1, n, 1.0);
124+ self.attach(p, i, c, weight);
125+ }
126+ Some((p, i)) => {
127+ let weight = self.mean_weight(p);
128+ self.attach(p, i + 1, n, weight);
129+ }
130+ None => {
131+ let c = self.new_container(self.stack.other());
132+ self.attach(c, 0, a, 1.0);
133+ self.attach(c, 1, n, 1.0);
134+ self.root = Some(c);
135+ }
136+ }
137+ }
138+
139+ /// Appends `w` as a row of its own along the stacking axis.
140+ pub fn insert_row(&mut self, w: W) {
141+ let n = self.new_leaf(w);
142+ match self.root {
143+ None => self.root = Some(n),
144+ Some(r) if self.axis(r) == Some(self.stack) => {
145+ let weight = self.mean_weight(r);
146+ let end = self.children(r).len();
147+ self.attach(r, end, n, weight);
148+ }
149+ Some(r) => {
150+ let c = self.new_container(self.stack);
151+ self.attach(c, 0, r, 1.0);
152+ self.attach(c, 1, n, 1.0);
153+ self.root = Some(c);
154+ }
155+ }
156+ }
157+
158+ pub fn remove(&mut self, w: W) -> bool {
159+ let Some(n) = self.leaves.remove(&w) else {
160+ return false;
161+ };
162+ self.detach(n);
163+ self.nodes.remove(n);
164+ true
165+ }
166+
167+ pub fn retain(&mut self, mut keep: impl FnMut(W) -> bool) {
168+ let gone: Vec<W> = self.windows().filter(|&w| !keep(w)).collect();
169+ for w in gone {
170+ self.remove(w);
171+ }
172+ }
173+
174+ /// Moves a window one step in `dir`. Leaves swap slots with the next
175+ /// visible sibling, enter a sibling container, or step out beside their
176+ /// container. Hidden windows never move.
177+ pub fn move_window(&mut self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {
178+ let Some(&n) = self.leaves.get(&w) else {
179+ return false;
180+ };
181+ self.move_node(n, dir, &vis, true)
182+ }
183+
184+ /// Moves the container holding `w` as a rigid block. A window sitting
185+ /// directly in the root is its own block.
186+ pub fn move_block(&mut self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {
187+ let Some(&n) = self.leaves.get(&w) else {
188+ return false;
189+ };
190+ let block = self.nodes[n]
191+ .parent
192+ .filter(|&p| Some(p) != self.root)
193+ .unwrap_or(n);
194+ self.move_node(block, dir, &vis, false)
195+ }
196+
197+ /// Transposes the whole tree.
198+ pub fn flip(&mut self) {
199+ self.stack = self.stack.other();
200+ for node in self.nodes.values_mut() {
201+ if let Kind::Container { axis, .. } = &mut node.kind {
202+ *axis = axis.other();
203+ }
204+ }
205+ }
206+
207+ /// Toggles expansion for `count` levels of `w`'s ancestry, starting
208+ /// `skip` levels up. Level 0 expands the window inside its container.
209+ pub fn toggle_expand(&mut self, w: W, skip: usize, count: usize) -> bool {
210+ let Some(&leaf) = self.leaves.get(&w) else {
211+ return false;
212+ };
213+ let mut path = Vec::new();
214+ let mut cur = leaf;
215+ while let Some(p) = self.nodes[cur].parent {
216+ path.push((p, cur));
217+ cur = p;
218+ }
219+ let path: Vec<_> = path.into_iter().skip(skip).take(count).collect();
220+ if path.is_empty() {
221+ return false;
222+ }
223+ let all = path.iter().all(|&(p, c)| self.expanded(p) == Some(c));
224+ for (p, c) in path {
225+ if let Kind::Container { expanded, .. } = &mut self.nodes[p].kind {
226+ *expanded = (!all).then_some(c);
227+ }
228+ }
229+ true
230+ }
231+
232+ /// Grows `w` by `delta`, a fraction of the visible length of the nearest
233+ /// container along `axis` where it has a visible sibling. Hidden siblings
234+ /// keep their weights, and with them their share.
235+ pub fn resize(&mut self, w: W, axis: Axis, delta: f64, vis: impl Fn(W) -> bool) -> bool {
236+ let Some(&leaf) = self.leaves.get(&w) else {
237+ return false;
238+ };
239+ let mut cur = leaf;
240+ while let Some((p, i)) = self.position(cur) {
241+ if self.axis(p) == Some(axis) {
242+ let shown: Vec<usize> = (0..self.children(p).len())
243+ .filter(|&j| self.visible(self.children(p)[j].node, &vis))
244+ .collect();
245+ if shown.len() >= 2 {
246+ let children = self.children_mut(p);
247+ let sum: f64 = shown.iter().map(|&j| children[j].weight).sum();
248+ let old = children[i].weight;
249+ let max = 1.0 - MIN_SHARE * (shown.len() - 1) as f64;
250+ let new = (old / sum + delta).clamp(MIN_SHARE, max) * sum;
251+ let scale = (sum - new) / (sum - old);
252+ for j in shown {
253+ children[j].weight = if j == i {
254+ new
255+ } else {
256+ children[j].weight * scale
257+ };
258+ }
259+ return true;
260+ }
261+ }
262+ cur = p;
263+ }
264+ false
265+ }
266+
267+ /// Lays the tree out in `area`. Hidden windows come back as zero-size
268+ /// rects on the boundary where they would reappear.
269+ pub fn arrange(&self, area: Rect, gap: f64, vis: impl Fn(W) -> bool) -> Vec<(W, Rect)> {
270+ let mut out = Vec::with_capacity(self.leaves.len());
271+ if let Some(r) = self.root {
272+ self.place(r, area, gap, &vis, &mut out);
273+ }
274+ out
275+ }
276+
277+ fn place(
278+ &self,
279+ n: NodeId,
280+ r: Rect,
281+ gap: f64,
282+ vis: &impl Fn(W) -> bool,
283+ out: &mut Vec<(W, Rect)>,
284+ ) {
285+ let (axis, children, expanded) = match &self.nodes[n].kind {
286+ Kind::Leaf(w) => return out.push((*w, r)),
287+ Kind::Container {
288+ axis,
289+ children,
290+ expanded,
291+ } => (*axis, children, *expanded),
292+ };
293+ let shown: Vec<bool> = children.iter().map(|c| self.visible(c.node, vis)).collect();
294+ let (start, len) = span(r, axis);
295+ if let Some(e) = expanded.and_then(|e| children.iter().position(|c| c.node == e))
296+ && shown[e]
297+ {
298+ for (i, c) in children.iter().enumerate() {
299+ let sub = match i.cmp(&e) {
300+ std::cmp::Ordering::Equal => r,
301+ std::cmp::Ordering::Less => with_span(r, axis, start, 0.0),
302+ std::cmp::Ordering::Greater => with_span(r, axis, start + len, 0.0),
303+ };
304+ self.place(c.node, sub, gap, vis, out);
305+ }
306+ return;
307+ }
308+ let count = shown.iter().filter(|s| **s).count();
309+ let total: f64 = children
310+ .iter()
311+ .zip(&shown)
312+ .filter(|(_, s)| **s)
313+ .map(|(c, _)| c.weight)
314+ .sum();
315+ let gaps = count.saturating_sub(1) as f64;
316+ let gap = if gaps > 0.0 { gap.min(len / gaps) } else { gap };
317+ let avail = len - gap * gaps;
318+ let last = shown.iter().rposition(|s| *s);
319+ let mut pos = start;
320+ let mut seen = false;
321+ for (i, c) in children.iter().enumerate() {
322+ if shown[i] {
323+ if seen {
324+ pos += gap;
325+ }
326+ let l = avail * c.weight / total;
327+ self.place(c.node, with_span(r, axis, pos, l), gap, vis, out);
328+ pos += l;
329+ seen = true;
330+ } else {
331+ let between = seen && last.is_some_and(|l| l > i);
332+ let at = if between { pos + gap / 2.0 } else { pos };
333+ self.place(c.node, with_span(r, axis, at, 0.0), gap, vis, out);
334+ }
335+ }
336+ }
337+
338+ fn move_node(&mut self, n: NodeId, dir: Dir, vis: &impl Fn(W) -> bool, enter: bool) -> bool {
339+ let mut cur = n;
340+ while let Some((p, i)) = self.position(cur) {
341+ if self.axis(p) == Some(dir.axis()) {
342+ if cur != n {
343+ let at = if dir.forward() { i + 1 } else { i };
344+ self.relocate(n, p, at);
345+ return true;
346+ }
347+ if let Some(j) = self.next_shown(p, i, dir, vis) {
348+ let sib = self.children(p)[j].node;
349+ if enter && self.axis(sib).is_some() {
350+ let end = self.children(sib).len();
351+ self.relocate(n, sib, end);
352+ } else {
353+ let children = self.children_mut(p);
354+ children[i].node = sib;
355+ children[j].node = n;
356+ }
357+ return true;
358+ }
359+ }
360+ cur = p;
361+ }
362+ if cur == n || self.axis(cur) == Some(dir.axis()) {
363+ return false;
364+ }
365+ let (old, i) = self.position(n).expect("n is below the root");
366+ self.unlink(old, i);
367+ self.normalize(old);
368+ let r = self.root.expect("the rest of the tree survives");
369+ if self.axis(r) == Some(dir.axis()) {
370+ let weight = self.mean_weight(r);
371+ let at = if dir.forward() {
372+ self.children(r).len()
373+ } else {
374+ 0
375+ };
376+ self.attach(r, at, n, weight);
377+ } else {
378+ let c = self.new_container(dir.axis());
379+ self.attach(c, 0, r, 1.0);
380+ let at = if dir.forward() {
381+ self.children(c).len()
382+ } else {
383+ 0
384+ };
385+ self.attach(c, at, n, 1.0);
386+ self.root = Some(c);
387+ }
388+ true
389+ }
390+
391+ fn next_shown(&self, p: NodeId, i: usize, dir: Dir, vis: &impl Fn(W) -> bool) -> Option<usize> {
392+ let children = self.children(p);
393+ if dir.forward() {
394+ (i + 1..children.len()).find(|&j| self.visible(children[j].node, vis))
395+ } else {
396+ (0..i).rev().find(|&j| self.visible(children[j].node, vis))
397+ }
398+ }
399+
400+ fn relocate(&mut self, n: NodeId, p: NodeId, at: usize) {
401+ let (old, i) = self.position(n).expect("relocated nodes have a parent");
402+ let weight = self.mean_weight(p);
403+ let at = if old == p && i < at { at - 1 } else { at };
404+ self.unlink(old, i);
405+ self.attach(p, at, n, weight);
406+ self.normalize(old);
407+ }
408+
409+ /// Places `node` among `p`'s children. A container running along `p`'s
410+ /// axis is dissolved into `p` so axes keep alternating.
411+ fn attach(&mut self, p: NodeId, at: usize, node: NodeId, weight: f64) {
412+ if self.axis(node) == self.axis(p) {
413+ let Some(Node {
414+ kind: Kind::Container { children, .. },
415+ ..
416+ }) = self.nodes.remove(node)
417+ else {
418+ unreachable!("only containers have an axis");
419+ };
420+ let total: f64 = children.iter().map(|c| c.weight).sum();
421+ for (k, c) in children.into_iter().enumerate() {
422+ self.nodes[c.node].parent = Some(p);
423+ let weight = weight * c.weight / total;
424+ self.children_mut(p).insert(
425+ at + k,
426+ Child {
427+ node: c.node,
428+ weight,
429+ },
430+ );
431+ }
432+ } else {
433+ self.nodes[node].parent = Some(p);
434+ self.children_mut(p).insert(at, Child { node, weight });
435+ }
436+ }
437+
438+ fn unlink(&mut self, p: NodeId, i: usize) -> Child {
439+ let Kind::Container {
440+ children, expanded, ..
441+ } = &mut self.nodes[p].kind
442+ else {
443+ unreachable!("parents are containers");
444+ };
445+ let child = children.remove(i);
446+ if *expanded == Some(child.node) {
447+ *expanded = None;
448+ }
449+ self.nodes[child.node].parent = None;
450+ child
451+ }
452+
453+ fn detach(&mut self, n: NodeId) {
454+ match self.position(n) {
455+ Some((p, i)) => {
456+ self.unlink(p, i);
457+ self.normalize(p);
458+ }
459+ None => self.root = None,
460+ }
461+ }
462+
463+ /// Repairs a container that lost a child.
464+ fn normalize(&mut self, c: NodeId) {
465+ if self.children(c).len() >= 2 {
466+ return;
467+ }
468+ let pos = self.position(c);
469+ let was_expanded = pos.is_some_and(|(p, _)| self.expanded(p) == Some(c));
470+ let Some(Node {
471+ kind: Kind::Container { children, .. },
472+ ..
473+ }) = self.nodes.remove(c)
474+ else {
475+ unreachable!("only containers are normalized");
476+ };
477+ let only = children.into_iter().next();
478+ match (only, pos) {
479+ (None, Some((p, i))) => {
480+ self.unlink_dead(p, i);
481+ self.normalize(p);
482+ }
483+ (None, None) => self.root = None,
484+ (Some(only), Some((p, i))) => {
485+ let weight = self.unlink_dead(p, i).weight;
486+ let leaf_like = self.axis(only.node) != self.axis(p);
487+ self.attach(p, i, only.node, weight);
488+ if was_expanded
489+ && leaf_like
490+ && let Kind::Container { expanded, .. } = &mut self.nodes[p].kind
491+ {
492+ *expanded = Some(only.node);
493+ }
494+ }
495+ (Some(only), None) => {
496+ self.nodes[only.node].parent = None;
497+ self.root = Some(only.node);
498+ }
499+ }
500+ }
501+
502+ /// Like `unlink`, for a child whose node is already gone.
503+ fn unlink_dead(&mut self, p: NodeId, i: usize) -> Child {
504+ let Kind::Container {
505+ children, expanded, ..
506+ } = &mut self.nodes[p].kind
507+ else {
508+ unreachable!("parents are containers");
509+ };
510+ let child = children.remove(i);
511+ if *expanded == Some(child.node) {
512+ *expanded = None;
513+ }
514+ child
515+ }
516+
517+ fn new_leaf(&mut self, w: W) -> NodeId {
518+ let n = self.nodes.insert(Node {
519+ parent: None,
520+ kind: Kind::Leaf(w),
521+ });
522+ self.leaves.insert(w, n);
523+ n
524+ }
525+
526+ fn new_container(&mut self, axis: Axis) -> NodeId {
527+ self.nodes.insert(Node {
528+ parent: None,
529+ kind: Kind::Container {
530+ axis,
531+ children: Vec::new(),
532+ expanded: None,
533+ },
534+ })
535+ }
536+
537+ fn position(&self, n: NodeId) -> Option<(NodeId, usize)> {
538+ let p = self.nodes[n].parent?;
539+ let i = self.children(p).iter().position(|c| c.node == n)?;
540+ Some((p, i))
541+ }
542+
543+ fn axis(&self, n: NodeId) -> Option<Axis> {
544+ match &self.nodes[n].kind {
545+ Kind::Container { axis, .. } => Some(*axis),
546+ Kind::Leaf(_) => None,
547+ }
548+ }
549+
550+ fn expanded(&self, n: NodeId) -> Option<NodeId> {
551+ match &self.nodes[n].kind {
552+ Kind::Container { expanded, .. } => *expanded,
553+ Kind::Leaf(_) => None,
554+ }
555+ }
556+
557+ fn children(&self, n: NodeId) -> &[Child] {
558+ match &self.nodes[n].kind {
559+ Kind::Container { children, .. } => children,
560+ Kind::Leaf(_) => &[],
561+ }
562+ }
563+
564+ fn children_mut(&mut self, n: NodeId) -> &mut Vec<Child> {
565+ match &mut self.nodes[n].kind {
566+ Kind::Container { children, .. } => children,
567+ Kind::Leaf(_) => unreachable!("leaves have no children"),
568+ }
569+ }
570+
571+ fn mean_weight(&self, p: NodeId) -> f64 {
572+ let children = self.children(p);
573+ if children.is_empty() {
574+ 1.0
575+ } else {
576+ children.iter().map(|c| c.weight).sum::<f64>() / children.len() as f64
577+ }
578+ }
579+
580+ fn visible(&self, n: NodeId, vis: &impl Fn(W) -> bool) -> bool {
581+ match &self.nodes[n].kind {
582+ Kind::Leaf(w) => vis(*w),
583+ Kind::Container { children, .. } => children.iter().any(|c| self.visible(c.node, vis)),
584+ }
585+ }
586+
587+ #[cfg(test)]
588+ pub(crate) fn check(&self) {
589+ let reachable = match self.root {
590+ Some(r) => {
591+ assert!(self.nodes[r].parent.is_none());
592+ self.check_node(r)
593+ }
594+ None => 0,
595+ };
596+ assert_eq!(reachable, self.nodes.len(), "orphaned nodes");
597+ for (&w, &n) in &self.leaves {
598+ assert!(matches!(self.nodes[n].kind, Kind::Leaf(x) if x == w));
599+ }
600+ }
601+
602+ #[cfg(test)]
603+ fn check_node(&self, n: NodeId) -> usize {
604+ match &self.nodes[n].kind {
605+ Kind::Leaf(_) => 1,
606+ Kind::Container {
607+ axis,
608+ children,
609+ expanded,
610+ } => {
611+ assert!(
612+ children.len() >= 2,
613+ "container with {} children",
614+ children.len()
615+ );
616+ if let Some(e) = expanded {
617+ assert!(children.iter().any(|c| c.node == *e));
618+ }
619+ let mut count = 1;
620+ for c in children {
621+ assert!(c.weight > 0.0 && c.weight.is_finite());
622+ assert_eq!(self.nodes[c.node].parent, Some(n));
623+ assert_ne!(self.axis(c.node), Some(*axis), "axes must alternate");
624+ count += self.check_node(c.node);
625+ }
626+ count
627+ }
628+ }
629+ }
630+}
631+
632+fn span(r: Rect, axis: Axis) -> (f64, f64) {
633+ match axis {
634+ Axis::Horizontal => (r.x, r.w),
635+ Axis::Vertical => (r.y, r.h),
636+ }
637+}
638+
639+fn with_span(r: Rect, axis: Axis, start: f64, len: f64) -> Rect {
640+ match axis {
641+ Axis::Horizontal => Rect::new(start, r.y, len, r.h),
642+ Axis::Vertical => Rect::new(r.x, start, r.w, len),
643+ }
644+}