status bar
git clone https://git.lucas.co/cce-status-interface.git
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);
}
}