git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit00aa4978b1fe8f4ffaaf115ce21d6ec0d1591395
parent8ef5349ebb
authorLucas Galante <[email protected]>
date2026-07-09 14:48
fix(input): popover areas veto window drags; render-only popups take no input

Two input holes found dissolving cce-graph's root Backplate:

- point_in_active_popover: a press inside an OPEN popover's plate must
  never start a window move — popovers are drawn on top but are not
  spatial-grid widgets, and the widget beneath may not block dragging
  (Graph's edge-exclusive canvas hit test returns false on empty canvas,
  so clicking a menu item over the canvas started an interactive move
  and swallowed the click — pre-existing, backplate or not). Both
  is_movable_backplate_at and drag_allowed_at now veto there.

- The engine's render-only xdg popups get an EMPTY input region: with
  the default full region the popup surface can swallow pointer input
  over an open menu that the main surface draws and hit-tests itself.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe

 src/backend/window_runner.rs | 13 +++++++++++--
 src/context.rs               | 25 +++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 2 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 2e54a7b..b5fec94 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2509,7 +2509,6 @@ impl<A: Application> PointerHandler for EngineState<A> {
             }
             
             self.cursor_pos = (lx, ly);
-            
             match &event.kind {
                 PointerEventKind::Enter { .. } => {
                     let is_status_bar = self.inner.as_ref().unwrap().settings().app_id.starts_with("cce-status");
@@ -3336,7 +3335,17 @@ pub fn run<A: Application>() {
                     &engine_state.xdg_shell_state,
                 ).unwrap();
                 sctk_popup.wl_surface().set_buffer_scale(engine_state.scale_factor as i32);
-                    
+                // Render-only popup: give it an EMPTY input region so pointer events pass
+                // through to the main surface beneath (which draws and hit-tests the popover
+                // content itself). With the default full input region the popup swallowed
+                // every click on an open menu — item clicks never reached the app.
+                {
+                    let compositor = engine_state.compositor_state.wl_compositor();
+                    let empty_region = compositor.create_region(&engine_state.qh, ());
+                    sctk_popup.wl_surface().set_input_region(Some(&empty_region));
+                    empty_region.destroy();
+                }
+
                     let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
                     let surface_ptr = sctk_popup.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
                     let wayland_handle = Box::leak(Box::new(WaylandSurfaceHandle {
diff --git a/src/context.rs b/src/context.rs
index f168524..db0bad1 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -778,7 +778,29 @@ impl UiContext {
         crate::widget::context_menu::text_labels()
     }
 
+    /// Whether the point lies inside an OPEN popover's plate. Popovers are drawn on top of
+    /// everything and are interactive UI, but they are not spatial-grid widgets — a press
+    /// there must never start a window move (the widgets beneath may not block dragging,
+    /// e.g. Graph's edge-exclusive canvas hit test).
+    fn point_in_active_popover(&self, px: f32, py: f32) -> bool {
+        for popover_ptr in &self.active_popovers {
+            unsafe {
+                if let Some(p) = popover_ptr.as_ref() {
+                    if let Some((x, y, w, h)) = p.popover_rect() {
+                        if px >= x && px <= x + w && py >= y && py <= y + h {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        false
+    }
+
     pub fn is_movable_backplate_at(&self, px: f32, py: f32) -> bool {
+        if self.point_in_active_popover(px, py) {
+            return false;
+        }
         let mut hit_backplate = false;
         let scroll_y = crate::widget::hover_animation::get_scroll_offset();
         let mut candidate_ids = self.spatial_grid.query(px, py).to_vec();
@@ -819,6 +841,9 @@ impl UiContext {
     /// [`is_movable_backplate_at`](UiContext::is_movable_backplate_at) minus the requirement
     /// that a registered movable `Backplate` is hit.
     pub fn drag_allowed_at(&self, px: f32, py: f32) -> bool {
+        if self.point_in_active_popover(px, py) {
+            return false;
+        }
         let scroll_y = crate::widget::hover_animation::get_scroll_offset();
         let mut candidate_ids = self.spatial_grid.query(px, py).to_vec();
         if scroll_y != 0.0 {