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