graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: popovers draw over widget labels in the display list
The display list draws strictly in order, and popover backgrounds were
emitted in the geometry pass — under the label text pass — so the labels
of buttons beneath the params pane's open dropdown bled through it.
Popovers (background, then option text) now append after
append_frame_text via append_popovers. Regression-tested by opening the
Main node's dropdown programmatically and asserting item order.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/main.rs | 43 +++++++++++++++++++++++++++++++++++++++++++
src/render.rs | 24 ++++++++++++++----------
2 files changed, 57 insertions(+), 10 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 1656190..c42afbb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -741,6 +741,49 @@ mod tests {
assert_eq!(mgr.match_action(&mods_none, &key_tab), None);
}
+ #[test]
+ fn test_popover_draws_over_widget_labels() {
+ // Regression: the display list draws strictly in order, so an open
+ // dropdown's popover (background AND option text) must be appended
+ // after the widget-label text pass — popover rects emitted in the
+ // geometry pass sat under every label, and the labels of buttons
+ // beneath the params pane's "Open" dropdown bled through it.
+ let mut state = State::new(false);
+ state.ensure_menubar_subnets();
+ let main_idx = state.fs_root.children.iter().position(|c| c.name == "Main").expect("Main node");
+ state.graph_mut().set_selected_node(Some(main_idx));
+ state.sync_parameters_pane();
+
+ {
+ let dropdown = state
+ .slots
+ .param
+ .inner_mut()
+ .choices
+ .iter_mut()
+ .flatten()
+ .next()
+ .expect("Main's params include a dropdown (Open)");
+ dropdown.open = true;
+ }
+
+ let list = state.collect_display_list();
+ let text_pos = |needle: &str| {
+ list.items.iter().position(|item| {
+ matches!(&item.prim, cce_ui::scene::paint::Prim::Text { text, .. } if text == needle)
+ })
+ };
+ // "New Project" is a button label sitting under the open dropdown;
+ // "Other" only exists inside the popover's option list.
+ let label_idx = text_pos("New Project").expect("button label in display list");
+ let option_idx = text_pos("Other").expect("popover option text in display list");
+ assert!(option_idx > label_idx, "popover text must draw after widget labels");
+ let has_bg_between = list.items[label_idx..option_idx]
+ .iter()
+ .any(|item| matches!(item.prim, cce_ui::scene::paint::Prim::Quad { .. }));
+ assert!(has_bg_between, "popover background must draw after widget labels");
+ }
+
#[test]
fn test_mcp_tools_map_to_actions() {
// Every MCP tool except get_state must dispatch by injecting its name
diff --git a/src/render.rs b/src/render.rs
index 00e1d84..3f73b16 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -107,6 +107,7 @@ impl State {
self.append_context_border(&mut pc);
self.append_frame_text(&mut pc);
+ self.append_popovers(&mut pc);
pc.finish()
}
@@ -223,14 +224,6 @@ impl State {
}
}
- if w.visible() && (self.focused_widget == Some(idx) || idx == PARAM_IDX) {
- let mut popover_pc = cce_ui::layout::PopoverCollector::new();
- w.render_popover(&mut popover_pc);
- for (color, px, py, pw, ph) in popover_pc.rects {
- pc.quad(rect(px, py, pw, ph), color);
- }
- }
-
if active_circle.is_some() {
pc.pop_clip_circle();
}
@@ -333,7 +326,8 @@ impl State {
/// The frame's text, as `Prim::Text` items shaped and drawn by the engine
/// (`display_list_text`): each non-menubar widget's walk-derived labels — the
/// graph's clamped to the network pane (and distance-filtered against the circular
- /// pane), network text fading with `network_opacity` — then the open popovers'.
+ /// pane), network text fading with `network_opacity`. Popovers follow in
+ /// `append_popovers`.
fn append_frame_text(&self, pc: &mut PaintCtx) {
let circular = self.circular_network_pane;
let ncx = self.circular_network_layout.x;
@@ -381,7 +375,14 @@ impl State {
}
}
- // Popover text, on top of (i.e. after) all widget labels.
+ }
+
+ /// Open popovers (the params pane's expanded dropdowns), background then
+ /// text per widget. Appended after `append_frame_text` so the popover
+ /// occludes the widget labels underneath it — the display list is drawn
+ /// strictly in order, so a popover background emitted in the geometry
+ /// pass would sit under every label.
+ fn append_popovers(&self, pc: &mut PaintCtx) {
for i in 0..WIDGET_COUNT {
let w = self.slots.get_dyn(i);
if !w.visible() {
@@ -394,6 +395,9 @@ impl State {
if self.focused_widget == Some(i) || i == PARAM_IDX {
let mut popover_pc = cce_ui::layout::PopoverCollector::new();
w.render_popover(&mut popover_pc);
+ for (color, px, py, pw, ph) in popover_pc.rects {
+ pc.quad(rect(px, py, pw, ph), color);
+ }
for (t, size, x, y, tc, font_opt, label_bounds) in popover_pc.texts {
let color = [
(tc[0] * 255.0).round().clamp(0.0, 255.0) as u8,