git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit4237b47464c9651c2b0ef3a5b3526bb872ff754e
parent0f943f5aea
authorLucas Galante <[email protected]>
date2026-07-10 14:58
feat: whole frame via display_list (Phase 6ak) — off the legacy view*/text_items path; LAST app across

cce-status-interface (6 of 6) — the persistent layer-shell bar. view() +
view_rounded_quads() bodies move into display_list() (rounded boxes, status-bar
bg, module rects, separators as prims, in the wrapper's order); the module text
becomes Prim::Text emitted fresh each frame. overlay_quads() stays a separate
on-top pass (tray hover highlights over text). The status bar's own text is never
set in this app, so its text path (get_text_items) was a no-op — the text_items()
and custom text_areas() overrides are deleted, and self.font_system is kept only
for the modules' measure-then-position shaping.

Mechanism: the modules build a StyledLabel to measure width, then emit a prim
via the new StyledLabel::into_prim (cce-ui, previous work) through a draw_label
helper — the vertical bar's per-char/centered text rides the boxed-text prim
(TextLayout wrap+center). Bundled fonts, so no invisible-text hazard; a
byte-match flip.

Live-verified: a single `--module clock` test instance renders "Friday, July 10,
2026 ... PM" with its rounded background pill through the display list. (Full
multi-module A/B avoided — the no-arg binary is a launcher daemon that would
spawn a bar conflicting with the user's live one; testing used one isolated
module segment.)

With this, all six legacy-path apps are across — the view*/text_items trait
deletion is now unblocked.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs    | 130 +++++++++++++++++++++++++++++++--------------------------
 src/modules.rs |  64 +++++++++++++++-------------
 2 files changed, 104 insertions(+), 90 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 90a2eb3..9b5cf28 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,11 +4,11 @@ use modules::{StatusModule, WindowModule, ClockModule, BatteryModule, VolumeModu
 use std::collections::HashMap;
 use std::sync::Arc;
 use glyphon::{
-    Attrs, Buffer, FontSystem, Metrics, TextArea, TextBounds,
+    Attrs, Buffer, FontSystem, Metrics,
 };
 use cce_ui::color;
 use cce_ui::widget::{
-    Adapted, TextItem, Separator, Element,
+    Adapted, Separator, Element,
     MouseButton, ElementState, MouseScrollDelta, KeyEvent,
 };
 
@@ -282,7 +282,7 @@ struct StatusApp {
     overlay_rects: Vec<RectWidget>,
     rounded_boxes: Vec<RoundedBox>,
     separators: Vec<Adapted<Separator>>,
-    text_items: Vec<TextItem>,
+    text_prims: Vec<TextPrim>,
 
     scale_factor: f64,
     width: u32,
@@ -365,7 +365,7 @@ impl StatusApp {
         self.overlay_rects.clear();
         self.rounded_boxes.clear();
         self.separators.clear();
-        self.text_items.clear();
+        self.text_prims.clear();
         self.input_regions.clear();
         self.module_bounds.clear();
         self.tray_item_bounds.clear();
@@ -445,7 +445,7 @@ impl StatusApp {
                     normal_color,
                     bar_h,
                     self.scale_factor,
-                    &mut self.text_items,
+                    &mut self.text_prims,
                     &mut self.rects,
                     &mut self.overlay_rects,
                     &mut self.viewport_bounds,
@@ -535,7 +535,7 @@ impl StatusApp {
                     normal_color,
                     bar_h,
                     self.scale_factor,
-                    &mut self.text_items,
+                    &mut self.text_prims,
                     &mut self.rects,
                     &mut self.overlay_rects,
                     &mut self.viewport_bounds,
@@ -628,17 +628,21 @@ impl StatusApp {
                 });
 
                 // Tooltip text
-                self.text_items.push(TextItem {
-                    buffer: buf,
-                    x: tx + padding,
-                    y: ty + padding,
-                    color: glyphon::Color::rgb(
+                let _ = buf; // shaped only to measure `text_w` above
+                self.text_prims.push((
+                    tooltip_text.clone(),
+                    tooltip_font_size,
+                    tx + padding,
+                    ty + padding,
+                    [
                         (color::TEXT_FG[0] * 255.0) as u8,
                         (color::TEXT_FG[1] * 255.0) as u8,
                         (color::TEXT_FG[2] * 255.0) as u8,
-                    ),
-                    bounds: None,
-                });
+                    ],
+                    Some(font_family.clone()),
+                    None,
+                    None,
+                ));
             }
         }
 
@@ -686,12 +690,12 @@ impl StatusApp {
                 r.w = old_h;
                 r.h = old_w;
             }
-            // Rotate text_items
-            for ti in &mut self.text_items {
-                let old_x = ti.x;
-                let old_y = ti.y;
-                ti.x = old_y;
-                ti.y = old_x;
+            // Rotate text prims (swap x/y — tuple fields .2 and .3)
+            for tp in &mut self.text_prims {
+                let old_x = tp.2;
+                let old_y = tp.3;
+                tp.2 = old_y;
+                tp.3 = old_x;
             }
             // Rotate tray_item_bounds
             for tib in &mut self.tray_item_bounds {
@@ -1072,6 +1076,19 @@ fn parse_selected_module_from_args() -> Option<(String, Side)> {
     None
 }
 
+/// The frame's text as prim data: (text, size, x, y, color_u8, font, bounds, box-layout).
+pub(crate) type TextPrim = (String, f32, f32, f32, [u8; 3], Option<String>, Option<[f32; 4]>, Option<cce_ui::scene::paint::TextLayout>);
+
+/// Emit a measured `StyledLabel` as a text-prim tuple, returning its width (like the legacy
+/// `StyledLabel::draw`). The label was built for its width; `into_prim` carries the source
+/// text/size/family/box-layout so the engine reshapes it through the shared cache.
+pub(crate) fn draw_label(prims: &mut Vec<TextPrim>, label: cce_ui::widget::StyledLabel, x: f32, y: f32) -> f32 {
+    let w = label.w;
+    let p = label.into_prim(x, y);
+    prims.push((p.text, p.size, p.x, p.y, p.color, p.font, None, p.layout));
+    w
+}
+
 impl cce_ui::engine::Application for StatusApp {
     type Message = CustomEvent;
 
@@ -1159,7 +1176,7 @@ impl cce_ui::engine::Application for StatusApp {
             overlay_rects: Vec::new(),
             rounded_boxes: Vec::new(),
             separators: Vec::new(),
-            text_items: Vec::new(),
+            text_prims: Vec::new(),
             scale_factor: 1.0,
             width: if selected_module.is_some() { 120 } else { 1920 },
             height: read_status_height_from_config() as u32,
@@ -1295,10 +1312,15 @@ impl cce_ui::engine::Application for StatusApp {
                 }
             }
         }
-        self.status_bar.prepare_text(&mut self.font_system);
     }
 
-    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: cce_ui::engine::LogicalSize, scale: f64) {
+    fn display_list(&mut self, size: cce_ui::engine::LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
+        // Phase 6ak single paint path: the rounded boxes, the status-bar bg / module rects /
+        // separators (the legacy view_rounded_quads then view() bodies, in the wrapper's
+        // order), and the module text (prims, reshaped by the engine cache). overlay_quads
+        // stays a separate on-top pass. The status bar's own text is never set in this app,
+        // so it contributes only its background quad.
+        use cce_ui::scene::layout::Rect;
         if self.needs_rebuild || self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale {
             self.width = size.width as u32;
             self.height = size.height as u32;
@@ -1306,57 +1328,45 @@ impl cce_ui::engine::Application for StatusApp {
             cce_ui::scale::set_scale_factor(scale as f32);
             self.rebuild_layout();
         }
+        let mut pc = cce_ui::scene::paint::PaintCtx::new();
+
+        for rb in &self.rounded_boxes {
+            let rect = Rect { x: rb.x, y: rb.y, width: rb.w, height: rb.h };
+            if rb.radius > 0.1 {
+                pc.rounded_rect(rect, rb.radius, rb.corners, rb.color);
+            } else {
+                pc.quad(rect, rb.color);
+            }
+        }
+
         let (sb_x, sb_y, sb_w, sb_h) = self.status_bar.rect();
-        quads.push((sb_x, sb_y, sb_w, sb_h, self.status_bar.color()));
+        pc.quad(Rect { x: sb_x, y: sb_y, width: sb_w, height: sb_h }, self.status_bar.color());
         for r in &self.rects {
-            quads.push((r.x, r.y, r.w, r.h, r.color));
+            pc.quad(Rect { x: r.x, y: r.y, width: r.w, height: r.h }, r.color);
         }
         for sep in &self.separators {
             let (x, y, w, h) = sep.rect();
-            quads.push((x, y, w, h, sep.color()));
+            pc.quad(Rect { x, y, width: w, height: h }, sep.color());
         }
-    }
 
-    fn view_rounded_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))>, _size: cce_ui::engine::LogicalSize, _scale: f64) {
-        for rb in &self.rounded_boxes {
-            quads.push((rb.x, rb.y, rb.w, rb.h, rb.radius, rb.color, rb.corners));
+        for (text, tsize, x, y, color, font, bounds, layout) in &self.text_prims {
+            match layout {
+                Some(l) => pc.text_boxed(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds, cce_ui::scene::paint::TextAttrs::default(), *l),
+                None => pc.text_with(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds),
+            }
         }
-    }
 
-    fn overlay_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: cce_ui::engine::LogicalSize, _scale: f64) {
-        for r in &self.overlay_rects {
-            quads.push((r.x, r.y, r.w, r.h, r.color));
-        }
+        Some(pc.finish())
     }
 
-    fn text_items(&self) -> &[TextItem] {
-        &self.text_items
+    fn display_list_text(&self) -> bool {
+        true
     }
 
-    fn text_areas(&self, scale_f32: f32, bounds: TextBounds) -> Vec<TextArea<'_>> {
-        let mut areas = self.text_items().iter().map(|ti| TextArea {
-            buffer: &ti.buffer,
-            left: (ti.x * scale_f32).round(),
-            top: (ti.y * scale_f32).round(),
-            scale: 1.0,
-            bounds,
-            default_color: ti.color,
-            custom_glyphs: &[],
-        }).collect::<Vec<_>>();
-
-        for (buf, x, y, col) in self.status_bar.get_text_items() {
-            areas.push(TextArea {
-                buffer: buf,
-                left: (x * scale_f32).round(),
-                top: (y * scale_f32).round(),
-                scale: 1.0,
-                bounds,
-                default_color: col,
-                custom_glyphs: &[],
-            });
+    fn overlay_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: cce_ui::engine::LogicalSize, _scale: f64) {
+        for r in &self.overlay_rects {
+            quads.push((r.x, r.y, r.w, r.h, r.color));
         }
-
-        areas
     }
 
     fn clear_color(&self) -> [f32; 4] {
diff --git a/src/modules.rs b/src/modules.rs
index b910d23..c2063a1 100644
--- a/src/modules.rs
+++ b/src/modules.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 use glyphon::FontSystem;
 use cce_ui::color;
-use cce_ui::widget::{StyledLabel as Label, TextItem};
+use cce_ui::widget::StyledLabel as Label;
 
 use crate::{
     RectWidget, RoundedBox, ViewportBounds, LayoutBounds, SystemStats, TrayItem,
@@ -40,7 +40,7 @@ pub trait StatusModule {
         normal_color: [f32; 4],
         bar_h: f32,
         scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         rects: &mut Vec<RectWidget>,
         overlay_rects: &mut Vec<RectWidget>,
         viewport_bounds: &mut Vec<ViewportBounds>,
@@ -128,7 +128,7 @@ impl StatusModule for WindowModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         viewport_bounds: &mut Vec<ViewportBounds>,
@@ -153,21 +153,21 @@ impl StatusModule for WindowModule {
                 }
                 let label = Label::new_with_family(font_system, &display_title, font_size, normal_color, font_family);
                 let w = label.w;
-                label.draw(text_items, cur_x, y_pos);
+                crate::draw_label(text_prims, label, cur_x, y_pos);
                 cur_x += w;
             }
 
             if has_title && has_layout {
                 let sep_label = Label::new_with_family(font_system, " - ", font_size, normal_color, font_family);
                 let w = sep_label.w;
-                sep_label.draw(text_items, cur_x, y_pos);
+                crate::draw_label(text_prims, sep_label, cur_x, y_pos);
                 cur_x += w;
             }
 
             if has_layout {
                 let layout_label = Label::new_with_family(font_system, layout, font_size, normal_color, font_family);
                 let w = layout_label.w;
-                layout_label.draw(text_items, cur_x, y_pos);
+                crate::draw_label(text_prims, layout_label, cur_x, y_pos);
                 *layout_bounds = Some(LayoutBounds {
                     x: cur_x - 2.0,
                     y: 0.0,
@@ -192,7 +192,7 @@ impl StatusModule for WindowModule {
                         corners: (false, false, true, true),
                     });
                 }
-                label.draw(text_items, cur_x + padding, (bar_h - font_size * 1.4) / 2.0);
+                crate::draw_label(text_prims, label, cur_x + padding, (bar_h - font_size * 1.4) / 2.0);
                 viewport_bounds.push(ViewportBounds {
                     name: text.clone(),
                     x: cur_x,
@@ -246,7 +246,7 @@ impl StatusModule for ClockModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -260,7 +260,7 @@ impl StatusModule for ClockModule {
     ) {
         if let Some(ref s) = stats {
             let label = Label::new_with_family(font_system, &s.clock, font_size, normal_color, font_family);
-            label.draw(text_items, x + padding, (bar_h - font_size * 1.4) / 2.0);
+            crate::draw_label(text_prims, label, x + padding, (bar_h - font_size * 1.4) / 2.0);
         }
     }
 }
@@ -305,7 +305,7 @@ impl StatusModule for BatteryModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -325,7 +325,7 @@ impl StatusModule for BatteryModule {
                     color::TEXT_ACCENT
                 };
                 let label = Label::new_with_family(font_system, &s.battery, font_size, bat_color, font_family);
-                label.draw(text_items, x + padding, (bar_h - font_size * 1.4) / 2.0);
+                crate::draw_label(text_prims, label, x + padding, (bar_h - font_size * 1.4) / 2.0);
             }
         }
     }
@@ -371,7 +371,7 @@ impl StatusModule for VolumeModule {
         normal_color: [f32; 4],
         bar_h: f32,
         scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -404,7 +404,7 @@ impl StatusModule for VolumeModule {
                         color: color::to_linear(scol),
                     });
                 }
-                label.draw(text_items, draw_x, start_y);
+                crate::draw_label(text_prims, label, draw_x, start_y);
             }
         }
     }
@@ -450,7 +450,7 @@ impl StatusModule for BrightnessModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -465,7 +465,7 @@ impl StatusModule for BrightnessModule {
         if let Some(ref s) = stats {
             if !s.brightness.is_empty() {
                 let label = Label::new_with_family(font_system, &s.brightness, font_size, normal_color, font_family);
-                label.draw(text_items, x + padding, (bar_h - font_size * 1.4) / 2.0);
+                crate::draw_label(text_prims, label, x + padding, (bar_h - font_size * 1.4) / 2.0);
             }
         }
     }
@@ -511,7 +511,7 @@ impl StatusModule for MemoryModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -525,7 +525,7 @@ impl StatusModule for MemoryModule {
     ) {
         if let Some(ref s) = stats {
             let label = Label::new_with_family(font_system, &s.memory, font_size, normal_color, font_family);
-            label.draw(text_items, x + padding, (bar_h - font_size * 1.4) / 2.0);
+            crate::draw_label(text_prims, label, x + padding, (bar_h - font_size * 1.4) / 2.0);
         }
     }
 }
@@ -570,7 +570,7 @@ impl StatusModule for CpuModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -584,7 +584,7 @@ impl StatusModule for CpuModule {
     ) {
         if let Some(ref s) = stats {
             let label = Label::new_with_family(font_system, &s.cpu, font_size, normal_color, font_family);
-            label.draw(text_items, x + padding, (bar_h - font_size * 1.4) / 2.0);
+            crate::draw_label(text_prims, label, x + padding, (bar_h - font_size * 1.4) / 2.0);
         }
     }
 }
@@ -628,7 +628,7 @@ impl StatusModule for TrayModule {
         _normal_color: [f32; 4],
         bar_h: f32,
         scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -763,22 +763,26 @@ impl StatusModule for TrayModule {
                     "⚙"
                 };
 
+                // Measure the throwaway buffer for centering, then emit a text prim.
                 let buf = make_text_buffer(font_system, symbol, font_size, font_family);
                 let scale = cce_ui::scale::scale_factor();
                 let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0) / scale;
                 let tx = icon_x + (icon_size - tw) / 2.0;
                 let ty = icon_y + (icon_size - font_size * 1.4) / 2.0;
-                text_items.push(TextItem {
-                    buffer: buf,
-                    x: tx,
-                    y: ty,
-                    color: glyphon::Color::rgb(
+                text_prims.push((
+                    symbol.to_string(),
+                    font_size,
+                    tx,
+                    ty,
+                    [
                         (color::TEXT_ACCENT[0] * 255.0) as u8,
                         (color::TEXT_ACCENT[1] * 255.0) as u8,
                         (color::TEXT_ACCENT[2] * 255.0) as u8,
-                    ),
-                    bounds: None,
-                });
+                    ],
+                    Some(font_family.to_string()),
+                    None,
+                    None,
+                ));
             }
         }
     }
@@ -836,7 +840,7 @@ impl StatusModule for LightSourceModule {
         normal_color: [f32; 4],
         bar_h: f32,
         _scale_factor: f64,
-        text_items: &mut Vec<TextItem>,
+        text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -851,6 +855,6 @@ impl StatusModule for LightSourceModule {
         let pos = get_light_source_pos_from_config();
         let text = format!("Light: {:.2} rad", pos);
         let label = Label::new_with_family(font_system, &text, font_size, normal_color, font_family);
-        label.draw(text_items, x + padding, (bar_h - font_size * 1.4) / 2.0);
+        crate::draw_label(text_prims, label, x + padding, (bar_h - font_size * 1.4) / 2.0);
     }
 }