GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(parameters_bg): the pane draws its controls' glyphs
ParametersBg paints its rows' chrome itself and collected only their
text (own_text_labels), never running a control's Paint::paint into the
frame. When the toolkit's symbols became cce-icons glyphs (3b09eeb) the
dropdown and picker arrows and the spinbox's minus and plus stopped
riding the labels, and every control in the pane lost them.
Adapted::own_glyphs is the image half of the label bridge; the pane's
paint_ui and paint emit its rows' glyphs over the chrome.
Test: the_pane_draws_its_controls_glyphs (fails with no glyphs without
the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 8 +++
src/widget/container/parameters_bg.rs | 102 +++++++++++++++++++++++++++++++++-
src/widget/model.rs | 20 +++++++
3 files changed, 128 insertions(+), 2 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 6ce306d..d94fcca 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1830,6 +1830,14 @@ falls back to, and never built from primitives. Apps hold to the same rule.
to `arc`, a disc to `circle` — each a no-op by default, so a host draws what
it implements (`a_widget_glyph_reaches_a_flat_host`,
`a_graphs_wires_reach_a_flat_host`).
+- **`ParametersBg` paints its controls' glyphs itself** (2026-10-06). The
+ pane draws its rows' chrome and collects their TEXT (`own_text_labels`)
+ without ever running a control's `Paint::paint` into the frame, so when
+ the symbols became glyphs every dropdown, picker and spinbox in it lost
+ its arrow or −/+. `Adapted::own_glyphs` is the image half of that bridge,
+ and `paint_ui` / `paint` emit `child_glyphs` over the chrome. A container
+ that collects its children's labels must collect their glyphs too
+ (`the_pane_draws_its_controls_glyphs`).
- **Kept as shapes**, being indicators and not symbols: `StatusDot`'s LED
disc and the plate dock's corner dot.
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 1bafbc7..3bf99aa 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1350,6 +1350,67 @@ impl ParametersBg {
labels
}
+ /// The hosted controls' GLYPHS — a dropdown's or a picker's arrow, a
+ /// spinbox's −/+ — as `(image, rect, alpha)`, row by row as
+ /// [`own_text_labels`](Self::own_text_labels) collects their text. The
+ /// pane paints its controls' chrome itself and never runs their
+ /// `Paint::paint` into the frame, so a symbol a control draws reaches
+ /// the pane only through here. Ramps are not asked: they paint whole
+ /// through `paint_scene_rows`.
+ fn child_glyphs(&self) -> Vec<(u32, Rect, f32)> {
+ let hidden = self.hidden_rows();
+ let mut glyphs = Vec::new();
+ for (i, (_, _, ptype)) in self.display_params.iter().enumerate() {
+ if hidden[i] {
+ continue;
+ }
+ if ptype.starts_with("slider") {
+ if let Some(s) = &self.sliders[i] {
+ glyphs.extend(s.own_glyphs());
+ }
+ } else if is_vec_row(ptype) {
+ if let Some(f) = &self.float3s[i] {
+ glyphs.extend(f.own_glyphs());
+ }
+ } else if ptype.starts_with("spinbox") {
+ if let Some(sb) = &self.spinboxes[i] {
+ glyphs.extend(sb.own_glyphs());
+ }
+ } else if is_text_row(ptype) {
+ if let Some(tb) = &self.texts[i] {
+ glyphs.extend(tb.own_glyphs());
+ }
+ if let Some(d) = &self.choices[i] {
+ glyphs.extend(d.own_glyphs());
+ }
+ } else if ptype.starts_with("choice") {
+ if let Some(d) = &self.choices[i] {
+ glyphs.extend(d.own_glyphs());
+ }
+ } else if ptype == "button" {
+ if let Some(b) = &self.buttons[i] {
+ glyphs.extend(b.own_glyphs());
+ }
+ } else if ptype == "toggle" || ptype == "checkbox" {
+ if let Some(cb) = &self.toggles[i] {
+ glyphs.extend(cb.own_glyphs());
+ }
+ } else if ptype.starts_with("color") || ptype == "rgb" || ptype == "rgba" {
+ if let Some(c) = &self.colors[i] {
+ glyphs.extend(c.own_glyphs());
+ }
+ }
+ }
+ glyphs
+ }
+
+ /// Paint [`child_glyphs`](Self::child_glyphs), over the chrome.
+ fn paint_child_glyphs(&self, ctx: &mut PaintCtx) {
+ for (image, rect, alpha) in self.child_glyphs() {
+ ctx.image(image, rect, alpha);
+ }
+ }
+
/// The dropdown rows' popover, if one is open — the widget's OWN popover surface
/// ([`Paint::popover`]); the raw `children`'s popovers are the adapter's recursion.
/// The ramp rows' field dropdowns count too.
@@ -2112,8 +2173,8 @@ impl Paint for ParametersBg {
/// The COMPLETE row chrome — everything the legacy hatches carry, in the hosts' canonical
/// draw order: the controls' rounded wells, the flat-style section outline fillets, the
/// relief steps (after the fills so the walls shade what they cross), the section carves'
- /// concave throat fillets, the slider-thumb spheres, the scene-path rows, then the flat
- /// plain-quad chrome. What stays OUT, deliberately: the background plate (see
+ /// concave throat fillets, the slider-thumb spheres, the scene-path rows, the flat
+ /// plain-quad chrome, then the hosted controls' glyphs (arrows, −/+) over it. What stays OUT, deliberately: the background plate (see
/// [`color`](Paint::color)), the scrollbar (hosts place its depth — the designer straddles
/// it around the pane plate), and text (`paint_self`'s own-labels bridge carries the
/// per-row fonts and code-box bounds). Hosts clip this to their pane viewport — a rect
@@ -2169,6 +2230,7 @@ impl Paint for ParametersBg {
for (qx, qy, qw, qh, qc) in self.plain_quads() {
ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
}
+ self.paint_child_glyphs(ctx);
}
/// Ui-less emission (the [`paint_ui`](Paint::paint_ui) override above is what `paint_self`
@@ -2180,6 +2242,7 @@ impl Paint for ParametersBg {
ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
}
self.paint_scene_rows(ctx);
+ self.paint_child_glyphs(ctx);
// Scene-path hosts get the scrollbar on top (the designer instead straddles it around
// the pane plate through `scrollbar_quads`).
for (qx, qy, qw, qh, qc) in self.scrollbar_quads() {
@@ -3929,6 +3992,41 @@ mod tests {
assert!((arrow(0) - arrow(1)).abs() < 1e-3, "picker {} vs dropdown {}", arrow(0), arrow(1));
}
+ /// The pane draws its controls' glyphs: a dropdown's and a picker's
+ /// arrow, a spinbox's minus and plus. The pane paints its rows' chrome
+ /// itself and took only their TEXT, so when the symbols became glyphs
+ /// (2026-10-05) every control in the pane lost them. Skipped where the
+ /// icon set is not checked out (no glyph uploads, nothing to name).
+ #[test]
+ fn the_pane_draws_its_controls_glyphs() {
+ if !std::path::Path::new(&crate::icons_dir()).join("chevron-down.svg").is_file() {
+ eprintln!("skipped: no icon set");
+ return;
+ }
+ let p = panel_with(&[
+ ("Name", "mass", "textpick:Norm,UV,Pos,Col"),
+ ("Type", "Float", "choice:Float,Float3,Int"),
+ ("Count", "3", "spinbox:0:10:1"),
+ ]);
+ let ui = UiContext::new();
+ let mut pc = PaintCtx::new();
+ let r = Rect { x: 0.0, y: 0.0, width: 300.0, height: 400.0 };
+ Paint::paint_ui(&*p, &ui, r, &mut pc);
+ let names: Vec<String> = pc
+ .finish()
+ .items
+ .into_iter()
+ .filter_map(|item| match item.prim {
+ crate::scene::paint::Prim::Image { image, .. } => crate::icon_source(image).map(|(n, _, _)| n),
+ _ => None,
+ })
+ .collect();
+ let count = |n: &str| names.iter().filter(|m| *m == n).count();
+ assert_eq!(count("chevron-down"), 2, "the picker's and the dropdown's arrows: {names:?}");
+ assert_eq!(count("minus"), 1, "the spinbox's minus: {names:?}");
+ assert_eq!(count("plus"), 1, "the spinbox's plus: {names:?}");
+ }
+
#[test]
fn textpick_rows_carry_a_picker() {
let p = panel_with(&[
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 841465f..c843e50 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -993,6 +993,26 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
out
}
+ /// This widget's GLYPHS — the image prims its [`Paint::paint`] emits, a
+ /// dropdown's arrow or a spinbox's −/+ — as `(image, rect, alpha)`, for a
+ /// container that paints its children's chrome itself and collects their
+ /// text through [`own_text_labels`](Self::own_text_labels) (the
+ /// parameters pane). Until 2026-10-05 those symbols were text and rode
+ /// the labels; as glyphs nothing carried them, and the pane drew its
+ /// controls without their arrows.
+ pub(crate) fn own_glyphs(&self) -> Vec<(u32, Rect, f32)> {
+ if !self.visible() {
+ return Vec::new();
+ }
+ self.painted_prims()
+ .into_iter()
+ .filter_map(|prim| match prim {
+ Prim::Image { image, rect, alpha } => Some((image, rect, alpha)),
+ _ => None,
+ })
+ .collect()
+ }
+
/// The paint walk's own-labels bridge: prim-derived text in the widget's content font
/// ([`Paint::text_font`]), plus the detached base label in the configured detached-label
/// font. Fed the Text prims `paint_self` already holds from its own pass — running