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

commitcd52cac2e9db3a509d0ca985a6cbcd69d875e65f
parentab44fd20f3
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 22:06
fix(layout): a widget's glyph reaches a flat host

render_widget replays a widget's prims onto a host's RenderTarget and
dropped every Prim::Image. Since the toolkit's symbols became cce-icons
glyphs (3b09eeb) that dropped every widget glyph in a flat host: a closed
dropdown lost its arrow in cce-files, and the breadcrumb's overflow mark,
a spinbox's minus and plus, and the rest went the same way.

An image that is a bundled glyph (icon_source knows its id) now goes to
RenderTarget::icon by name, in its tint and alpha, under the item's clip.
A host that implements icon draws it; one that does not draws nothing,
as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/layout.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/src/layout.rs b/src/layout.rs
index 4ff9360..9ca2045 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4452,6 +4452,25 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
                     None => pc.text_with_bounds(&text, x, y, font_size, color_f32, merged),
                 }
             }
+            // A widget's GLYPH (a dropdown's arrow, a spinbox's −/+, a menu
+            // mark): drawn by name through the host's `RenderTarget::icon`,
+            // since an image id means nothing to a flat host. Until this arm
+            // every widget glyph vanished from a flat host — the toolkit drew
+            // its symbols as text before 2026-10-05, and this replay dropped
+            // images. An image that is not a bundled glyph is still dropped.
+            Prim::Image { image, rect, alpha } => {
+                if let Some((name, _, tint)) = crate::icon_source(image) {
+                    let [r, g, b] = tint.unwrap_or([255, 255, 255]);
+                    let color = [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, alpha];
+                    if let Some(c) = item.clip {
+                        pc.push_clip_rect(c.x, c.y, c.width, c.height);
+                    }
+                    pc.icon(&name, rect, color);
+                    if item.clip.is_some() {
+                        pc.pop_clip_rect();
+                    }
+                }
+            }
             _ => {}
         }
     }
@@ -6609,6 +6628,33 @@ mod tests {
         }
     }
 
+    /// A widget's glyph reaches a flat host: `render_widget` hands the
+    /// dropdown's arrow to the host's `RenderTarget::icon` by name. It used
+    /// to drop every image, so after the toolkit's symbols became glyphs a
+    /// flat host showed a dropdown with no arrow. Skipped where the icon set
+    /// is not checked out (no glyph uploads, so there is nothing to name).
+    #[test]
+    fn a_widget_glyph_reaches_a_flat_host() {
+        if !std::path::Path::new(&crate::icons_dir()).join("chevron-down.svg").is_file() {
+            eprintln!("skipped: no icon set");
+            return;
+        }
+        #[derive(Default)]
+        struct Icons(Vec<String>);
+        impl RenderTarget for Icons {
+            fn rect(&mut self, _: [f32; 4], _: f32, _: f32, _: f32, _: f32) {}
+            fn text(&mut self, _: &str, _: f32, _: f32, _: f32, _: [f32; 4]) {}
+            fn icon(&mut self, name: &str, _: crate::scene::layout::Rect, _: [f32; 4]) {
+                self.0.push(name.to_string());
+            }
+        }
+        let mut pc = Icons::default();
+        let mut ctx = crate::context::UiContext::new();
+        let mut dd = crate::widget::Dropdown::new(vec!["One".to_string(), "Two".to_string()], 0);
+        render_widget(&mut pc, &mut dd, 10.0, 10.0, 160.0, 24.0, &mut ctx);
+        assert!(pc.0.iter().any(|n| n == "chevron-down"), "the arrow reached the host: {:?}", pc.0);
+    }
+
     /// Everything a section places horizontally — text, button rows, widgets,
     /// the column grid — must share ONE inset, or content does not line up
     /// with the content beside it. Text used to sit `padding()` to the left of