atagen

monolith

public
Star0
Watch0

Commit 406db966344c

tarball

atagen <boss@atagen.co>2026-10-20 12:08 UTC

3 files changed+52-25
scene: draw a double border around an extended window

An extended window looked like any other tile, giving no hint that moving it carries the windows it covers. A second ring one border width further out marks it as a group.
Modified file: crates/layout/src/lib.rs+8 additions
@@ -647,6 +647,14 @@ impl Layout {
647647 merged
648648 }
649649
650+ /// Whether a tiled window is extended over others sharing its container.
651+ pub fn grouped(&self, w: WindowId) -> bool {
652+ let Some(o) = self.windows.get(w).and_then(|w| w.output) else {
653+ return false;
654+ };
655+ self.outputs[o].tree.grouped(w, self.vis_on(o))
656+ }
657+
650658 /// Whether another window is extended over the tiled window `w`, which
651659 /// leaves it no room and takes it out of reach of focus.
652660 pub fn eclipsed(&self, w: WindowId) -> bool {
Modified file: crates/monolith/src/scene.rs+42 additions−23 deletions
@@ -193,11 +193,24 @@ pub fn build(
193193 deco.unfocused.0
194194 };
195195 let alpha = effect.map_or(1.0, |e| e.progress) * opacity;
196- let borders = &mut state.borders;
197- let first = out.len();
198- border_elements(&mut out, borders, r, deco, color, alpha, scale);
199- if let Some(clip) = clip {
200- ring_borders(&mut out, first, clip, border_width(deco, scale));
196+ let b = border_width(deco, scale);
197+ let radius = deco.radius.0.into_iter().max().unwrap_or(0);
198+ let radius = (f64::from(radius) * scale).round() as i32;
199+ let rings = if layout.grouped(id) { 2 } else { 1 };
200+ let Some((at, size)) = pixels(r, scale) else {
201+ continue;
202+ };
203+ let window = Rectangle::new(at.into(), size.into());
204+ for (k, borders) in state.borders.iter_mut().take(rings).enumerate() {
205+ let by = 2 * b * k as i32;
206+ let frame = outset_rect(window, by);
207+ let reach = if radius > 0 { radius + by } else { 0 };
208+ let reach = reach.min(frame.size.w / 2).min(frame.size.h / 2);
209+ let first = out.len();
210+ border_elements(&mut out, borders, frame, b, reach, color, alpha);
211+ if let Some(clip) = clip {
212+ ring_borders(&mut out, first, outset(clip, by), b);
213+ }
201214 }
202215 }
203216 }
@@ -342,24 +355,19 @@ fn window_elements(
342355 );
343356 }
344357
345-/// Four strips just outside `r`, in the gap, so the client keeps its size.
346-/// With rounded corners they reach in by the largest radius, to cover the
347-/// band the ring curves through inside the window's corners.
358+/// Four strips `b` wide just outside `frame`, in the gap, so the client keeps
359+/// its size. With rounded corners they `reach` in by the largest radius, to
360+/// cover the band the ring curves through inside the frame's corners.
348361 fn border_elements(
349362 out: &mut Vec<Draw>,
350363 borders: &mut Option<[SolidColorBuffer; 4]>,
351- r: Rect,
352- deco: &Decoration,
364+ frame: Rectangle<i32, Physical>,
365+ b: i32,
366+ reach: i32,
353367 color: [f32; 4],
354368 alpha: f32,
355- scale: f64,
356369 ) {
357- let Some(((x, y), (w, h))) = pixels(r, scale) else {
358- return;
359- };
360- let b = border_width(deco, scale);
361- let reach = (f64::from(deco.radius.0.into_iter().max().unwrap_or(0)) * scale).round() as i32;
362- let reach = reach.min(w / 2).min(h / 2);
370+ let (x, y, w, h) = (frame.loc.x, frame.loc.y, frame.size.w, frame.size.h);
363371 let strips = [
364372 ((x - b, y - b), (w + 2 * b, b + reach)),
365373 ((x - b, y + h - reach), (w + 2 * b, b + reach)),
@@ -662,20 +670,31 @@ fn clip_window(out: &mut [Draw], first: usize, clip: Clip) {
662670 /// Turns the border strips from `first` on into a ring around the window's
663671 /// rounded outline.
664672 fn ring_borders(out: &mut [Draw], first: usize, window: Clip, b: i32) {
665- let grow = |r: f32| if r > 0.0 { r + b as f32 } else { 0.0 };
666673 let ring = Clip {
667- rect: Rectangle::new(
668- window.rect.loc - Point::from((b, b)),
669- window.rect.size + Size::from((2 * b, 2 * b)),
670- ),
671- radii: window.radii.map(grow),
672674 ring: b as f32,
675+ ..outset(window, b)
673676 };
674677 for draw in &mut out[first..] {
675678 wrap(draw, ring);
676679 }
677680 }
678681
682+fn outset(clip: Clip, by: i32) -> Clip {
683+ let grow = |r: f32| if r > 0.0 { r + by as f32 } else { 0.0 };
684+ Clip {
685+ rect: outset_rect(clip.rect, by),
686+ radii: clip.radii.map(grow),
687+ ring: clip.ring,
688+ }
689+}
690+
691+fn outset_rect(r: Rectangle<i32, Physical>, by: i32) -> Rectangle<i32, Physical> {
692+ Rectangle::new(
693+ r.loc - Point::from((by, by)),
694+ r.size + Size::from((2 * by, 2 * by)),
695+ )
696+}
697+
679698 fn wrap(draw: &mut Draw, clip: Clip) {
680699 let inner = std::mem::replace(draw, Draw::Clipped(ClipElement::empty()));
681700 *draw = Draw::Clipped(ClipElement::new(inner, clip));
Modified file: crates/monolith/src/wm.rs+2 additions−2 deletions
@@ -99,7 +99,7 @@ pub struct WindowState {
9999 /// A rule sent it somewhere, so activation tokens leave it alone.
100100 pub placed_by_rule: bool,
101101 pub list_handle: Option<ForeignToplevelHandle>,
102- pub borders: Option<[SolidColorBuffer; 4]>,
102+ pub borders: [Option<[SolidColorBuffer; 4]>; 2],
103103 pub app_id: String,
104104 pub title: String,
105105 /// The title it mapped with, which `initial_title` rules match.
@@ -172,7 +172,7 @@ impl Wm {
172172 reclassify: false,
173173 placed_by_rule,
174174 list_handle: None,
175- borders: None,
175+ borders: [None, None],
176176 app_id,
177177 initial_title: title.clone(),
178178 title,