GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(widget): migrate Label (Phase 5g)
Text lives on the model and is emitted as a Text prim; the adapter's
prim-derived text_labels bridge serves every legacy text path. Adds a
generic synced Element::set_text override on Adapted — the trait method
wrote only the base label and left self-painted text stale, which
live-updating labels (system-info CPU readouts) hit constantly.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LkfJazPs9bRchkcozmxXCX
docs/rfc-core-rebuild.md | 7 +++
src/widget/display/label.rs | 110 +++++++++++++++++++++++++++++++-------------
src/widget/model.rs | 7 +++
3 files changed, 92 insertions(+), 32 deletions(-)
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 3092439..bf3ea3d 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -441,6 +441,13 @@ Constraint respected: **each crate still builds standalone** — the new core is
pixel-diffs cursor-only vs the 5e baseline; a live hover A/B against the stashed legacy
build showed the identical fill pixel (the inert hover on that page is pre-existing app
behavior, not a regression).
+ - **5g — `Label`. DONE.** Text lives on the model, emitted as a `Text` prim; the adapter's
+ prim bridge serves every legacy text path. Added the generic synced `Element::set_text`
+ override on `Adapted` (same trap as `set_label`: the trait method wrote only the base and
+ left the painted text stale — live-updating labels like system-info's CPU readouts hit it
+ constantly). Builders mirrored; three app repos' field types updated. Workspace builds;
+ 155 tests pass; test-interface diff vs the post-Button baseline has a 0x0 bbox at 2%
+ threshold (sub-perceptual blend noise only).
- **Still to do:** migrate remaining widgets
off `impl Element` onto the narrow traits (per-widget, Phase 6 flavour); replace the 8 live
`as_*_controller` downcast pairs (called by `cce-designer`, `cce-test-interface`, and ~10
diff --git a/src/widget/display/label.rs b/src/widget/display/label.rs
index 2135876..04ffa5a 100644
--- a/src/widget/display/label.rs
+++ b/src/widget/display/label.rs
@@ -1,25 +1,38 @@
use crate::widget::*;
use crate::widget::display::{TextLabel, TextItem};
+use crate::scene::layout::{Rect, Size};
+use crate::scene::paint::PaintCtx;
+/// Narrow-trait text label (Phase 5f leaf sweep). The text lives on the model and is emitted by
+/// `paint` as a `Text` prim, which the adapter's prim-derived `text_labels` bridge serves to
+/// every legacy text path; `set_text` sync comes from the adapter's generic `Element::set_text`
+/// override via [`Paint::sync_label`].
#[derive(Debug, Clone)]
pub struct Label {
- base: Widget,
+ text: String,
font_size: f32,
color: [u8; 3],
}
impl Label {
- pub fn new(text: &str) -> Self {
- let mut base = Widget::new();
- base.label = Some(text.to_string());
+ pub fn new(text: &str) -> Adapted<Label> {
let (_, font_size) = crate::layout::control_label_font_parsed();
- Self {
- base,
+ let mut l = Adapted::new(Label {
+ text: text.to_string(),
font_size,
color: colors::control_label_color_u8(),
- }
+ });
+ // Keep the base copy in step too (context menus, fallback machinery).
+ Element::set_text(&mut l, text);
+ l
+ }
+
+ pub fn set_color(&mut self, color: [u8; 3]) {
+ self.color = color;
}
+}
+impl Adapted<Label> {
pub fn with_font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
@@ -29,41 +42,74 @@ impl Label {
self.color = color;
self
}
-
- pub fn set_text(&mut self, text: &str) {
- self.base.label = Some(text.to_string());
- }
-
- pub fn set_color(&mut self, color: [u8; 3]) {
- self.color = color;
- }
}
-impl Element for Label {
- crate::impl_widget_base!(Label);
- fn blocks_backplate_drag(&self) -> bool { false }
-
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+impl Layout for Label {
+ fn inline_label(&self) -> bool {
+ true
+ }
/// 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> {
+ fn intrinsic_size(&self) -> Option<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);
+ let width = crate::widget::display::measure_text_width(&self.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))
+ Some(Size::new(width, height))
}
+}
- fn text_labels(&self) -> Vec<TextLabel> {
- vec![TextLabel {
- text: self.base.label.clone().unwrap_or_default(),
- x: self.base.x,
- y: crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, 0.0),
- font_size: self.font_size,
- color: self.color,
- }]
+impl Paint for Label {
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn sync_label(&mut self, label: &str) {
+ self.text = label.to_string();
+ }
+
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ ctx.text(
+ self.text.clone(),
+ rect.x,
+ crate::layout::align_text_y(rect.y, rect.height, self.font_size, 0.0),
+ self.font_size,
+ self.color,
+ );
+ }
+}
+
+impl Input for Label {
+ fn blocks_backplate_drag(&self) -> bool {
+ false
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Legacy `text_labels` parity through the prim bridge, and `set_text` staying in sync
+ /// through the adapter's `Element::set_text` override (the trait method apps actually hit).
+ #[test]
+ fn text_flows_and_set_text_syncs() {
+ let mut l = Label::new("CPU: 3%").with_font_size(13.0).with_color([1, 2, 3]);
+ Element::set_rect(&mut l, 10.0, 20.0, 100.0, 16.0);
+
+ let labels = Element::text_labels(&l);
+ assert_eq!(labels.len(), 1);
+ assert_eq!(labels[0].text, "CPU: 3%");
+ assert_eq!(labels[0].x, 10.0);
+ assert_eq!(labels[0].font_size, 13.0);
+ assert_eq!(labels[0].color, [1, 2, 3]);
+
+ Element::set_text(&mut l, "CPU: 99%");
+ assert_eq!(Element::text_labels(&l)[0].text, "CPU: 99%", "set_text reaches the paint source");
+
+ let size = Element::intrinsic_size(&l).unwrap();
+ assert!(size.width > 0.0);
+ assert!(!Element::blocks_backplate_drag(&l));
}
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 231ee8f..73b09d6 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -387,6 +387,13 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
std::any::type_name::<W>().split("::").last().unwrap_or("Widget")
}
+ /// Text-content mutation (legacy `Element::set_text` wrote only `base.label`): keep the base
+ /// copy and the widget's own copy ([`Paint::sync_label`]) in step, like `set_label`.
+ fn set_text(&mut self, text: &str) {
+ self.base.label = Some(text.to_string());
+ Paint::sync_label(&mut self.inner, text);
+ }
+
// --- Paint concern -> `Paint` ---
fn color(&self) -> [f32; 4] {
Paint::color(&self.inner)