git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit42a1a84418a49dcca226e872c848a500df60601d
parentc5765b25b0
authorLucas Galante <[email protected]>
date2026-07-09 16:58
feat: popovers + context menu draw INTO the frame (Phase 6t) — engine xdg popup dropped

The render_popovers override is deleted; rebuild_layout runs the same
collector into the frame's own tuple stream, appended above everything
(window, page, search bar) and kept OUT of widgets/texts so the wheel
fast-path can never scroll popover content. The app opts out of the
engine popup via draws_own_popovers.

This fixes the below-window popover: the page dropdown's open-upward
popover now renders exactly where it always hit-tested (in-window, above
the dropdown). Chrome popovers draw at their registered coords; page-
widget popovers (notifications/fonts menus) shift by -scroll_y — the
same subtraction the old popup positioner applied at creation, now
keeping display and hit-testing aligned under scroll. dl-text under the
popover is clamped via the existing ui_context registration; under the
context menu via the engine's new overlay rect.

Live-verified: dropdown popover in-window with page geometry occluded
beneath, item hover, click switches Audio -> Processes and back,
popover dismissal; spinbox right-click context menu at cursor with text
beneath occluded, dismissal on leave.

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

 src/main.rs     | 54 +++++++++++++++++-----------------------------
 src/renderer.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 34 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 0543bdb..993f320 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -38,6 +38,11 @@ struct SystemInterface {
     // display-list Text prims (scroll shift, search dim/highlight, and viewport clamps
     // already applied by rebuild_layout).
     texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
+    // Popover + context-menu content, drawn INTO the frame on top of everything (Phase 6t —
+    // no engine xdg popup). Separate from widgets/texts so the wheel fast-path never
+    // scrolls them.
+    popover_widgets: Vec<AppWidget>,
+    popover_texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
     page_buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, AppAction)>,
 
     sidebar_width: f32,
@@ -145,6 +150,8 @@ impl cce_ui::engine::Application for SystemInterface {
             font_system,
             widgets: Vec::new(),
             texts: Vec::new(),
+            popover_widgets: Vec::new(),
+            popover_texts: Vec::new(),
             page_buttons: Vec::new(),
             sidebar_width,
             header_height: 0.0,
@@ -277,7 +284,15 @@ impl cce_ui::engine::Application for SystemInterface {
         if let Some((qx, qy, qw, qh, qc)) = cce_ui::widget::hover_animation::get_quad() {
             pc.quad(Rect { x: qx, y: qy - self.scroll_y, width: qw, height: qh }, qc);
         }
-        for (text, font_size, x, y, col, font, bounds) in &self.texts {
+        for w in &self.popover_widgets {
+            let rect = Rect { x: w.x, y: w.y, width: w.w, height: w.h };
+            if w.radius > 0.1 {
+                pc.rounded_rect(rect, w.radius, w.corners, w.color);
+            } else {
+                pc.quad(rect, w.color);
+            }
+        }
+        for (text, font_size, x, y, col, font, bounds) in self.texts.iter().chain(self.popover_texts.iter()) {
             pc.text_with(
                 text.clone(),
                 *x,
@@ -303,39 +318,10 @@ impl cce_ui::engine::Application for SystemInterface {
         Some(&self.ui_context)
     }
 
-    fn render_popovers(&self, pc: &mut dyn cce_ui::layout::RenderTarget) {
-        cce_ui::layout::render_popovers(pc, &self.ui_context);
-
-        if cce_ui::widget::context_menu::is_visible() {
-            let cx = cce_ui::widget::context_menu::x();
-            let cy = cce_ui::widget::context_menu::y();
-            let cw = cce_ui::widget::context_menu::w();
-            let ch = cce_ui::widget::context_menu::h();
-
-            // Border
-            pc.rect([0.22, 0.22, 0.28, 1.0], cx, cy, cw, ch);
-            // Bg
-            pc.rect([0.06, 0.06, 0.09, 1.0], cx + 1.0, cy + 1.0, cw - 2.0, ch - 2.0);
-
-            // Hover highlight
-            if let Some(h_idx) = cce_ui::widget::context_menu::hovered_item() {
-                let iy = cy + h_idx as f32 * 24.0;
-                pc.rect([0.20, 0.40, 0.65, 0.6], cx + 2.0, iy + 2.0, cw - 4.0, 20.0);
-            }
-
-            // Texts
-            for (idx, opt) in cce_ui::widget::context_menu::options().iter().enumerate() {
-                let iy = cy + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
-                let text_color = if idx == 0 {
-                    [0.44, 0.44, 0.47, 1.0]
-                } else if cce_ui::widget::context_menu::hovered_item() == Some(idx) {
-                    [1.0, 1.0, 1.0, 1.0]
-                } else {
-                    [0.80, 0.80, 0.83, 1.0]
-                };
-                pc.text_with_bounds(opt, cx + 8.0, iy, 12.0, text_color, Some([cx, cy, cx + cw, cy + ch]));
-            }
-        }
+    fn draws_own_popovers(&self) -> bool {
+        // Popovers + context menu draw into the display list (rebuild_layout) — the engine
+        // must not spawn its render-only xdg popup.
+        true
     }
 
     fn clear_color(&self) -> [f32; 4] {
diff --git a/src/renderer.rs b/src/renderer.rs
index 5875eda..56a34c5 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -593,6 +593,72 @@ impl SystemInterface {
             texts.push((t.clone(), *size, *x, *y, *tc, font_opt.clone(), *bounds));
         }
 
+        // Popovers + context menu draw INTO the frame (Phase 6t): the same collector the
+        // engine's xdg popup used, but emitted on top of the whole window — the dropdown's
+        // open-upward popover finally renders where it hit-tests. Kept out of
+        // widgets/texts so the wheel fast-path can't scroll them; dl-text occlusion comes
+        // from the ui_context popover registration (and the engine's context-menu overlay
+        // rect under draws_own_popovers).
+        let mut popover_pc = PageContent::new();
+        {
+            // Chrome popover (the page dropdown) is in window coords; page-widget popovers
+            // (notifications/fonts menus) are in page coords and shift with the viewport —
+            // the same scroll subtraction the old popup positioner applied at creation.
+            let chrome_ptr = self.page_dropdown.as_ptr();
+            let mut page_pop_pc = PageContent::new();
+            for popover_ptr in &self.ui_context.active_popovers {
+                unsafe {
+                    if std::ptr::addr_eq(*popover_ptr, chrome_ptr) {
+                        (**popover_ptr).render_popover(&mut popover_pc);
+                    } else {
+                        (**popover_ptr).render_popover(&mut page_pop_pc);
+                    }
+                }
+            }
+            for (c, x, y, w, h, r, corners) in page_pop_pc.rects {
+                popover_pc.rects.push((c, x, y - self.scroll_y, w, h, r, corners));
+            }
+            for (t, size, x, y, tc, font, bounds) in page_pop_pc.texts {
+                let shifted = bounds.map(|[l, tb, rr, b]| [l, tb - self.scroll_y, rr, b - self.scroll_y]);
+                popover_pc.texts.push((t, size, x, y - self.scroll_y, tc, font, shifted));
+            }
+        }
+        if cce_ui::widget::context_menu::is_visible() {
+            use cce_ui::layout::RenderTarget;
+            let cx = cce_ui::widget::context_menu::x();
+            let cy = cce_ui::widget::context_menu::y();
+            let cw = cce_ui::widget::context_menu::w();
+            let ch = cce_ui::widget::context_menu::h();
+
+            popover_pc.rect([0.22, 0.22, 0.28, 1.0], cx, cy, cw, ch);
+            popover_pc.rect([0.06, 0.06, 0.09, 1.0], cx + 1.0, cy + 1.0, cw - 2.0, ch - 2.0);
+
+            if let Some(h_idx) = cce_ui::widget::context_menu::hovered_item() {
+                let iy = cy + h_idx as f32 * 24.0;
+                popover_pc.rect([0.20, 0.40, 0.65, 0.6], cx + 2.0, iy + 2.0, cw - 4.0, 20.0);
+            }
+
+            for (idx, opt) in cce_ui::widget::context_menu::options().iter().enumerate() {
+                let iy = cy + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
+                let text_color = if idx == 0 {
+                    [0.44, 0.44, 0.47, 1.0]
+                } else if cce_ui::widget::context_menu::hovered_item() == Some(idx) {
+                    [1.0, 1.0, 1.0, 1.0]
+                } else {
+                    [0.80, 0.80, 0.83, 1.0]
+                };
+                popover_pc.text_with_bounds(opt, cx + 8.0, iy, 12.0, text_color, Some([cx, cy, cx + cw, cy + ch]));
+            }
+        }
+        self.popover_widgets = popover_pc.rects.iter().map(|(c, x, y, w, h, r, corners)| AppWidget {
+            x: *x, y: *y, w: *w, h: *h,
+            color: *c, hover_color: *c,
+            hovering: false,
+            radius: *r,
+            corners: *corners,
+        }).collect();
+        self.popover_texts = popover_pc.texts;
+
         self.widgets = widgets;
         self.texts = texts;
         self.page_buttons = page_buttons;