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

commita0149825652ff83b38254de58ddf3ab9b84dd6e2
parent081eed5f72
authorLucas Galante <[email protected]>
date2026-09-18 19:55
fix(borders): in overview the ring is on the hovered window only

`Window::is_adjust_target` keys on the pointer's hover target in
overview too, not on focus: a pointer on the background shows no ring,
and the ring follows the pointer from window to window exactly as it
does with Super held. Overview's hover-to-focus is unchanged — focus
still follows the pointer there — it just no longer decides where the
handles are drawn.

Verified in a shadow with two windows and a desktop image: background →
no ring; over a window → its ring only; back to the background → none.

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

 CLAUDE.md            | 15 ++++++++-------
 src/server/cursor.rs | 39 ++++++++++++++++++---------------------
 src/server/window.rs | 30 ++++++++++++++----------------
 3 files changed, 40 insertions(+), 44 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9253a9b..adf15b9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -417,10 +417,10 @@ sat outside the edges and the ring that followed hugged them.
   body-drag as overview, gated by one predicate,
   `WindowManager::window_adjust_active()` (overview OR `adjust_held`).
   But NOT hover-to-focus: the ring lands on the window **under the
-  pointer** (`Cursor::adjust_hover`, set by `passthrough`), focused or not,
-  and focus stays put — so pressing Super arms whatever the pointer is
-  already on, and a focus chord pressed next acts on the window the user
-  had. **A drag never focuses the window it moves or resizes** (the grab
+  pointer** (`Cursor::adjust_hover`, set by `passthrough` — the same
+  target overview uses), focused or not, and focus stays put — so pressing
+  Super arms whatever the pointer is already on, and a focus chord pressed
+  next acts on the window the user had. **A drag never focuses the window it moves or resizes** (the grab
   paths in `handle_button` call no `seat.focus`; `op_start_pointer` raises
   a Floating one instead); a tap on the band or body — press+release
   without motion — is a click and focuses in `op_end`.
@@ -432,10 +432,11 @@ sat outside the edges and the ring that followed hugged them.
   A background press with Super held is an ordinary desktop press — only
   overview exits on it.
 - Handles are shown on the **adjust target only** — `Window::is_adjust_target`:
-  the focused window in overview, the hovered one with Super held — for as
+  the window under the pointer (`Cursor::adjust_hover`), in overview and
+  with Super held alike; a pointer on the background shows none — for as
   long as the mode is on (`step_border_fade`'s `all_on` branch, `draw_borders`'
-  `handles_live`, and `get_border_zone` all ask it). **Focus follows the
-  pointer in overview**: the
+  `handles_live`, and `get_border_zone` all ask it). Separately, **focus
+  follows the pointer in overview** (the ring does not key on it): the
   motion path focuses the hovered toplevel — guarded on an actual change,
   because `seat.focus` raises a Floating window *before* its same-focus
   short-circuit, so an unguarded call would raise and relayout on every
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 492444e..21a5d60 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -123,13 +123,12 @@ pub struct Cursor {
     pub hovered_border_window: *mut crate::window::Window,
     /// Which of that window's 8 border zones is highlighted.
     pub hovered_border_element: Option<crate::window::BorderElement>,
-    /// The toplevel under the pointer while Super-held adjust mode is on:
-    /// the window the ring lands on and whose handles are live, focused or
-    /// not. Set by `passthrough` on every hover evaluation and cleared when
-    /// the pointer rests on nothing adjustable or the mode is off. Overview
-    /// keys the ring on focus instead — see `Window::is_adjust_target`. May
-    /// dangle after a close: compared by address only, and nulled in
-    /// `Window::destroy`.
+    /// The toplevel under the pointer while adjust mode (overview, or Super
+    /// held) is on: the window the ring lands on and whose handles are live,
+    /// focused or not. Set by `passthrough` on every hover evaluation and
+    /// cleared when the pointer rests on nothing adjustable or the mode is
+    /// off — see `Window::is_adjust_target`. May dangle after a close:
+    /// compared by address only, and nulled in `Window::destroy`.
     pub adjust_hover: *mut crate::window::Window,
     pub right_click_on_bg: bool,
     pub right_click_on_border: bool,
@@ -865,11 +864,10 @@ impl Cursor {
                         is_window = true;
                         hovered_toplevel = window;
                     }
-                    // Super held at zoom 1: the ring lands on the window
-                    // under the pointer, focused or not. Set BEFORE the zone
-                    // test below, so the band is live on the first hover.
-                    // (Null for the status bar, wallpaper and grid; overview
-                    // keys the ring on focus and ignores this.)
+                    // Adjust mode: the ring lands on the window under the
+                    // pointer, focused or not. Set BEFORE the zone test
+                    // below, so the band is live on the first hover. (Null
+                    // for the status bar, wallpaper and grid.)
                     self.set_adjust_hover(if (*server).wm.window_adjust_active() {
                         hovered_toplevel
                     } else {
@@ -928,9 +926,9 @@ impl Cursor {
                 && !hovered_chrome
                 && (*server).wm.window_adjust_active()
             {
-                // Focus follows the pointer in overview: there the ring is
-                // drawn on the focused window only, so hovering is how it
-                // moves between windows without a click. (Super-held adjust mode takes this
+                // Focus follows the pointer in overview, so a click-less
+                // hover chooses the window a focus chord or the exit lands
+                // on. (The ring itself keys on `adjust_hover`, not focus.) (Super-held adjust mode takes this
                 // branch too, for the pointer-focus clear below, but not the
                 // refocus.) Guarded on an actual change —
                 // seat.focus raises a Floating window BEFORE its same-focus
@@ -4010,12 +4008,11 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
     if !crate::window::window_takes_handles(window) {
         return BorderZone::None;
     }
-    // The adjust target only, matching what draw_borders draws: the focused
-    // window in overview, the hovered one with Super held. A window showing
-    // no ring has no band, and a grab that is not drawn is the failure mode
-    // this file keeps warning about. Either way the pointer reaches a
-    // window's edge through its body — hover-to-focus in overview, the hover
-    // target at zoom 1 — so the band is live by the time it arrives.
+    // The adjust target only — the window under the pointer — matching what
+    // draw_borders draws. A window showing no ring has no band, and a grab
+    // that is not drawn is the failure mode this file keeps warning about.
+    // The pointer reaches a window's edge through its body, so the band is
+    // live by the time it arrives.
     if !(*window).is_adjust_target() {
         return BorderZone::None;
     }
diff --git a/src/server/window.rs b/src/server/window.rs
index 0e6a8a6..325bce9 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -3570,19 +3570,18 @@ impl Window {
         false
     }
 
-    /// Whether this is the window the adjust-mode ring and handles belong
-    /// to. In overview that is the focused window (hover-to-focus moves it
-    /// from window to window); with Super held at zoom 1 it is the toplevel
-    /// under some seat's pointer (`Cursor::adjust_hover`), focused or not —
-    /// pressing Super arms the window the pointer is already on, and a drag
-    /// on it never changes focus. The reveal (`step_border_fade`), the drawn
-    /// handles and catchers (`draw_borders`) and the hit test
+    /// Whether this is the window the adjust-mode handles belong to: the
+    /// toplevel under some seat's pointer (`Cursor::adjust_hover`), focused
+    /// or not, in overview and with Super held alike — the handles are
+    /// shown on what the pointer is over and on nothing else, so a pointer
+    /// on the background shows none. (Overview's hover-to-focus still moves
+    /// focus with the pointer, but focus is not what the ring keys on: a
+    /// pointer resting on the background would otherwise keep the last
+    /// window's ring up.) The reveal (`step_border_fade`), the drawn handles
+    /// and catchers (`draw_borders`) and the hit test
     /// (`cursor::get_border_zone`) all ask this one predicate, so the ring
     /// cannot be drawn on one window and grabbed on another.
     pub unsafe fn is_adjust_target(&self) -> bool {
-        if (*self.server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
-            return self.is_seat_focused();
-        }
         let me = self as *const Window as *mut Window;
         let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
         let mut curr = (*seats).next;
@@ -3808,11 +3807,10 @@ impl Window {
     pub unsafe fn step_border_fade(&mut self) -> bool {
         let mut moving = false;
         let mut changed = false;
-        // The adjust TARGET shows its whole ring for as long as the mode is
-        // on — the focused window in overview, the hovered one with Super
-        // held; other windows show nothing. Either way the ring follows the
-        // pointer from window to window, each swap easing through this same
-        // fade. Hover still reads through on the revealed ring, as
+        // The adjust TARGET — the window under the pointer — shows its whole
+        // ring for as long as the mode is on; other windows show nothing.
+        // The ring follows the pointer from window to window, each swap
+        // easing through this same fade. Hover still reads through on the revealed ring, as
         // `color_for` paints the hovered zone in hover_color over the full
         // reveal.
         let all_on = (*self.server).wm.window_adjust_active()
@@ -4174,7 +4172,7 @@ impl Window {
             // A window thinner than two bands has no interior left for a
             // ring; drawing one would be a solid block over the whole window.
             // Live handles: the mode is on and this is the adjust target
-            // (focused in overview, hovered with Super held). Target-only,
+            // (the window under the pointer). Target-only,
             // like the reveal in step_border_fade: without this the
             // invisible catcher rects would keep intercepting scene hits on
             // windows whose ring is not even drawn.