atagen

monolith

public
Star0
Watch0

Commit 4cbf881c67bd

tarball

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

3 files changed+239-230
layout: move output parking and scratchpads into their own modules

Both are self-contained state machines on top of placement, and lib.rs had passed 1000 lines with them inline.
Modified file: crates/layout/src/lib.rs+3 additions−230 deletions
@@ -2,6 +2,8 @@
22 //! tiling tree per output filtered by those views, floating windows,
33 //! scratchpads, focus, and restoring windows when an output comes back.
44
5+mod outputs;
6+mod scratchpad;
57 mod tree;
68
79 #[cfg(test)]
@@ -10,6 +12,7 @@ mod tests;
1012 pub use monolith_planner::Rect;
1113 pub use tree::{Axis, Dir, Tree};
1214
15+use outputs::Parked;
1316 use slotmap::{SlotMap, new_key_type};
1417 use std::collections::HashMap;
1518
@@ -90,14 +93,6 @@ pub struct Scratchpad {
9093 pub output: Option<OutputId>,
9194 }
9295
93-#[derive(Clone, Debug)]
94-struct Parked {
95- tree: Tree<WindowId>,
96- viewed: Vec<TagId>,
97- floating: Vec<WindowId>,
98- area: Rect,
99-}
100-
10196 #[derive(Clone, Debug, Default)]
10297 pub struct Arrangement {
10398 /// Every tiled window on the output. Hidden ones are zero-size seeds.
@@ -220,139 +215,6 @@ impl Layout {
220215 }
221216 }
222217
223- /// Adds an output. One that was unplugged before gets back its tree,
224- /// views and the windows that had to leave it.
225- pub fn add_output(&mut self, name: &str, area: Rect) -> OutputId {
226- let id = self.outputs.insert(Output {
227- name: name.into(),
228- area,
229- reserved: Insets::default(),
230- tree: Tree::default(),
231- viewed: Vec::new(),
232- floating: Vec::new(),
233- });
234- if let Some(parked) = self.parked.remove(name) {
235- self.restore(id, parked);
236- }
237- self.active.get_or_insert(id);
238- let detached: Vec<WindowId> = self
239- .windows
240- .iter()
241- .filter(|(_, w)| w.output.is_none())
242- .map(|(id, _)| id)
243- .collect();
244- for w in detached {
245- self.windows[w].output = Some(id);
246- self.place(w);
247- }
248- id
249- }
250-
251- fn restore(&mut self, id: OutputId, parked: Parked) {
252- let name = self.outputs[id].name.clone();
253- let ours = |l: &Self, w: WindowId| {
254- l.windows
255- .get(w)
256- .is_some_and(|x| x.evicted_from.as_deref() == Some(&name) && x.scratchpad.is_none())
257- };
258- let mut tree = parked.tree;
259- tree.retain(|w| ours(self, w) && !self.windows[w].floating);
260- let floating: Vec<WindowId> = parked
261- .floating
262- .into_iter()
263- .filter(|&w| ours(self, w) && self.windows[w].floating)
264- .collect();
265- for w in tree
266- .windows()
267- .chain(floating.iter().copied())
268- .collect::<Vec<_>>()
269- {
270- self.unplace(w);
271- let win = &mut self.windows[w];
272- if let (Some(from), Some(rect)) = (win.output, win.float_rect) {
273- win.float_rect = Some(translate(rect, self.outputs[from].area, parked.area));
274- }
275- win.output = Some(id);
276- win.evicted_from = None;
277- }
278- let out = &mut self.outputs[id];
279- out.tree = tree;
280- out.floating = floating;
281- out.viewed = parked
282- .viewed
283- .into_iter()
284- .filter(|&t| self.tags.contains_key(t))
285- .collect();
286- }
287-
288- /// Removes an output. Its windows move to an output viewing their tag,
289- /// or the active one, and remember where they came from.
290- pub fn remove_output(&mut self, o: OutputId) {
291- let Some(out) = self.outputs.remove(o) else {
292- return;
293- };
294- self.parked.insert(
295- out.name.clone(),
296- Parked {
297- tree: out.tree,
298- viewed: out.viewed,
299- floating: out.floating,
300- area: out.area,
301- },
302- );
303- if self.active == Some(o) {
304- self.active = self.outputs.keys().next();
305- }
306- for sp in &mut self.scratchpads {
307- if sp.output == Some(o) {
308- sp.output = self.active;
309- }
310- }
311- let leaving: Vec<WindowId> = self
312- .windows
313- .iter()
314- .filter(|(_, w)| w.output == Some(o))
315- .map(|(id, _)| id)
316- .collect();
317- for w in leaving {
318- let tag = self.windows[w].tag;
319- let target = self
320- .outputs
321- .keys()
322- .find(|&t| self.is_viewed(t, tag))
323- .or(self.active);
324- let win = &mut self.windows[w];
325- if let (Some(t), Some(rect)) = (target, win.float_rect) {
326- win.float_rect = Some(translate(rect, out.area, self.outputs[t].area));
327- }
328- win.output = target;
329- if win.scratchpad.is_some() {
330- continue;
331- }
332- win.evicted_from = Some(out.name.clone());
333- if target.is_some() {
334- self.place(w);
335- }
336- }
337- }
338-
339- pub fn set_output_area(&mut self, o: OutputId, area: Rect) {
340- if let Some(out) = self.outputs.get_mut(o) {
341- out.area = area;
342- }
343- }
344-
345- /// Returns whether anything changed.
346- pub fn set_reserved(&mut self, o: OutputId, reserved: Insets) -> bool {
347- match self.outputs.get_mut(o) {
348- Some(out) if out.reserved != reserved => {
349- out.reserved = reserved;
350- true
351- }
352- _ => false,
353- }
354- }
355-
356218 pub fn is_viewed(&self, o: OutputId, tag: TagId) -> bool {
357219 tag == self.homeless
358220 || self
@@ -760,95 +622,6 @@ impl Layout {
760622 }
761623 }
762624
763- /// Adds a window to a named scratchpad, creating it. Scratchpad windows
764- /// float and are shown only by summoning.
765- pub fn scratchpad_add(&mut self, w: WindowId, name: &str) {
766- let Some(win) = self.windows.get(w) else {
767- return;
768- };
769- if let Some(s) = win.scratchpad {
770- self.leave_scratchpad(s, w);
771- }
772- self.unplace(w);
773- let s = match self.scratchpads.iter().position(|sp| sp.name == name) {
774- Some(s) => s,
775- None => {
776- self.scratchpads.push(Scratchpad {
777- name: name.into(),
778- windows: Vec::new(),
779- shown: None,
780- output: None,
781- });
782- self.scratchpads.len() - 1
783- }
784- };
785- self.scratchpads[s].windows.push(w);
786- let area = self.windows[w].output.map(|o| self.outputs[o].area);
787- let win = &mut self.windows[w];
788- win.scratchpad = Some(s);
789- win.floating = true;
790- if win.float_rect.is_none() {
791- win.float_rect = area.map(|a| centred(a, 0.6));
792- }
793- }
794-
795- /// Cycles a scratchpad through off, each window, and off again, on the
796- /// active output.
797- pub fn summon(&mut self, name: &str) -> Option<WindowId> {
798- let s = self.scratchpads.iter().position(|sp| sp.name == name)?;
799- let o = self.active?;
800- let sp = &mut self.scratchpads[s];
801- let next = match sp.shown {
802- None => 0,
803- Some(i) => i + 1,
804- };
805- if next >= sp.windows.len() {
806- sp.shown = None;
807- return None;
808- }
809- let from = sp.output.map(|f| self.outputs[f].area);
810- sp.shown = Some(next);
811- sp.output = Some(o);
812- let w = sp.windows[next];
813- self.last_scratchpad = Some(s);
814- let area = self.outputs[o].area;
815- let win = &mut self.windows[w];
816- if win.output != Some(o) {
817- win.float_rect = match (from, win.float_rect) {
818- (Some(from), Some(r)) => Some(translate(r, from, area)),
819- _ => Some(centred(area, 0.6)),
820- };
821- win.output = Some(o);
822- }
823- self.focus(w);
824- Some(w)
825- }
826-
827- /// Hides a scratchpad, by default the last one summoned.
828- pub fn dismiss(&mut self, name: Option<&str>) {
829- let s = match name {
830- Some(name) => self.scratchpads.iter().position(|sp| sp.name == name),
831- None => self.last_scratchpad,
832- };
833- if let Some(sp) = s.and_then(|s| self.scratchpads.get_mut(s)) {
834- sp.shown = None;
835- }
836- }
837-
838- fn leave_scratchpad(&mut self, s: usize, w: WindowId) {
839- let sp = &mut self.scratchpads[s];
840- let Some(i) = sp.windows.iter().position(|&x| x == w) else {
841- return;
842- };
843- sp.windows.remove(i);
844- sp.shown = match sp.shown {
845- Some(j) if j == i => None,
846- Some(j) if j > i => Some(j - 1),
847- other => other,
848- };
849- self.windows[w].scratchpad = None;
850- }
851-
852625 /// Target geometry for everything on `o`, in output-local logical
853626 /// coordinates offset by the output's area.
854627 pub fn arrange(&self, o: OutputId) -> Arrangement {
Added file: crates/layout/src/outputs.rs+144 additions
@@ -0,0 +1,144 @@
1+use crate::{Insets, Layout, Output, OutputId, Rect, TagId, Tree, WindowId, translate};
2+
3+#[derive(Clone, Debug)]
4+pub(crate) struct Parked {
5+ tree: Tree<WindowId>,
6+ viewed: Vec<TagId>,
7+ floating: Vec<WindowId>,
8+ area: Rect,
9+}
10+
11+impl Layout {
12+ /// Adds an output. One that was unplugged before gets back its tree,
13+ /// views and the windows that had to leave it.
14+ pub fn add_output(&mut self, name: &str, area: Rect) -> OutputId {
15+ let id = self.outputs.insert(Output {
16+ name: name.into(),
17+ area,
18+ reserved: Insets::default(),
19+ tree: Tree::default(),
20+ viewed: Vec::new(),
21+ floating: Vec::new(),
22+ });
23+ if let Some(parked) = self.parked.remove(name) {
24+ self.restore(id, parked);
25+ }
26+ self.active.get_or_insert(id);
27+ let detached: Vec<WindowId> = self
28+ .windows
29+ .iter()
30+ .filter(|(_, w)| w.output.is_none())
31+ .map(|(id, _)| id)
32+ .collect();
33+ for w in detached {
34+ self.windows[w].output = Some(id);
35+ self.place(w);
36+ }
37+ id
38+ }
39+
40+ fn restore(&mut self, id: OutputId, parked: Parked) {
41+ let name = self.outputs[id].name.clone();
42+ let ours = |l: &Self, w: WindowId| {
43+ l.windows
44+ .get(w)
45+ .is_some_and(|x| x.evicted_from.as_deref() == Some(&name) && x.scratchpad.is_none())
46+ };
47+ let mut tree = parked.tree;
48+ tree.retain(|w| ours(self, w) && !self.windows[w].floating);
49+ let floating: Vec<WindowId> = parked
50+ .floating
51+ .into_iter()
52+ .filter(|&w| ours(self, w) && self.windows[w].floating)
53+ .collect();
54+ for w in tree
55+ .windows()
56+ .chain(floating.iter().copied())
57+ .collect::<Vec<_>>()
58+ {
59+ self.unplace(w);
60+ let win = &mut self.windows[w];
61+ if let (Some(from), Some(rect)) = (win.output, win.float_rect) {
62+ win.float_rect = Some(translate(rect, self.outputs[from].area, parked.area));
63+ }
64+ win.output = Some(id);
65+ win.evicted_from = None;
66+ }
67+ let out = &mut self.outputs[id];
68+ out.tree = tree;
69+ out.floating = floating;
70+ out.viewed = parked
71+ .viewed
72+ .into_iter()
73+ .filter(|&t| self.tags.contains_key(t))
74+ .collect();
75+ }
76+
77+ /// Removes an output. Its windows move to an output viewing their tag,
78+ /// or the active one, and remember where they came from.
79+ pub fn remove_output(&mut self, o: OutputId) {
80+ let Some(out) = self.outputs.remove(o) else {
81+ return;
82+ };
83+ self.parked.insert(
84+ out.name.clone(),
85+ Parked {
86+ tree: out.tree,
87+ viewed: out.viewed,
88+ floating: out.floating,
89+ area: out.area,
90+ },
91+ );
92+ if self.active == Some(o) {
93+ self.active = self.outputs.keys().next();
94+ }
95+ for sp in &mut self.scratchpads {
96+ if sp.output == Some(o) {
97+ sp.output = self.active;
98+ }
99+ }
100+ let leaving: Vec<WindowId> = self
101+ .windows
102+ .iter()
103+ .filter(|(_, w)| w.output == Some(o))
104+ .map(|(id, _)| id)
105+ .collect();
106+ for w in leaving {
107+ let tag = self.windows[w].tag;
108+ let target = self
109+ .outputs
110+ .keys()
111+ .find(|&t| self.is_viewed(t, tag))
112+ .or(self.active);
113+ let win = &mut self.windows[w];
114+ if let (Some(t), Some(rect)) = (target, win.float_rect) {
115+ win.float_rect = Some(translate(rect, out.area, self.outputs[t].area));
116+ }
117+ win.output = target;
118+ if win.scratchpad.is_some() {
119+ continue;
120+ }
121+ win.evicted_from = Some(out.name.clone());
122+ if target.is_some() {
123+ self.place(w);
124+ }
125+ }
126+ }
127+
128+ pub fn set_output_area(&mut self, o: OutputId, area: Rect) {
129+ if let Some(out) = self.outputs.get_mut(o) {
130+ out.area = area;
131+ }
132+ }
133+
134+ /// Returns whether anything changed.
135+ pub fn set_reserved(&mut self, o: OutputId, reserved: Insets) -> bool {
136+ match self.outputs.get_mut(o) {
137+ Some(out) if out.reserved != reserved => {
138+ out.reserved = reserved;
139+ true
140+ }
141+ _ => false,
142+ }
143+ }
144+}
Added file: crates/layout/src/scratchpad.rs+92 additions
@@ -0,0 +1,92 @@
1+use crate::{Layout, Scratchpad, WindowId, centred, translate};
2+
3+impl Layout {
4+ /// Adds a window to a named scratchpad, creating it. Scratchpad windows
5+ /// float and are shown only by summoning.
6+ pub fn scratchpad_add(&mut self, w: WindowId, name: &str) {
7+ let Some(win) = self.windows.get(w) else {
8+ return;
9+ };
10+ if let Some(s) = win.scratchpad {
11+ self.leave_scratchpad(s, w);
12+ }
13+ self.unplace(w);
14+ let s = match self.scratchpads.iter().position(|sp| sp.name == name) {
15+ Some(s) => s,
16+ None => {
17+ self.scratchpads.push(Scratchpad {
18+ name: name.into(),
19+ windows: Vec::new(),
20+ shown: None,
21+ output: None,
22+ });
23+ self.scratchpads.len() - 1
24+ }
25+ };
26+ self.scratchpads[s].windows.push(w);
27+ let area = self.windows[w].output.map(|o| self.outputs[o].area);
28+ let win = &mut self.windows[w];
29+ win.scratchpad = Some(s);
30+ win.floating = true;
31+ if win.float_rect.is_none() {
32+ win.float_rect = area.map(|a| centred(a, 0.6));
33+ }
34+ }
35+
36+ /// Cycles a scratchpad through off, each window, and off again, on the
37+ /// active output.
38+ pub fn summon(&mut self, name: &str) -> Option<WindowId> {
39+ let s = self.scratchpads.iter().position(|sp| sp.name == name)?;
40+ let o = self.active?;
41+ let sp = &mut self.scratchpads[s];
42+ let next = match sp.shown {
43+ None => 0,
44+ Some(i) => i + 1,
45+ };
46+ if next >= sp.windows.len() {
47+ sp.shown = None;
48+ return None;
49+ }
50+ let from = sp.output.map(|f| self.outputs[f].area);
51+ sp.shown = Some(next);
52+ sp.output = Some(o);
53+ let w = sp.windows[next];
54+ self.last_scratchpad = Some(s);
55+ let area = self.outputs[o].area;
56+ let win = &mut self.windows[w];
57+ if win.output != Some(o) {
58+ win.float_rect = match (from, win.float_rect) {
59+ (Some(from), Some(r)) => Some(translate(r, from, area)),
60+ _ => Some(centred(area, 0.6)),
61+ };
62+ win.output = Some(o);
63+ }
64+ self.focus(w);
65+ Some(w)
66+ }
67+
68+ /// Hides a scratchpad, by default the last one summoned.
69+ pub fn dismiss(&mut self, name: Option<&str>) {
70+ let s = match name {
71+ Some(name) => self.scratchpads.iter().position(|sp| sp.name == name),
72+ None => self.last_scratchpad,
73+ };
74+ if let Some(sp) = s.and_then(|s| self.scratchpads.get_mut(s)) {
75+ sp.shown = None;
76+ }
77+ }
78+
79+ pub(crate) fn leave_scratchpad(&mut self, s: usize, w: WindowId) {
80+ let sp = &mut self.scratchpads[s];
81+ let Some(i) = sp.windows.iter().position(|&x| x == w) else {
82+ return;
83+ };
84+ sp.windows.remove(i);
85+ sp.shown = match sp.shown {
86+ Some(j) if j == i => None,
87+ Some(j) if j > i => Some(j - 1),
88+ other => other,
89+ };
90+ self.windows[w].scratchpad = None;
91+ }
92+}