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

commit53960abd35e69fa93f784173d97e9ef3c17842c8
parent5d4cafd020
authorLucas Galante <[email protected]>
date2026-08-20 15:43
UiContext::popover_owner_at — press priority for open popovers

Positional dispatch knows nothing about z-order: an app iterating its
roots can hand a press to a CLOSED sibling whose trigger band sits under
an open menu, which then swallows the click and the menu closes
unselected. Found on the settings Default Apps page — the Terminal
dropdown's popover covers the Images row's trigger, and the roots
iterate bottom-up — but latent for any page whose popover overlaps a
later-dispatched widget.

popover_owner_at(x, y) returns the registered widget whose OPEN popover
(popover_rect, the drawn animated surface) contains the point — the
press-priority companion to close_popovers_missed_by_press, scanning
the registry the same way since popover registration is optional and
spotty across apps. Apps route MouseButton events to this owner before
their positional dispatch.

Also: CCE_DD_DEBUG=1 traces dropdown press geometry (rect/popover/
inside flags) and key-selection — the probe that found the bug.

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

 src/context.rs               | 23 +++++++++++++++++++++++
 src/widget/input/dropdown.rs |  9 +++++++++
 2 files changed, 32 insertions(+)

diff --git a/src/context.rs b/src/context.rs
index bcde4e5..126a76d 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -635,6 +635,29 @@ impl UiContext {
         }
     }
 
+    /// The registered widget whose OPEN popover contains `(x, y)`, if any — the
+    /// press-priority companion to
+    /// [`close_popovers_missed_by_press`](Self::close_popovers_missed_by_press).
+    /// A popover paints OVER whatever sits beneath it, but positional dispatch
+    /// knows nothing about z-order: an app iterating its roots can hand the
+    /// press to a closed sibling whose trigger band lies under the open menu
+    /// (the Default Apps page's Terminal dropdown covering the Images row).
+    /// Apps route a `MouseButton` to this owner before their positional
+    /// dispatch. Scans the registry like the missed-press walk — popover
+    /// registration is optional and spotty, so `active_popovers` alone cannot
+    /// be trusted to know about every open menu.
+    pub fn popover_owner_at(&self, x: f32, y: f32) -> Option<WidgetId> {
+        self.tree.iter_registered().find_map(|(id, ptr)| unsafe {
+            ptr.as_ref().and_then(|w| {
+                if !w.visible() {
+                    return None;
+                }
+                let (rx, ry, rw, rh) = w.popover_rect()?;
+                (x >= rx && x <= rx + rw && y >= ry && y <= ry + rh).then_some(id)
+            })
+        })
+    }
+
     /// Register an open popover. Takes `&mut` so the registry can be refreshed with the
     /// pointer we are handed (the occlusion walks resolve the stored id through the tree).
     pub fn register_popover(&mut self, w: &mut (dyn WidgetHost + 'static)) {
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 3a66c99..cb9c096 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -801,6 +801,9 @@ impl Dropdown {
                 true
             }
             Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
+                if std::env::var_os("CCE_DD_DEBUG").is_some() {
+                    eprintln!("[dd] key-select hovered={:?} selected={}", self.hovered_item, self.selected);
+                }
                 if let Some(idx) = self.hovered_item {
                     if idx < self.options.len() && self.options[idx] != "-" {
                         if self.selected != idx || self.custom_display_text.is_some() {
@@ -1121,6 +1124,12 @@ impl Input for Dropdown {
                     && !self.closing
                     && *px >= rx && *px <= rx + rw && *py >= ry && *py <= ry + rh;
 
+                if std::env::var_os("CCE_DD_DEBUG").is_some() {
+                    eprintln!(
+                        "[dd] press ({px},{py}) rect=({:.0},{:.0},{:.0},{:.0}) popover=({rx:.0},{ry:.0},{rw:.0},{rh:.0}) open={} in_trig={inside_trigger} in_pop={inside_popover}",
+                        content.x, content.y, content.width, content.height, self.open
+                    );
+                }
                 if inside_popover {
                     let idx = ((py - ry) / 24.0) as usize;
                     if idx < self.options.len() {