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

src/widget/display/label.rs (10K)

  1 use crate::widget::*;
  2 use crate::widget::display::TextItem;
  3 use crate::scene::layout::{Rect, Size};
  4 use crate::scene::paint::PaintCtx;
  5 
  6 /// Narrow-trait text label (Phase 5f leaf sweep). The text lives on the model and is emitted by
  7 /// `paint` as a `Text` prim, which the adapter's prim-derived `text_labels` bridge serves to
  8 /// every legacy text path; `set_text` sync comes from the adapter's generic `WidgetHost::set_text`
  9 /// override via [`Paint::sync_label`].
 10 #[derive(Debug, Clone)]
 11 pub struct Label {
 12     text: String,
 13     font_size: f32,
 14     color: [u8; 3],
 15 }
 16 
 17 impl Label {
 18     pub fn new(text: &str) -> Adapted<Label> {
 19         let (_, font_size) = crate::layout::control_label_font_parsed();
 20         let mut l = Adapted::new(Label {
 21             text: text.to_string(),
 22             font_size,
 23             color: colors::control_label_color_u8(),
 24         });
 25         // Keep the base copy in step too (context menus, fallback machinery).
 26         l.set_text(text);
 27         l
 28     }
 29 
 30     pub fn set_color(&mut self, color: [u8; 3]) {
 31         self.color = color;
 32     }
 33 }
 34 
 35 impl Adapted<Label> {
 36     pub fn with_font_size(mut self, size: f32) -> Self {
 37         self.font_size = size;
 38         self
 39     }
 40 
 41     pub fn with_color(mut self, color: [u8; 3]) -> Self {
 42         self.color = color;
 43         self
 44     }
 45 }
 46 
 47 impl Layout for Label {
 48     fn inline_label(&self) -> bool {
 49         true
 50     }
 51 
 52     /// Content size for the scene layout engine (Phase 2b). Width is the measured text extent
 53     /// (via the FontSystem-free `measure_text_width`); height is one line at this font size.
 54     fn intrinsic_size(&self) -> Option<Size> {
 55         let (family, _) = crate::layout::control_label_font_parsed();
 56         let width = crate::widget::display::measure_text_width(&self.text, &family, self.font_size);
 57         // Match the line-height factor used elsewhere in the toolkit (e.g. text_box).
 58         let height = self.font_size * 1.333;
 59         Some(Size::new(width, height))
 60     }
 61 }
 62 
 63 impl Paint for Label {
 64     fn color(&self) -> [f32; 4] {
 65         [0.0, 0.0, 0.0, 0.0]
 66     }
 67 
 68     fn sync_label(&mut self, label: &str) {
 69         self.text = label.to_string();
 70     }
 71 
 72     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 73         // Bounded to the rect, with two pixels of slack on the right.
 74         //
 75         // The slack is not cosmetic. A Label reports its own content width
 76         // from `measure_text_width`, which is FontSystem-free and therefore an
 77         // ESTIMATE; a layout that allocates exactly that width would, on a
 78         // hard clip, shave the last glyph of every correctly-sized label the
 79         // moment the shaper disagreed with the estimator by a fraction. The
 80         // slack absorbs that while still catching the case this bound is for:
 81         // a Label handed a box narrower than its text.
 82         ctx.text_with(
 83             self.text.clone(),
 84             rect.x,
 85             crate::layout::align_text_y(rect.y, rect.height, self.font_size, 0.0),
 86             self.font_size,
 87             self.color,
 88             None,
 89             Some([rect.x, rect.y, rect.x + rect.width + 2.0, rect.y + rect.height]),
 90         );
 91     }
 92 }
 93 
 94 impl Input for Label {
 95     fn blocks_root_plate_drag(&self) -> bool {
 96         false
 97     }
 98 }
 99 
100 #[cfg(test)]
101 mod tests {
102     use super::*;
103 
104     /// Legacy `text_labels` parity through the prim bridge, and `set_text` staying in sync
105     /// through the adapter's `WidgetHost::set_text` override (the trait method apps actually hit).
106     #[test]
107     fn text_flows_and_set_text_syncs() {
108         let mut l = Label::new("CPU: 3%").with_font_size(13.0).with_color([1, 2, 3]);
109         WidgetHost::set_rect(&mut l, 10.0, 20.0, 100.0, 16.0);
110 
111         let labels = l.own_text_labels();
112         assert_eq!(labels.len(), 1);
113         assert_eq!(labels[0].text, "CPU: 3%");
114         assert_eq!(labels[0].x, 10.0);
115         assert_eq!(labels[0].font_size, 13.0);
116         assert_eq!(labels[0].color, [1, 2, 3]);
117 
118         l.set_text("CPU: 99%");
119         assert_eq!(l.own_text_labels()[0].text, "CPU: 99%", "set_text reaches the paint source");
120 
121         let size = l.intrinsic_size().unwrap();
122         assert!(size.width > 0.0);
123         assert!(!WidgetHost::blocks_root_plate_drag(&l));
124     }
125 }
126 
127 
128 // Styled label builder with optional strikethrough
129 #[derive(Debug)]
130 pub struct StyledLabel {
131     pub buffer: cosmic_text::Buffer,
132     pub w: f32,
133     pub color: [f32; 4],
134     pub g_color: cosmic_text::Color,
135     pub strikethrough: bool,
136     pub strikethrough_color: Option<[f32; 4]>,
137     // Source retained so the label can be re-emitted as a display-list Text prim (Phase 6ak):
138     // the (possibly vertical-transformed) text, its size, family, and the box layout for the
139     // vertical case (per-char lines + centered wrap). `buffer` above is kept for the legacy
140     // draw()/width measurement path.
141     src_text: String,
142     src_size: f32,
143     src_family: String,
144     prim_layout: Option<crate::scene::paint::TextLayout>,
145 }
146 
147 /// The data to emit a [`StyledLabel`] as a display-list `Prim::Text`: what
148 /// [`StyledLabel::into_prim`] returns, matching `PaintCtx::text_with` / `text_boxed` args.
149 pub struct LabelPrim {
150     pub text: String,
151     pub size: f32,
152     pub x: f32,
153     pub y: f32,
154     pub color: [u8; 3],
155     pub font: Option<String>,
156     pub layout: Option<crate::scene::paint::TextLayout>,
157 }
158 
159 impl StyledLabel {
160     pub fn new(fs: &mut cosmic_text::FontSystem, text: &str, size: f32, color: [f32; 4]) -> Self {
161         Self::new_with_family(fs, text, size, color, "sans-serif")
162     }
163 
164     pub fn new_with_family(fs: &mut cosmic_text::FontSystem, text: &str, size: f32, color: [f32; 4], family: &str) -> Self {
165         let scale = crate::scale::scale_factor();
166         let mut final_text = text.to_string();
167         let is_vert = crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed);
168         if is_vert {
169             final_text = text.chars().map(|c| c.to_string()).collect::<Vec<_>>().join("\n");
170         }
171         let mut buffer = crate::backend::window_runner::get_text_buffer(fs, &final_text, size, Some(family));
172         if is_vert {
173             let bar_thickness = crate::BAR_THICKNESS.load(std::sync::atomic::Ordering::Relaxed) as f32;
174             buffer.set_size(fs, Some(bar_thickness * scale as f32), None);
175             for line in &mut buffer.lines {
176                 line.set_align(Some(cosmic_text::Align::Center));
177             }
178             buffer.shape_until_scroll(fs, true);
179         }
180         let mut w = buffer.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0) / scale;
181         if is_vert {
182             let num_lines = buffer.layout_runs().count();
183             w = num_lines as f32 * size * 1.05;
184         }
185         let g_color = cosmic_text::Color::rgb(
186             (color[0] * 255.0) as u8,
187             (color[1] * 255.0) as u8,
188             (color[2] * 255.0) as u8,
189         );
190         // Vertical text is boxed: the per-char-newline `final_text` wrapped to the bar
191         // thickness and centered — the same set_size + center-align the buffer path applied.
192         let prim_layout = if is_vert {
193             let bar_thickness = crate::BAR_THICKNESS.load(std::sync::atomic::Ordering::Relaxed) as f32;
194             Some(crate::scene::paint::TextLayout {
195                 // Effectively unbounded height (the legacy vertical path used height None);
196                 // align_v Top means no vertical offset, so only set_size's height sees this.
197                 wrap_width: Some(bar_thickness),
198                 box_height: 100_000.0,
199                 align_h: crate::scene::paint::AlignH::Center,
200                 align_v: crate::scene::paint::AlignV::Top,
201             })
202         } else {
203             None
204         };
205         Self {
206             buffer,
207             w,
208             color,
209             g_color,
210             strikethrough: false,
211             strikethrough_color: None,
212             src_text: final_text,
213             src_size: size,
214             src_family: family.to_string(),
215             prim_layout,
216         }
217     }
218 
219     /// Consume the label and return the data to emit it as a display-list `Prim::Text`
220     /// (Phase 6ak): the source text, size, family, and — for vertical bars — the box layout.
221     /// `x, y` are the draw position; vertical labels pin `y` to 0 (as `draw` did).
222     pub fn into_prim(self, x: f32, y: f32) -> LabelPrim {
223         let is_vert = crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed);
224         LabelPrim {
225             text: self.src_text,
226             size: self.src_size,
227             x,
228             y: if is_vert { 0.0 } else { y },
229             color: [
230                 (self.color[0] * 255.0) as u8,
231                 (self.color[1] * 255.0) as u8,
232                 (self.color[2] * 255.0) as u8,
233             ],
234             font: Some(self.src_family),
235             layout: self.prim_layout,
236         }
237     }
238 
239     pub fn with_strikethrough(mut self, enabled: bool) -> Self {
240         self.strikethrough = enabled;
241         self
242     }
243 
244     pub fn with_strikethrough_color(mut self, color: [f32; 4]) -> Self {
245         self.strikethrough_color = Some(color);
246         self
247     }
248 
249     pub fn draw(self, text_items: &mut Vec<TextItem>, x: f32, y: f32) -> f32 {
250         let w = self.w;
251         let is_vert = crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed);
252         text_items.push(TextItem {
253             buffer: self.buffer,
254             x,
255             y: if is_vert { 0.0 } else { y },
256             color: self.g_color,
257             bounds: None,
258             clip_circle: None,
259             clip_rrect: None,
260         });
261         w
262     }
263 
264     pub fn strikethrough_rect(&self, x: f32, y: f32, scale: f32) -> Option<(f32, f32, f32, f32, [f32; 4])> {
265         if self.strikethrough {
266             let col = self.strikethrough_color.unwrap_or(self.color);
267             let font_size = self.buffer.metrics().font_size / scale;
268             let line_y = self.buffer.layout_runs().next().map(|r| r.line_y).unwrap_or(font_size * scale * 1.05) / scale;
269             let offset_y = line_y - 0.28 * font_size;
270             let padding = 4.0;
271             Some((
272                 x - padding,
273                 y + offset_y,
274                 self.w + 2.0 * padding,
275                 1.0,
276                 col,
277             ))
278         } else {
279             None
280         }
281     }
282 }