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

commitf2d8f4d47977e33a2f6f721f13ebfc09bf94e9c0
parent0c22103e6a
authorLucas Galante <[email protected]>
date2026-07-09 16:57
feat(engine): Application::draws_own_popovers — opt out of the render-only xdg popup

Phase 6 apps that draw popovers and the context menu INTO their display
list (registered in ui_context for the occlusion clamp) opt out of the
engine's render-only popup surface entirely — both spawn triggers (the
global popover registry and the global context-menu visibility) are
gated. The popup path also anchored at the widget's bottom edge
regardless of an open-upward popover rect, which is why settings' page
popover displayed below the window; drawing in-list fixes display and
hit-testing coherence at the source.

Under the flag, render() adds the visible context menu's rect to the
dl-text occlusion overlays (the menu is engine-global state, not a
ui_context popover); the menu's own labels are exempt because they carry
bounds equal to the rect. Default false: data-editor and text-editor
keep the popup path until their turns.

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

 src/backend/window_runner.rs | 27 +++++++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b5fec94..595c2fa 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1667,6 +1667,15 @@ pub trait Application: Sized + 'static {
     fn load_system_fonts(&self) -> bool {
         false
     }
+
+    /// Phase 6 opt-in: this app draws its popovers and context menu INTO its display list
+    /// (registered in `ui_context` for the occlusion clamp) — the engine must NOT spawn its
+    /// render-only xdg popup for globally-registered popovers or the global context menu.
+    /// Default `false`: unmigrated apps keep the popup surface + `render_popovers` collector
+    /// path. The popup path (and this flag) go away once its last consumer is across.
+    fn draws_own_popovers(&self) -> bool {
+        false
+    }
 }
 
 pub struct PressedKey {
@@ -1999,6 +2008,17 @@ impl<A: Application> EngineState<A> {
                 }
             }
         }
+        // An app drawing its own context menu into the list (draws_own_popovers) gets the
+        // same occlusion for it: the menu rect clamps list text beneath, and the menu's own
+        // labels are exempt because they carry bounds equal to the rect.
+        if self.inner.as_ref().unwrap().draws_own_popovers() && crate::widget::context_menu::is_visible() {
+            dl_overlay_rects.push((
+                crate::widget::context_menu::x(),
+                crate::widget::context_menu::y(),
+                crate::widget::context_menu::w(),
+                crate::widget::context_menu::h(),
+            ));
+        }
         for ti in &self.dl_text_items {
             let mut item_bounds = if let Some([l, t, r, b]) = ti.bounds {
                 TextBounds {
@@ -3270,16 +3290,19 @@ pub fn run<A: Application>() {
             last_title = current_title;
         }
 
+        // Apps that draw popovers/context menu into their own display list (Phase 6) opt out
+        // of the render-only popup surface entirely.
+        let draws_own = engine_state.inner.as_ref().map_or(false, |a| a.draws_own_popovers());
         let active_popovers = crate::widget::popovers::get_active();
         let context_menu_visible = crate::widget::context_menu::is_visible();
 
         let mut active_popover_rect = None;
-        if !active_popovers.is_empty() {
+        if !draws_own && !active_popovers.is_empty() {
             let popover_widget = unsafe { &*active_popovers[0] };
             active_popover_rect = popover_widget.popover_rect();
         }
 
-        if active_popover_rect.is_some() || context_menu_visible {
+        if active_popover_rect.is_some() || (context_menu_visible && !draws_own) {
             let (px, py, mut pw, mut ph, is_context_menu) = if let Some((x, y, w, h)) = active_popover_rect {
                 (x, y, w.max(1.0), h.max(1.0), false)
             } else {