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

commitd1212969ded95f5121f6739d8c52202230cd7c85
parent22ccfd37e3
authorLucas Galante <[email protected]>
date2026-09-04 12:23
fix(overview): deliver pointer motion and wheel to chrome, not the desktop

With the launcher open in overview, passthrough treated its Popup window
like any world thumbnail — cleared pointer focus and returned — so the
launcher never saw an enter, and handle_axis panned the desktop on every
wheel event regardless of what was under the pointer. Presses already fell
through to normal delivery for chrome; motion and axis now do the same:
Popup/Overlay windows and cce-cloud layer surfaces take enter/motion and
their own wheel, while world windows keep the hover ring and desktop pan.

Verified headless: wheel over the launcher no longer moves the camera,
wheel over a world window still pans, and a click on a launcher row in
overview selects and spawns it.

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

 src/server/cursor.rs | 45 ++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 40 insertions(+), 5 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index c57ee0a..36a9958 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -821,7 +821,22 @@ impl Cursor {
             }
             self.set_border_hover(std::ptr::null_mut(), None);
 
-            if is_window && (*server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
+            // Chrome — a Popup (the cce-cloud launcher) or an Overlay dock —
+            // is live UI during overview, not a spatial thumbnail: the button
+            // path already lets its presses through to the app, and hover has
+            // to reach it the same way or its rows never highlight and a
+            // press lands on a surface that never saw an enter. So it skips
+            // the overview branch below and takes normal delivery.
+            let hovered_chrome = !hovered_toplevel.is_null()
+                && matches!(
+                    (*hovered_toplevel).tiling_mode,
+                    crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
+                );
+
+            if is_window
+                && !hovered_chrome
+                && (*server).wm.mode == crate::window_manager::WindowManagerMode::Overview
+            {
                 // 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 —
@@ -2019,21 +2034,41 @@ unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std
         return;
     }
 
-    let is_on_background = {
+    let (is_on_background, over_chrome) = {
         let lx = cursor.x();
         let ly = cursor.y();
         let server = seat.server;
         let mut over_interactive = false;
+        // Chrome under the pointer — a Popup (the cce-cloud launcher) or an
+        // Overlay dock, or a cce-cloud layer surface (context menu). Live UI
+        // during overview, same as in the button and motion paths: a wheel
+        // over the launcher's list scrolls the list, not the desktop.
+        let mut over_chrome = false;
         if let Some(result) = (*server).scene.at(lx, ly) {
             match result.data {
-                SceneNodeDataVal::Window(_) | SceneNodeDataVal::LayerSurface(_) | SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
+                SceneNodeDataVal::Window(window) => {
+                    over_interactive = true;
+                    over_chrome = !window.is_null()
+                        && matches!(
+                            (*window).tiling_mode,
+                            crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
+                        );
+                }
+                SceneNodeDataVal::LayerSurface(layer_surface) => {
+                    over_interactive = true;
+                    over_chrome = is_cloud_layer(layer_surface);
+                }
+                SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
                     over_interactive = true;
                 }
             }
         }
-        !over_interactive
+        (!over_interactive, over_chrome)
     };
-    let is_overview = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview;
+    // Overview pans on any scroll — except over chrome, which takes the
+    // event itself.
+    let is_overview = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview
+        && !over_chrome;
 
     let is_finger = (*event).source == ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_FINGER
         || (*event).source == ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_CONTINUOUS;