Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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;