GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/display/status_bar.rs (10.3K)
1 //! Narrow-trait `StatusBar` (Phase 5t) — a one-line text bar whose theming is parent-coupled
2 //! exactly like MenuBar's: when its tracked parent is a root plate container it pulls the root plate
3 //! statusbar color/text-color/blur and derives its rounded corners from where it sits against
4 //! the parent's edges ([`Paint::corner_style`] + the corners walk). Two text paths: the
5 //! [`Paint::paint`] prim (carrying the configured statusbar font — the legacy default-font
6 //! behavior on this path dropped the family and rendered sans), and pre-shaped cosmic-text
7 //! buffers through [`Paint::text_items`] (new with this migration) for manual hosts —
8 //! cce-status-interface calls `prepare_text` then `get_text_items` into its own paint.
9
10 use crate::colors;
11 use crate::scene::layout::Rect;
12 use crate::scene::paint::PaintCtx;
13 use crate::widget::display::make_widget_text_buffer;
14 use crate::widget::{Adapted, Input, Layout, Paint};
15
16 pub struct StatusBar {
17 rect: Rect,
18 pub text: String,
19 pub text_buf: Option<cosmic_text::Buffer>,
20 pub text_offset_x: Option<f32>,
21 pub text_color: Option<[f32; 4]>,
22 pub bg_color: Option<[f32; 4]>,
23 /// Draw as a step carved into the window root plate instead of an opaque slab: no
24 /// background fill of its own, just the shaded wall facing the content, so the plate
25 /// shows through. `bg_color` is ignored while this is set — see
26 /// [`Adapted::<StatusBar>::with_recess`].
27 pub recessed: Option<bool>,
28 }
29
30 impl StatusBar {
31 /// The style in force: the per-widget override (`with_recess`) when set, else
32 /// the DE's `control_relief`, read live so a runtime switch
33 /// (`layout::set_control_relief`) restyles every control at once.
34 fn recessed(&self) -> bool {
35 self.recessed.unwrap_or_else(crate::layout::control_relief)
36 }
37
38 pub fn new() -> Adapted<StatusBar> {
39 Adapted::new(StatusBar {
40 rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
41 text: String::new(),
42 text_buf: None,
43 text_offset_x: None,
44 text_color: None,
45 bg_color: None,
46 recessed: None,
47 })
48 }
49
50 pub fn set_text_offset_x(&mut self, offset: f32) {
51 self.text_offset_x = Some(offset);
52 }
53 pub fn set_text_color(&mut self, color: [f32; 4]) {
54 self.text_color = Some(color);
55 }
56 pub fn set_bg_color(&mut self, color: [f32; 4]) {
57 self.bg_color = Some(color);
58 }
59
60 pub fn get_actual_text_color(&self) -> [f32; 4] {
61 self.text_color.unwrap_or([0.6666, 0.6666, 0.7333, 1.0])
62 }
63
64 pub fn is_blur_enabled(&self) -> bool {
65 false
66 }
67
68 fn bg(&self) -> [f32; 4] {
69 self.bg_color.unwrap_or(colors::STATUS_BG)
70 }
71
72 fn statusbar_font_size(&self) -> f32 {
73 let (_, font_size) = crate::layout::statusbar_font_parsed();
74 if font_size > 0.0 { font_size } else { 12.0 }
75 }
76 }
77
78 impl Adapted<StatusBar> {
79 pub fn with_text(mut self, text: &str) -> Self {
80 self.text = text.to_string();
81 self
82 }
83 pub fn with_text_offset_x(mut self, offset: f32) -> Self {
84 self.text_offset_x = Some(offset);
85 self
86 }
87 pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
88 self.text_color = Some(color);
89 self
90 }
91 pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
92 self.bg_color = Some(color);
93 self
94 }
95
96 /// Drop the bar's own background and sink it into the window root plate instead, the
97 /// mirror of `MenuBar::with_recess`. A status bar always sits flush with the bottom of
98 /// the plate, so it is shaded as a plateau one step down whose only wall is the top one
99 /// (facing the content) — the other three sides are the plate's outer edge, which
100 /// carries its own roll.
101 pub fn with_recess(mut self, recessed: bool) -> Self {
102 self.recessed = Some(recessed);
103 self
104 }
105 }
106
107 impl Layout for StatusBar {
108 /// The status text draws inside the bar; the base label must never inflate the rect or
109 /// emit a detached label (`WidgetHost::set_text` writes both the base copy and
110 /// [`Paint::sync_label`]).
111 fn inline_label(&self) -> bool {
112 true
113 }
114
115 fn rect_assigned(&mut self, rect: Rect) {
116 self.rect = rect;
117 }
118
119 }
120
121 impl Paint for StatusBar {
122 fn color(&self) -> [f32; 4] {
123 self.bg()
124 }
125
126 fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
127 // Corners never round (the root plate-adjacency source is gone). The radius was the
128 // parent's, read through a stored pointer — but nothing ever set_parent's a StatusBar,
129 // so 0.0 is what production always read (6bd: the dead pointer field is gone).
130 Some((0.0, (false, false, false, false)))
131 }
132
133 /// `WidgetHost::set_text` lands here: swap the text and drop the shaped buffer so
134 /// `prepare_text` rebuilds it.
135 fn sync_label(&mut self, label: &str) {
136 if self.text != label {
137 self.text = label.to_string();
138 self.text_buf = None;
139 }
140 }
141
142 /// Background exactly on the legacy split: a plain quad when cornerless (the legacy
143 /// `extra_quads` body), a rounded rect against the parent's corners otherwise (the legacy
144 /// default `all_rounded_quads` path) — plus the text label (the legacy `text_labels`
145 /// body; deliberately no `widget_font`, see module docs).
146 fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
147 if self.recessed() {
148 // The recess shading is a light/shadow overlay — whatever the plate painted
149 // here shows through modulated, so no surface color is needed.
150 // Capped against the bar's own height so a deep DE-wide roll can't swallow it
151 // (a single wall straddling the boundary intrudes only half its width).
152 let depth = crate::layout::bar_wall_width().min(rect.height * 0.6);
153 // The wall stays inside the bar (`layout::carve_inside`).
154 let bar = Rect { y: rect.y + depth * 0.5, height: rect.height - depth * 0.5, ..rect };
155 ctx.recess_edges(bar, (0.0, 0.0, 0.0, 0.0), depth, (true, false, false, false));
156 } else {
157 // Always the plain background quad — the rounded-against-parent variant required a
158 // root plate parent, which no longer exists.
159 ctx.quad(rect, self.bg());
160 }
161
162 if !self.text.is_empty() {
163 let offset_x = self.text_offset_x.unwrap_or(12.0);
164 let c = self.get_actual_text_color();
165 let color = [
166 (c[0] * 255.0) as u8,
167 (c[1] * 255.0) as u8,
168 (c[2] * 255.0) as u8,
169 ];
170 let size = self.statusbar_font_size();
171 let text_y = crate::layout::align_text_y(rect.y, rect.height, size, 0.0);
172 ctx.text_with(
173 self.text.clone(),
174 rect.x + offset_x,
175 text_y,
176 size,
177 color,
178 Some(crate::layout::statusbar_font()),
179 None,
180 );
181 }
182 }
183
184 /// The paint walk re-fonts prim-derived labels through this (the prim's own font field
185 /// is stripped by `own_labels_for_walk`) — without it the bar's text falls back to sans.
186 fn text_font(&self) -> Option<String> {
187 Some(crate::layout::statusbar_font())
188 }
189
190 fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
191 if !self.text.is_empty() && self.text_buf.is_none() {
192 let (font_fam, font_size) = crate::layout::statusbar_font_parsed();
193 let size = if font_size > 0.0 { font_size } else { 12.0 };
194 let fam = if font_fam.is_empty() { "Berkeley Mono".to_string() } else { font_fam };
195 self.text_buf = Some(make_widget_text_buffer(fs, &self.text, size, &fam));
196 }
197 }
198
199 }
200
201 impl Input for StatusBar {
202 fn blocks_root_plate_drag(&self) -> bool {
203 false
204 }
205 }
206
207 #[cfg(test)]
208 mod tests {
209 use super::*;
210 use crate::widget::WidgetHost;
211
212 /// The shaped-buffer lifecycle behind the old manual-host path: `set_text` drops the
213 /// buffer, `prepare_text` rebuilds it. (The `get_text_items` getter that served it is
214 /// deleted; the bar's rendered text is the `Paint::paint` prim.)
215 #[test]
216 fn manual_host_text_pipeline() {
217 let mut fs = cosmic_text::FontSystem::new();
218 // Pin the flat style: the bg-quad bridge under test is skipped by the
219 // config-default recessed band.
220 let mut bar = StatusBar::new().with_text("hello").with_text_offset_x(15.0).with_recess(false);
221 WidgetHost::set_rect(&mut bar, 0.0, 570.0, 800.0, 30.0);
222
223 assert!(bar.text_buf.is_none(), "no buffer before prepare_text");
224 WidgetHost::prepare_text(&mut bar, &mut fs);
225 assert!(bar.text_buf.is_some(), "one shaped buffer");
226
227 // set_text drops the stale buffer; prepare_text reshapes.
228 bar.set_text("world");
229 assert!(bar.text_buf.is_none(), "buffer dropped on text change");
230 WidgetHost::prepare_text(&mut bar, &mut fs);
231 assert!(bar.text_buf.is_some());
232 assert_eq!(bar.text, "world");
233
234 // Parentless: cornerless plain bg through the plain-quad bridge, at STATUS_BG.
235 let extra = WidgetHost::extra_quads(&bar);
236 assert_eq!(extra.len(), 1, "cornerless bg quad");
237 assert_eq!(WidgetHost::corner_style(&bar).1, (false, false, false, false));
238 assert!(!WidgetHost::blocks_root_plate_drag(&bar));
239 }
240
241 /// The paint walk strips prim fonts and re-fonts labels via `Paint::text_font` — the
242 /// bar's text must come out of the walk carrying the configured statusbar font.
243 #[test]
244 fn walk_text_carries_statusbar_font() {
245 let ui = crate::context::UiContext::new();
246 let mut bar = StatusBar::new().with_text("ready");
247 WidgetHost::set_rect(&mut bar, 0.0, 570.0, 800.0, 30.0);
248
249 let mut pc = PaintCtx::new();
250 crate::scene::painter::paint_root_into(&ui, &bar, &mut pc);
251 let fonts: Vec<_> = pc
252 .finish()
253 .items
254 .into_iter()
255 .filter_map(|item| match item.prim {
256 crate::scene::paint::Prim::Text { font, .. } => Some(font),
257 _ => None,
258 })
259 .collect();
260 assert_eq!(fonts.len(), 1, "one text label out of the walk");
261 assert_eq!(fonts[0].as_deref(), Some(crate::layout::statusbar_font().as_str()));
262 }
263 }