atagen

monolith

public
Star0
Watch0

Commit 4b73e40af1e3

tarball

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

4 files changed+85-21
layout: rescale floating windows with their output

set_output_area and set_reserved left float_rect untouched, so a mode change, rotation or new bar stranded floats partly off screen. They now map the rect proportionally from the old usable area to the new one and clamp it. Moves between outputs share that translate, which used to scale only the position and cap the size.
Modified file: crates/layout/src/lib.rs+12 additions−7 deletions
@@ -664,11 +664,11 @@ impl Layout {
664664 if !self.outputs.contains_key(o) || win.output == Some(o) || win.scratchpad.is_some() {
665665 return;
666666 }
667- let from = win.output.map(|f| self.outputs[f].area);
667+ let from = win.output.map(|f| self.outputs[f].usable());
668668 self.unplace(w);
669669 let win = &mut self.windows[w];
670670 if let (Some(from), Some(rect)) = (from, win.float_rect) {
671- win.float_rect = Some(translate(rect, from, self.outputs[o].area));
671+ win.float_rect = Some(translate(rect, from, self.outputs[o].usable()));
672672 }
673673 win.output = Some(o);
674674 win.evicted_from = None;
@@ -948,14 +948,19 @@ pub fn clamp_float(r: Rect, bounds: Rect) -> Rect {
948948 Rect::new(x, y, r.w, r.h)
949949 }
950950
951-/// Keeps a rect at the same relative spot when it changes output.
951+/// Keeps a floating rect at the same relative spot and size when the area
952+/// it lives in changes.
952953 fn translate(r: Rect, from: Rect, to: Rect) -> Rect {
954+ if to.w <= 0.0 || to.h <= 0.0 {
955+ return r;
956+ }
953957 let sx = if from.w > 0.0 { to.w / from.w } else { 1.0 };
954958 let sy = if from.h > 0.0 { to.h / from.h } else { 1.0 };
955- Rect::new(
959+ let scaled = Rect::new(
956960 to.x + (r.x - from.x) * sx,
957961 to.y + (r.y - from.y) * sy,
958- r.w.min(to.w),
959- r.h.min(to.h),
960- )
962+ (r.w * sx).max(MIN_FLOAT_SIZE),
963+ (r.h * sy).max(MIN_FLOAT_SIZE),
964+ );
965+ clamp_float(scaled, to)
961966 }
Modified file: crates/layout/src/outputs.rs+34 additions−12 deletions
@@ -5,7 +5,6 @@ pub(crate) struct Parked {
55 tree: Tree<WindowId>,
66 viewed: Vec<TagId>,
77 floating: Vec<WindowId>,
8- area: Rect,
98 }
109
1110 impl Layout {
@@ -59,7 +58,11 @@ impl Layout {
5958 self.unplace(w);
6059 let win = &mut self.windows[w];
6160 if let (Some(from), Some(rect)) = (win.output, win.float_rect) {
62- win.float_rect = Some(translate(rect, self.outputs[from].area, parked.area));
61+ win.float_rect = Some(translate(
62+ rect,
63+ self.outputs[from].usable(),
64+ self.outputs[id].usable(),
65+ ));
6366 }
6467 win.output = Some(id);
6568 win.evicted_from = None;
@@ -80,13 +83,13 @@ impl Layout {
8083 let Some(out) = self.outputs.remove(o) else {
8184 return;
8285 };
86+ let left_usable = out.usable();
8387 self.parked.insert(
8488 out.name.clone(),
8589 Parked {
8690 tree: out.tree,
8791 viewed: out.viewed,
8892 floating: out.floating,
89- area: out.area,
9093 },
9194 );
9295 if self.active == Some(o) {
@@ -112,7 +115,7 @@ impl Layout {
112115 .or(self.active);
113116 let win = &mut self.windows[w];
114117 if let (Some(t), Some(rect)) = (target, win.float_rect) {
115- win.float_rect = Some(translate(rect, out.area, self.outputs[t].area));
118+ win.float_rect = Some(translate(rect, left_usable, self.outputs[t].usable()));
116119 }
117120 win.output = target;
118121 if win.scratchpad.is_some() {
@@ -126,19 +129,38 @@ impl Layout {
126129 }
127130
128131 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+ let Some(out) = self.outputs.get_mut(o) else {
133+ return;
134+ };
135+ let before = out.usable();
136+ out.area = area;
137+ self.rescale_floats(o, before);
132138 }
133139
134140 /// Returns whether anything changed.
135141 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
142+ let Some(out) = self.outputs.get_mut(o) else {
143+ return false;
144+ };
145+ if out.reserved == reserved {
146+ return false;
147+ }
148+ let before = out.usable();
149+ out.reserved = reserved;
150+ self.rescale_floats(o, before);
151+ true
152+ }
153+
154+ /// Carries floating windows along when the usable area of o changes.
155+ fn rescale_floats(&mut self, o: OutputId, before: Rect) {
156+ let after = self.outputs[o].usable();
157+ if after == before {
158+ return;
159+ }
160+ for win in self.windows.values_mut().filter(|w| w.output == Some(o)) {
161+ if let Some(rect) = win.float_rect {
162+ win.float_rect = Some(translate(rect, before, after));
140163 }
141- _ => false,
142164 }
143165 }
144166 }
Modified file: crates/layout/src/scratchpad.rs+3 additions−2 deletions
@@ -47,16 +47,17 @@ impl Layout {
4747 sp.shown = None;
4848 return None;
4949 }
50- let from = sp.output.map(|f| self.outputs[f].area);
50+ let from = sp.output.map(|f| self.outputs[f].usable());
5151 sp.shown = Some(next);
5252 sp.output = Some(o);
5353 let w = sp.windows[next];
5454 self.last_scratchpad = Some(s);
5555 let area = self.outputs[o].area;
56+ let usable = self.outputs[o].usable();
5657 let win = &mut self.windows[w];
5758 if win.output != Some(o) {
5859 win.float_rect = match (from, win.float_rect) {
59- (Some(from), Some(r)) => Some(translate(r, from, area)),
60+ (Some(from), Some(r)) => Some(translate(r, from, usable)),
6061 _ => Some(centred(area, 0.6)),
6162 };
6263 win.output = Some(o);
Modified file: crates/layout/src/tests.rs+36 additions
@@ -641,3 +641,39 @@ fn oversized_floats_keep_their_top_edge_and_a_strip() {
641641 let fits = clamp_float(Rect::new(900.0, 500.0, 300.0, 200.0), bounds);
642642 assert_eq!((fits.x, fits.y), (700.0, 400.0));
643643 }
644+
645+#[test]
646+fn floats_follow_their_output_area_and_panels() {
647+ let mut l = Layout::default();
648+ let o = l.add_output("a", AREA);
649+ let w = l.add_window(Some(o), None, Some(Rect::new(500.0, 300.0, 400.0, 300.0)));
650+ l.set_output_area(o, Rect::new(100.0, 50.0, 500.0, 300.0));
651+ let r = l.window(w).unwrap().float_rect.unwrap();
652+ assert_eq!((r.x, r.y, r.w, r.h), (350.0, 200.0, 200.0, 150.0));
653+
654+ l.set_reserved(
655+ o,
656+ Insets {
657+ top: 100.0,
658+ ..Insets::default()
659+ },
660+ );
661+ let r = l.window(w).unwrap().float_rect.unwrap();
662+ let usable = l.output(o).unwrap().usable();
663+ assert!(r.y >= usable.y && r.bottom() <= usable.bottom());
664+}
665+
666+#[test]
667+fn floats_shrink_into_a_smaller_output_they_are_evicted_to() {
668+ let mut l = Layout::default();
669+ let a = l.add_output("a", AREA);
670+ let b = l.add_output("b", Rect::new(1000.0, 0.0, 500.0, 300.0));
671+ let w = l.add_window(Some(a), None, Some(Rect::new(800.0, 400.0, 200.0, 200.0)));
672+ l.remove_output(a);
673+ assert_eq!(l.window(w).unwrap().output, Some(b));
674+ let r = l.window(w).unwrap().float_rect.unwrap();
675+ let usable = l.output(b).unwrap().usable();
676+ assert_eq!((r.w, r.h), (100.0, 100.0));
677+ assert!(r.x >= usable.x && r.right() <= usable.right());
678+ assert!(r.y >= usable.y && r.bottom() <= usable.bottom());
679+}