git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit18feea978f360be40016177b055c272600940f31
parentd48785e1eb
authorLucas Galante <[email protected]>
date2026-09-18 00:43
feat(borders): dim a Floating window that covers the adjust target

While adjust mode is on (overview, or Super held), a Floating window
lying above the window whose handles are up and overlapping it on
screen eases down to `border.overlap_opacity` (default 0.4; 1.0
disables), so it does not hide the handles; it eases back when the mode
ends, the target moves elsewhere, or the overlap stops. Windows beneath
the target are left alone.

`Window::adjust_dim_wanted` walks the render list bottom-up for a target
met before the window; `step_adjust_dim` eases `adjust_dim` on the
border-fade timer and applies the opacity; `effective_opacity` folds it
into the tree opacity render_finish sets. arrange_views, raise_window
and every op_update step re-arm the fade so a move, resize or restack
re-evaluates who covers whom.

Verified in a shadow with two overlapping floating windows: with Super
held over the lower one the upper dims to ~40% and the lower's discs
show through; the pointer moving onto the upper restores it and leaves
the lower undimmed; releasing Super restores everything.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 CLAUDE.md                    | 11 ++++++
 src/server/config.rs         | 16 ++++++++
 src/server/seat.rs           |  3 ++
 src/server/window.rs         | 89 +++++++++++++++++++++++++++++++++++++++++++-
 src/server/window_manager.rs | 12 ++++++
 5 files changed, 130 insertions(+), 1 deletion(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 5551ae9..b4a1a02 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -384,6 +384,17 @@ sat outside the edges and the ring that followed hugged them.
   keeps a zoomed-out window from being mostly handle. `draw_borders` and
   `cursor::get_border_zone` each derive it the same way and must stay in
   step.
+- **A Floating window lying over the adjust target is dimmed** to
+  `border.overlap_opacity` (default 0.4; 1.0 disables) while the mode is
+  on, so it does not hide the handles. `Window::adjust_dim_wanted` walks
+  the render list bottom-up: only windows ABOVE the target that overlap it
+  on screen qualify (one beneath hides nothing). `step_adjust_dim` eases
+  `adjust_dim` on the same border-fade timer as the ring, and
+  `effective_opacity` folds it into the scene-tree opacity `render_finish`
+  sets — set the tree opacity through that, never from
+  `rendering_requested.opacity` directly, or the dim is clobbered on the
+  next commit. Anything that can change who covers whom re-arms the fade:
+  `arrange_views`, `raise_window`, and every `op_update` step.
 - `handle_width` under `border` in config.kdl is the diameter. `taper`,
   `swell_curve`, `bulge`, `corner_length` and `segment_gap` belonged to the
   retired ring profiles (an even ring, then a wave of hills and valleys):
diff --git a/src/server/config.rs b/src/server/config.rs
index 7ece8f7..e5d55b8 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -38,6 +38,9 @@ pub struct Layout {
     /// ring has to be thick enough to see and hit while the desktop is zoomed
     /// out, and the window's visible border is a much finer line than that.
     pub border_handle_width: f32,
+    /// Opacity a Floating window is dimmed to while it overlaps the window
+    /// whose resize handles are up (adjust mode), 0..1. 1.0 disables.
+    pub border_overlap_opacity: f32,
     /// Shape of the handle ring's swell along a side. Below 1 the ring gains
     /// its thickness early — a corner that visibly swells, then a long slow
     /// approach to the middle. Above 1 stays thin near the corner and gains
@@ -210,6 +213,7 @@ impl Default for Layout {
             border_segment_gap: 4,
             border_taper: 0.35,
             border_handle_width: 32.0,
+            border_overlap_opacity: 0.4,
             border_swell_curve: 0.45,
             border_corner_bulge: 48.0,
             border_corner_length: 0,
@@ -566,6 +570,8 @@ pub struct SurfaceConfig {
     pub border_taper: f64,
     #[serde(default = "default_border_handle_width")]
     pub border_handle_width: f64,
+    #[serde(default = "default_border_overlap_opacity")]
+    pub border_overlap_opacity: f64,
     #[serde(default = "default_border_swell_curve")]
     pub border_swell_curve: f64,
     #[serde(default = "default_border_corner_bulge")]
@@ -671,6 +677,7 @@ impl Default for SurfaceConfig {
             border_segment_gap: default_border_segment_gap(),
             border_taper: default_border_taper(),
             border_handle_width: default_border_handle_width(),
+            border_overlap_opacity: default_border_overlap_opacity(),
             border_swell_curve: default_border_swell_curve(),
             border_corner_bulge: default_border_corner_bulge(),
             border_corner_length: 0,
@@ -781,6 +788,7 @@ fn default_border_corner_radius() -> i64 {
 
 fn default_border_taper() -> f64 { 0.35 }
 fn default_border_handle_width() -> f64 { 32.0 }
+fn default_border_overlap_opacity() -> f64 { 0.4 }
 fn default_border_swell_curve() -> f64 { 0.45 }
 fn default_border_corner_bulge() -> f64 { 48.0 }
 
@@ -2177,6 +2185,13 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
                                             surface.border_handle_width = val as f64;
                                         }
                                     }
+                                    "overlap_opacity" => {
+                                        if let Some(val) = entry.value().as_f64() {
+                                            surface.border_overlap_opacity = val;
+                                        } else if let Some(val) = entry.value().as_i64() {
+                                            surface.border_overlap_opacity = val as f64;
+                                        }
+                                    }
                                     "swell_curve" => {
                                         if let Some(val) = entry.value().as_f64() {
                                             surface.border_swell_curve = val;
@@ -2523,6 +2538,7 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
     // which is the moulding inside out.
     state.layout.border_taper = config.surface.border_taper.clamp(0.05, 1.0) as f32;
     state.layout.border_handle_width = config.surface.border_handle_width.max(4.0) as f32;
+    state.layout.border_overlap_opacity = config.surface.border_overlap_opacity.clamp(0.0, 1.0) as f32;
     state.layout.border_swell_curve = config.surface.border_swell_curve.clamp(0.1, 6.0) as f32;
     state.layout.border_corner_bulge = config.surface.border_corner_bulge.max(0.0) as f32;
     state.layout.border_corner_length = config.surface.border_corner_length.max(0) as i32;
diff --git a/src/server/seat.rs b/src/server/seat.rs
index c9ec1af..0a2778e 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1213,6 +1213,9 @@ impl Seat {
             
             let win = op.window_ptr;
             if !win.is_null() && !(*win).closed {
+                // Every drag step can bring a Floating window over the
+                // adjust target or take it off: re-evaluate the overlap dim.
+                (*self.server).wm.arm_border_fade();
                 if (*win).tiling_mode != crate::tiling::TilingMode::Floating
                     && (*win).tiling_mode != crate::tiling::TilingMode::Overlay
                     // A drag moves a Utility window; it must not re-class it.
diff --git a/src/server/window.rs b/src/server/window.rs
index d0b8711..0e6a8a6 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -434,6 +434,13 @@ pub struct Window {
     /// `rendering_requested.border`, which the arrange pass rewrites wholesale
     /// every pass and would otherwise clobber.
     pub border_reveal: [f32; 8],
+    /// How far this window is dimmed for lying OVER the adjust target, 0.0
+    /// (full opacity) to 1.0 (`border.overlap_opacity`): a Floating window
+    /// overlapping the window whose handles are up would hide them, so it
+    /// eases down while the mode is on and back up when it ends. Stepped by
+    /// `step_adjust_dim` on the border-fade timer; applied through
+    /// `effective_opacity`.
+    pub adjust_dim: f32,
     pub decorations_above: ffi::wl_list,
     pub decorations_above_tree: *mut ffi::wlr_scene_tree,
     pub popup_tree: *mut ffi::wlr_scene_tree,
@@ -752,6 +759,7 @@ impl Window {
             hovered_border_element: None,
             border_hover_drawn: None,
             border_reveal: [0.0; 8],
+            adjust_dim: 0.0,
             decorations_above: std::mem::zeroed(),
             decorations_above_tree,
             popup_tree,
@@ -2980,7 +2988,7 @@ impl Window {
             self.update_shadow(width, height, radius, want_shadow);
                 self.update_bevel(width, height, radius, want_bevel, want_decor);
                 self.update_droplet(width, height);
-            ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
+            ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, self.effective_opacity());
 
             // Device px, like the blur radius above: the surface content is
             // scaled to its dest size, so an unscaled clip radius would keep
@@ -3715,6 +3723,85 @@ impl Window {
 
 
 
+    /// The opacity the scene tree gets: the requested one, scaled down by the
+    /// adjust-mode overlap dim (`adjust_dim`, 0..1) toward
+    /// `border.overlap_opacity`.
+    pub unsafe fn effective_opacity(&self) -> f32 {
+        let floor = (*self.server).wm.layout.border_overlap_opacity;
+        self.rendering_requested.opacity * (1.0 - self.adjust_dim.clamp(0.0, 1.0) * (1.0 - floor))
+    }
+
+    /// Whether this window should be dimmed right now: adjust mode is on,
+    /// this is a Floating window, and it lies ABOVE the adjust target in the
+    /// render stack while overlapping it on screen — where it would cover
+    /// the target's handles. Windows under the target are left alone; they
+    /// hide nothing.
+    pub unsafe fn adjust_dim_wanted(&self) -> bool {
+        let wm = &(*self.server).wm;
+        if !wm.window_adjust_active()
+            || self.closed
+            || self.tiling_mode != crate::tiling::TilingMode::Floating
+            || self.is_status_bar()
+            || self.is_wallpaper()
+            || self.is_grid()
+        {
+            return false;
+        }
+        let me = self as *const Window as *mut Window;
+        let on_screen = |w: *mut Window| -> (f64, f64, f64, f64) {
+            let sc = if (*w).scale > 0.0 { (*w).scale } else { 1.0 };
+            let g = (*w).box_geom;
+            (g.x as f64, g.y as f64, g.width as f64 * sc, g.height as f64 * sc)
+        };
+        let (mx, my, mw, mh) = on_screen(me);
+        // The render list runs bottom to top (raise_window moves to the
+        // tail), so a target met before this window sits beneath it.
+        let list = &wm.rendering_requested.list as *const ffi::wl_list as *mut WlList;
+        let mut curr = (*list).next;
+        let mut covered = false;
+        while curr != list {
+            let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
+            if let crate::wm_node::WmNodeType::Window(w) = (*node).get() {
+                if w == me {
+                    return covered;
+                }
+                if !w.is_null()
+                    && !(*w).closed
+                    && window_takes_handles(w)
+                    && (*w).is_adjust_target()
+                {
+                    let (tx, ty, tw, th) = on_screen(w);
+                    if mx < tx + tw && tx < mx + mw && my < ty + th && ty < my + mh {
+                        covered = true;
+                    }
+                }
+            }
+            curr = (*curr).next;
+        }
+        false
+    }
+
+    /// Advance the overlap dim one tick toward where `adjust_dim_wanted`
+    /// says it should rest, applying the opacity as it goes. Returns true
+    /// while still in motion, like `step_border_fade`.
+    pub unsafe fn step_adjust_dim(&mut self) -> bool {
+        let target = if self.adjust_dim_wanted() { 1.0 } else { 0.0 };
+        let delta = target - self.adjust_dim;
+        let moving;
+        if delta.abs() <= BORDER_FADE_EPSILON {
+            if self.adjust_dim == target {
+                return false;
+            }
+            self.adjust_dim = target;
+            moving = false;
+        } else {
+            self.adjust_dim += delta * BORDER_FADE_STEP;
+            moving = true;
+        }
+        ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, self.effective_opacity());
+        moving
+    }
+
     /// Advance the hover fade one tick. Every zone eases toward 1.0 if it is
     /// the one under the pointer and 0.0 otherwise. Returns true while any
     /// zone is still in motion, so the caller knows to schedule another tick.
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index d0622b9..39bd136 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3612,6 +3612,11 @@ impl WindowManager {
 
         self.update_status();
         self.rendering_scheduled.dirty = true;
+        // A window that just moved, resized or restacked may now cover the
+        // adjust target, or no longer: let the overlap dim re-evaluate.
+        if self.window_adjust_active() {
+            self.arm_border_fade();
+        }
     }
 
     pub unsafe fn update_viewport_local(&mut self) {
@@ -4268,6 +4273,10 @@ impl WindowManager {
             }
         }
         self.keep_status_bar_on_top();
+        // Restacking changes who covers the adjust target.
+        if self.window_adjust_active() {
+            self.arm_border_fade();
+        }
     }
 
     /// The overview action a POINTER-LESS toggle stands for — a key press
@@ -6856,6 +6865,9 @@ unsafe extern "C" fn handle_border_fade_tick(data: *mut std::ffi::c_void) -> std
         if (*window).step_border_fade() {
             moving = true;
         }
+        if (*window).step_adjust_dim() {
+            moving = true;
+        }
         if (*window).step_fs_anim() {
             (*window).render_finish();
             moving = true;