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

commit2165ae8ed9f2d8af9d081f0694231acf36165294
parent71b7cef246
authorLucas Galante <[email protected]>
date2026-07-07 18:20
feat(scene): content sizing — Label::intrinsic_size + engine proof (Phase 2b)

Give Label an intrinsic_size that measures its text via the FontSystem-free, memoized measure_text_width (width) and font line-height (height), so the layout engine can size content-driven widgets — the capability that unlocks migrating most real widgets, not just fixed/grow ones. Add a bridge test laying out real Labels in a row and asserting each is sized to its measured text. Inert until a container routes Labels through the bridge. 110 tests.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

 src/scene/bridge.rs         | 36 ++++++++++++++++++++++++++++++++++++
 src/widget/display/label.rs | 11 +++++++++++
 2 files changed, 47 insertions(+)

diff --git a/src/scene/bridge.rs b/src/scene/bridge.rs
index e8f9096..0ca3549 100644
--- a/src/scene/bridge.rs
+++ b/src/scene/bridge.rs
@@ -155,6 +155,14 @@ mod tests {
         (id, ptr)
     }
 
+    /// Register any real `Element` (not just the test `W`) and return its (id, ptr).
+    fn reg_elem(ctx: &mut UiContext, e: &mut dyn Element) -> (WidgetId, ElemPtr) {
+        let ptr = e.as_ptr_mut();
+        let id = e.base().expect("widget has a base").id();
+        ctx.register_widget(id, ptr);
+        (id, ptr)
+    }
+
     fn rect_of(ptr: ElemPtr) -> Rect {
         let (x, y, w, h) = unsafe { (*ptr).rect() };
         Rect { x, y, width: w, height: h }
@@ -259,4 +267,32 @@ mod tests {
         // Grandchild untouched — recursion stopped at the opaque pane.
         assert_eq!(rect_of(g_ptr), Rect { x: 1.0, y: 2.0, width: 7.0, height: 7.0 });
     }
+
+    #[test]
+    fn sizes_real_labels_to_their_text_content() {
+        // End-to-end content sizing with a production widget: real Labels report intrinsic_size
+        // from measured text, and the engine lays them out at those widths.
+        use crate::widget::display::Label;
+        let mut ctx = UiContext::new();
+        let mut root = W::container(Style::row().gap(5.0));
+        let mut short = Label::new("Hi");
+        let mut long = Label::new("A considerably longer label");
+
+        let (root_id, root_ptr) = reg(&mut ctx, &mut root);
+        let (short_id, short_ptr) = reg_elem(&mut ctx, &mut short);
+        let (long_id, long_ptr) = reg_elem(&mut ctx, &mut long);
+        ctx.link_ids(root_id, short_id);
+        ctx.link_ids(root_id, long_id);
+
+        let w_short = short.intrinsic_size().unwrap().width;
+        let w_long = long.intrinsic_size().unwrap().width;
+        assert!(w_short > 0.0 && w_long > w_short, "longer text must measure wider");
+
+        layout_subtree(&ctx, root_ptr, Rect { x: 0.0, y: 0.0, width: 500.0, height: 50.0 });
+
+        // Each label sized to its own text; laid out left-to-right with the gap between them.
+        assert_eq!(rect_of(short_ptr).width, w_short);
+        assert_eq!(rect_of(long_ptr).width, w_long);
+        assert_eq!(rect_of(long_ptr).x, w_short + 5.0);
+    }
 }
diff --git a/src/widget/display/label.rs b/src/widget/display/label.rs
index b0b9520..2135876 100644
--- a/src/widget/display/label.rs
+++ b/src/widget/display/label.rs
@@ -45,6 +45,17 @@ impl Element for Label {
 
     fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
 
+    /// Content size for the scene layout engine (Phase 2b). Width is the measured text extent
+    /// (via the FontSystem-free `measure_text_width`); height is one line at this font size.
+    fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
+        let (family, _) = crate::layout::control_label_font_parsed();
+        let text = self.base.label.as_deref().unwrap_or("");
+        let width = crate::widget::display::measure_text_width(text, &family, self.font_size);
+        // Match the line-height factor used elsewhere in the toolkit (e.g. text_box).
+        let height = self.font_size * 1.333;
+        Some(crate::scene::layout::Size::new(width, height))
+    }
+
     fn text_labels(&self) -> Vec<TextLabel> {
         vec![TextLabel {
             text: self.base.label.clone().unwrap_or_default(),