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

commit03348e1bfe374c384598b1a4248e371d9a3786dd
parent6a0039b7ff
authorLucas Galante <[email protected]>
date2026-09-17 12:06
feat(input): holding Super is window-adjust mode at zoom 1

Overview's window handling — the frame (handles) on the focused
window, focus following the pointer so the frame lands on whatever is
hovered, and a press on the body grabbing the window to move it — is
now also active while Super is held, without leaving zoom 1. One
predicate, `WindowManager::window_adjust_active()` (overview OR
`adjust_held`), gates every site that used to test for overview: the
ring hit test, the reveal fade, the catcher rects, hover-to-focus and
the body grab. A background press with Super held stays an ordinary
desktop press; only overview exits on it.

`adjust_held` is re-read from the seat keyboard's modifier mask on
every modifiers event (`refresh_adjust_held`), which on a change arms
the frame fade and re-runs the pointer passthrough in place, so the
ring appears under a still pointer the moment the key goes down and
the app gets its hover back when it comes up; a drag in progress is
left to end on its release. `ccectl key-down 125` holds it in a shadow
(injection bypasses the device mask, so the WM keeps its own flag).

Verified headless: Super down over a floating window's body shows its
ring and hover moves focus with the pointer; press-drag-release moves
the window by exactly the pointer travel; a background click with
Super held changes nothing; Super up clears the ring. Overview's own
body drag still moves the window.

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

 CLAUDE.md                    | 10 ++++++++
 src/server/cursor.rs         | 25 ++++++++++++-------
 src/server/keyboard.rs       |  1 +
 src/server/window.rs         |  6 ++---
 src/server/window_manager.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 88 insertions(+), 12 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index f144760..ed280b7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -426,6 +426,16 @@ where the old band sat outside them.
   this arrangement exists to prevent. Note the *grab* zone stays the full
   even band (the four catcher rects) even where the ring is drawn thin: the
   swell is ornament, and a corner you can see but not grab would be worse.
+- **Holding Super is window-adjust mode at zoom 1**: the same handles,
+  hover-to-focus and body-drag as overview, gated by one predicate,
+  `WindowManager::window_adjust_active()` (overview OR `adjust_held`).
+  `adjust_held` is refreshed from the keyboard's modifier mask on every
+  modifiers event (`refresh_adjust_held`), which also re-runs the pointer
+  passthrough so the ring lands under a still pointer on key-down and the
+  app gets its hover back on key-up. `ccectl key-down 125` holds it in a
+  shadow (injection bypasses the device mask, so it keeps its own flag).
+  A background press with Super held is an ordinary desktop press — only
+  overview exits on it.
 - Handles are shown on the **focused window only**, for as long as overview
   is on (`step_border_fade`'s `all_on` branch, gated on
   `Window::is_seat_focused`). **Focus follows the pointer in overview**: the
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 8df3b9b..00029c8 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -874,11 +874,12 @@ impl Cursor {
 
             if is_window
                 && !hovered_chrome
-                && (*server).wm.mode == crate::window_manager::WindowManagerMode::Overview
+                && (*server).wm.window_adjust_active()
             {
-                // Focus follows the pointer in overview: the ring is drawn on
-                // the focused window only, so hovering is how it moves between
-                // windows without a click. Guarded on an actual change —
+                // Focus follows the pointer in overview — and while Super is
+                // held, which is the same adjust mode at zoom 1: the ring is
+                // drawn on the focused window only, so hovering is how it
+                // moves between windows without a click. Guarded on an actual change —
                 // seat.focus raises a Floating window BEFORE its same-focus
                 // short-circuit, so an unguarded call would raise and relayout
                 // on every motion event. And with the pan suppressed: hovering
@@ -1444,8 +1445,13 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
             }
         }
 
-        // --- ZOOMED OUT CLICK HANDLING ---
-        if (*event).button == 0x110 && (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
+        // --- ADJUST-MODE CLICK HANDLING (overview, or Super held) ---
+        // A press on a window's body grabs the whole window to move it; a
+        // press on its ring falls through to the border path. Only in
+        // overview does a background press mean anything (it exits); with
+        // Super held at zoom 1 it falls through to the normal desktop press.
+        let in_overview = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview;
+        if (*event).button == 0x110 && (*(*seat).server).wm.window_adjust_active() {
             let mut clicked_win: *mut crate::window::Window = std::ptr::null_mut();
             let mut clicked_cloud_layer = false;
             if let Some(result) = (*server).scene.at(lx, ly) {
@@ -1520,13 +1526,13 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
                     start_pan_y: (*server).wm.desk_pan_y,
                     start_tiling_mode: (*clicked_win).tiling_mode,
                     start_mode_locked: (*clicked_win).mode_locked,
-                    started_in_overview: true,
+                    started_in_overview: in_overview,
                 });
                 cursor.op_start_pointer();
                 cursor.pressed.insert((*event).button, None);
                 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
                 return;
-            } else if !overview_win_valid {
+            } else if !overview_win_valid && in_overview {
                 // Click-away with a cce-cloud popup open (desktop context
                 // menu, launcher): the press dismisses the popup and does
                 // nothing else — overview stays up. Dropping keyboard focus
@@ -3930,7 +3936,8 @@ pub unsafe fn grid_node_info(
 /// `window.rs`'s `draw_borders` draws the handles from the same band width
 /// and corner length, so the zones and the visuals cannot drift.
 pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f64) -> BorderZone {
-    if (*(*window).server).wm.mode != crate::window_manager::WindowManagerMode::Overview {
+    // Overview, or Super held (window-adjust mode): the same ring either way.
+    if !(*(*window).server).wm.window_adjust_active() {
         return BorderZone::None;
     }
     if !crate::window::window_takes_handles(window) {
diff --git a/src/server/keyboard.rs b/src/server/keyboard.rs
index 68c79b3..67b56d0 100644
--- a/src/server/keyboard.rs
+++ b/src/server/keyboard.rs
@@ -311,6 +311,7 @@ unsafe extern "C" fn handle_modifiers(listener: *mut ffi::wl_listener, _data: *m
         let seat = (*keyboard.group).seat;
         if !seat.is_null() && !(*seat).server.is_null() {
             (*(*seat).server).wm.update_status();
+            (*(*seat).server).wm.refresh_adjust_held();
         }
     }
 }
diff --git a/src/server/window.rs b/src/server/window.rs
index 44a70d2..20445f1 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -3673,7 +3673,7 @@ impl Window {
         // window, each swap easing through this same fade. Hover still reads
         // through on the focused ring, as `color_for` paints the hovered
         // zone in hover_color over the full reveal.
-        let all_on = (*self.server).wm.mode == crate::window_manager::WindowManagerMode::Overview
+        let all_on = (*self.server).wm.window_adjust_active()
             && window_takes_handles(self as *mut Window)
             && self.is_seat_focused();
         for elem in BorderElement::ALL {
@@ -4025,8 +4025,8 @@ impl Window {
             // segments are both disabled outright. The window's own border
             // (`window_background`, above) is untouched in either mode: this
             // moved the HANDLES inward, not the border.
-            let in_overview = (*self.server).wm.mode
-                == crate::window_manager::WindowManagerMode::Overview;
+            // Overview, or Super held: the same adjust mode at any zoom.
+            let in_overview = (*self.server).wm.window_adjust_active();
             let bw = band;
             let layout_handle_w = (*self.server).wm.layout.border_handle_width;
             let sc = if self.scale > 0.0 { self.scale } else { 1.0 };
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 6eb6448..ec5f3f2 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -192,6 +192,14 @@ pub struct WindowManager {
     pub status_backdrops: std::cell::RefCell<Vec<(String, u8, u8)>>,
     pub status_hide_mode: bool,
     pub adjust_position_mode: bool,
+    /// Window-adjust mode: Super is held. The focused window shows its
+    /// frame (handles) and its body drags it, as in overview — the two
+    /// are one predicate, `window_adjust_active`. Set from the keyboard's
+    /// modifier state (`refresh_adjust_held`), never assigned directly.
+    pub adjust_held: bool,
+    /// A Super held through `ccectl key-down 125|126`, which bypasses the
+    /// keyboard device the modifier mask is read from.
+    pub injected_super_held: bool,
     /// xkb modifier mask currently held via injected `key-down` (see the ipc handler):
     /// OR'd over the device state on every synthetic modifiers notify so clients see
     /// ctrl/shift/alt/super combos from injection like they would from hardware.
@@ -587,6 +595,8 @@ impl WindowManager {
         self.status_backdrops = std::cell::RefCell::new(Vec::new());
         self.status_hide_mode = false;
         self.adjust_position_mode = false;
+        self.adjust_held = false;
+        self.injected_super_held = false;
         self.injected_key_mods = 0;
         let _ = std::fs::remove_file("/tmp/cce-status-interface-adjust-mode");
 
@@ -2147,6 +2157,48 @@ impl WindowManager {
         self.arm_border_fade();
     }
 
+    /// Overview, or Super held: the focused window shows its frame and
+    /// its body drags it. Every site that gates the handles — the hit
+    /// test, the reveal, the catcher rects, hover-to-focus, the body
+    /// grab — asks this, so the two ways in cannot drift apart.
+    pub fn window_adjust_active(&self) -> bool {
+        self.mode == WindowManagerMode::Overview || self.adjust_held
+    }
+
+    /// Re-read whether Super is held (the seat keyboard's live mask, or an
+    /// injected one) and, on a change, bring the desk into or out of
+    /// adjust mode: fade the frame in/out and re-evaluate the pointer in
+    /// place, so the ring lands on the window under a still pointer the
+    /// moment the key goes down and the app gets its hover back when it
+    /// comes up. A drag in progress is left alone — it ends on release.
+    pub unsafe fn refresh_adjust_held(&mut self) {
+        let mut held = self.injected_super_held;
+        let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr_seat = (*seats_list).next;
+        while curr_seat != seats_list {
+            let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
+            let kb = ffi::river_wlr_seat_get_keyboard((*seat).wlr_seat);
+            if !kb.is_null() && (ffi::wlr_keyboard_get_modifiers(kb) & ffi::wlr_keyboard_modifier_WLR_MODIFIER_LOGO) != 0 {
+                held = true;
+            }
+            curr_seat = (*curr_seat).next;
+        }
+        if held == self.adjust_held {
+            return;
+        }
+        self.adjust_held = held;
+        self.arm_border_fade();
+        let now = crate::util::msec_timestamp();
+        let mut curr_seat = (*seats_list).next;
+        while curr_seat != seats_list {
+            let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
+            if (*seat).op.is_none() {
+                (*seat).cursor.passthrough(now);
+            }
+            curr_seat = (*curr_seat).next;
+        }
+    }
+
     pub unsafe fn arm_border_fade(&mut self) {
         if self.border_fade_running || self.border_fade_timer.is_null() {
             return;
@@ -5720,6 +5772,12 @@ impl WindowManager {
                         }
                         curr_seat = next_seat;
                     }
+                    // The compositor's own Super state (window-adjust mode)
+                    // reads the keyboard device, which injection bypasses.
+                    if matches!(keycode, 125 | 126) {
+                        self.injected_super_held = pressed;
+                        self.refresh_adjust_held();
+                    }
                     "ok\n".to_string()
                 } else {
                     "error: invalid keycode\n".to_string()