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

commit3f3ccd4537509dcad4d7da388664b673de4e24c9
parentc811a34188
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 21:24
perf(model): paint each leaf once a frame, not twice

Adapted::paint_self ran Paint::paint_ui into a scratch context, replayed
everything but Text, then rebuilt the own-labels from painted_prims() --
a second full Paint::paint of the same widget, only to get its Text
prims back. Every leaf of every app paid for its paint twice a frame
(a multiline TextBox's whole-document label pass included).

The Text prims the replay hands back are now kept and fed to the bridge
(own_labels_from_painted). Only ParametersBg and Group override
paint_ui, and neither reaches that bridge (legacy labels / subtree
painter), so the text is identical to what the second paint produced.
own_labels_for_walk / own_labels_with_prim_font had no other callers.

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

 src/widget/display/status_bar.rs |  2 +-
 src/widget/model.rs              | 37 ++++++++++++++++++-------------------
 2 files changed, 19 insertions(+), 20 deletions(-)

diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index 36beb0b..f9f107e 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -182,7 +182,7 @@ impl Paint for StatusBar {
     }
 
     /// The paint walk re-fonts prim-derived labels through this (the prim's own font field
-    /// is stripped by `own_labels_for_walk`) — without it the bar's text falls back to sans.
+    /// is stripped by the adapter's own-labels bridge) — without it the bar's text falls back to sans.
     fn text_font(&self) -> Option<String> {
         Some(crate::layout::statusbar_font())
     }
diff --git a/src/widget/model.rs b/src/widget/model.rs
index af77ef9..841465f 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -993,14 +993,16 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         out
     }
 
-    /// The paint-walk view of `own_labels_with_font_and_bounds`: prim-derived text carries the
-    /// widget's content font ([`Paint::text_font`]); the detached base label is drawn in the
-    /// configured detached-label font either way (via `own_labels_with_prim_font`).
-    fn own_labels_for_walk(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        self.own_labels_with_prim_font(ctx, Paint::text_font(&self.inner))
-    }
-
-    fn own_labels_with_prim_font(&self, _ctx: &UiContext, prim_font: Option<String>) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+    /// 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
+    /// `Paint::paint` a second time just to get them back doubled every leaf's paint cost.
+    fn own_labels_from_painted(
+        &self,
+        _ctx: &UiContext,
+        painted: Vec<TextLabel>,
+        prim_font: Option<String>,
+    ) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
         // The detached label is the adapter's, not the widget's: one font for every
         // control's label — `style.control.label.font_detached`, whose size
         // `base_label_fallback` already takes — whatever font the widget's own content
@@ -1010,16 +1012,7 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         let base_font = Some(crate::layout::control_label_font_detached());
         let mut fonted: Vec<(TextLabel, Option<String>)> = Vec::new();
         if self.visible() {
-            fonted.extend(
-                self.painted_prims()
-                    .into_iter()
-                    .filter_map(|prim| match prim {
-                        Prim::Text { text, x, y, font_size, color, .. } => {
-                            Some((TextLabel { text, x, y, font_size, color }, prim_font.clone()))
-                        }
-                        _ => None,
-                    }),
-            );
+            fonted.extend(painted.into_iter().map(|l| (l, prim_font.clone())));
             if !Layout::inline_label(&self.inner) {
                 fonted.extend(self.base_label_fallback().into_iter().map(|l| (l, base_font.clone())));
             }
@@ -1304,6 +1297,10 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         // verbatim and the single-font own-labels re-derivation below is skipped
         // (re-deriving would flatten a composite's mixed child fonts to widget_font).
         let subtree = Paint::paints_own_subtree(&self.inner);
+        // The Text prims the own-labels bridge below re-derives labels from. Only
+        // ParametersBg and Group override `paint_ui`, and neither reaches that
+        // bridge, so this pass's text is exactly what `Paint::paint` would emit.
+        let mut painted_text: Vec<TextLabel> = Vec::new();
         for item in tmp.finish().items {
             // Re-emitting through ctx re-records clip state, so restore the
             // circular clip the widget authored the prim under (Ramp's
@@ -1321,6 +1318,8 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
             {
                 if subtree {
                     ctx.text_with(text, x, y, font_size, color, font, bounds);
+                } else {
+                    painted_text.push(TextLabel { text, x, y, font_size, color });
                 }
             }
             if clip_circle.is_some() {
@@ -1358,7 +1357,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         let labels = if Paint::serves_legacy_labels(&self.inner) {
             Paint::legacy_labels_with_font_and_bounds(&self.inner, self.content_rect(), ui)
         } else {
-            self.own_labels_for_walk(ui)
+            self.own_labels_from_painted(ui, painted_text, Paint::text_font(&self.inner))
         };
         for (tl, font, bounds) in labels {
             ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);