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 }