git.lucas.co / cce-ui
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 }