system settings
git clone https://git.lucas.co/cce-system-interface.git
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;