Skip to main content

smudge_text/
label.rs

1//! The label widget: one block of static or reactive text.
2//!
3//! A [`Label`] lays its text out through the thread's [`TextSystem`] with one
4//! [`TextStyle`], measures itself from the laid out lines, and paints the
5//! resulting glyph runs. A change to the text, the font size, the weight,
6//! the family or the line cap lays the label out again, and a change to the
7//! color only repaints it.
8//!
9//! Create one with [`label`] and shape it with the [`LabelExt`] builder
10//! methods. At mount the label reads the ambient text style, which
11//! [`provide_text_style`](crate::provide_text_style) sets, and fills every
12//! style field that no builder method set. The color the glyphs paint with
13//! resolves against the theme when the label paints, so a theme switch
14//! repaints it: the node's
15//! [`text_color`](smudge_core::element::Element::text_color) when it sets
16//! one, which may name a theme token, and otherwise the ambient style's
17//! color, which defaults to the theme's text token.
18
19use std::cell::RefCell;
20use std::rc::Rc;
21
22use parley::FontFamily;
23use parley::FontWeight;
24use smudge_core::element::Element;
25use smudge_core::element::Value;
26use smudge_core::geometry::Color;
27use smudge_core::geometry::Point;
28use smudge_core::geometry::Size;
29use smudge_core::paint::GlyphRun;
30use smudge_core::paint::PaintOp;
31use smudge_core::semantics::{Role, Semantics};
32use smudge_core::tree::Dirty;
33use smudge_core::widget::AvailableSpace;
34use smudge_core::widget::MeasureInput;
35use smudge_core::widget::PaintCx;
36use smudge_core::widget::Widget;
37
38use crate::layout::TextLayout;
39use crate::style::TextStyle;
40use crate::style::current_text_style;
41
42/// A block of text laid out with one text style.
43///
44/// Build one with [`label`]. The label keeps the layout of its current text
45/// and rebuilds it only when the text, the style or the offered width
46/// changes, and keeps the glyph runs of that layout and rebuilds them only
47/// when the layout, the node's position or the resolved text color changes.
48pub struct Label {
49    /// The text the label shows.
50    text: String,
51    /// The text style the label lays out and paints with, resolved from the
52    /// ambient style and the explicit fields at mount.
53    style: TextStyle,
54    /// The style fields an explicit write set, which the ambient style leaves
55    /// alone.
56    explicit: ExplicitStyle,
57    /// The most lines the label shows, or `None` for every line.
58    max_lines: Option<u32>,
59    /// The layout of [`Label::text`], or `None` before the first measure and
60    /// after every change that invalidates it.
61    ///
62    /// The layout holds the shaped text once; measuring at another offered
63    /// width re-breaks its lines instead of shaping the text again, which is
64    /// what keeps a layout engine's repeated measure calls for one node cheap.
65    layout: Option<TextLayout>,
66    /// The width the lines of [`Label::layout`] were last broken at, `None`
67    /// for max content, or `None` inside `None` when nothing is laid out.
68    broken_width: Option<Option<f32>>,
69    /// The size of the text wrapped at every opportunity, which is its
70    /// min-content size, or `None` until it is asked for.
71    ///
72    /// It is cached apart from the line breaking, so answering a min-content
73    /// request never depends on the width other calls measured at.
74    min_content: Option<Size>,
75    /// The glyph runs of the current layout, with the origin and the resolved
76    /// color they were built at. Paint operations share them through an `Rc`.
77    runs: RefCell<Option<CachedRuns>>,
78}
79
80/// The glyph runs of one layout, with the origin and resolved color they were
81/// built at.
82struct CachedRuns {
83    /// The origin the glyph positions are absolute to.
84    origin: Point,
85    /// The color every run was built with.
86    color: Color,
87    /// The runs, shared by the paint operations that show them.
88    runs: Vec<Rc<GlyphRun>>,
89}
90
91impl Label {
92    /// Creates a label with the default style until the mount reads the
93    /// ambient one.
94    fn new(text: String) -> Label {
95        Label {
96            text,
97            style: TextStyle::default(),
98            explicit: ExplicitStyle::default(),
99            max_lines: None,
100            layout: None,
101            broken_width: None,
102            min_content: None,
103            runs: RefCell::new(None),
104        }
105    }
106
107    /// Drops the cached layout, the glyph runs built from it and the cached
108    /// min-content size.
109    fn invalidate_layout(&mut self) {
110        self.layout = None;
111        self.broken_width = None;
112        self.min_content = None;
113        *self.runs.borrow_mut() = None;
114    }
115}
116
117impl Widget for Label {
118    /// Reports the text as the accessible value.
119    ///
120    /// The role is what an accessibility backend maps a static text to, and
121    /// such a node takes its name from its value there, so the text rides in
122    /// the value and the label stays empty.
123    fn semantics(&self) -> Option<Semantics> {
124        Some(Semantics {
125            role: Role::Label,
126            label: None,
127            value: Some(self.text.clone()),
128        })
129    }
130
131    /// Lays the text out against the offered width and returns its size.
132    ///
133    /// A definite [`AvailableSpace::Definite`] wraps the text at that width,
134    /// and [`AvailableSpace::MaxContent`] lays it out on one line. An
135    /// [`AvailableSpace::MinContent`] request gets the text wrapped at every
136    /// opportunity, whose width is its longest unbreakable word, so a label
137    /// in a row too narrow for it can shrink and wrap instead of overflowing
138    /// and squeezing its siblings. A label capped to one line answers with
139    /// its ellipsis alone and one line of height, since it can cut any text
140    /// down to that. Under a cap of more lines, the min-content height stays
141    /// the full text's, since it answers what the text needs rather than what
142    /// the cap shows; the line cap reports its height at a definite width.
143    ///
144    /// The text is shaped once and kept; a call that offers another width
145    /// than the last one re-breaks the shaped text's lines, because the
146    /// layout engine measures one node at several widths within a pass. The
147    /// min-content answer breaks the same shaped text at every opportunity
148    /// and then breaks it back at the width the label was last measured at,
149    /// so once the label has been measured at one, the answer never changes
150    /// the lines paint draws. The glyph runs are dropped whenever the shaped text
151    /// or its line breaking changed, so paint rebuilds them from the lines the
152    /// last call left.
153    ///
154    /// # Panics
155    ///
156    /// Panics under the conditions of [`TextSystem::with`](crate::TextSystem::with),
157    /// in particular when called from inside another `with` call on the same
158    /// thread.
159    fn measure(&mut self, input: MeasureInput) -> Option<Size> {
160        if input.available_width == AvailableSpace::MinContent && input.known_width.is_none() {
161            if self.min_content.is_none() {
162                let painted = self.broken_width;
163                self.shape_and_break(Some(0.0));
164                let layout = self
165                    .layout
166                    .as_mut()
167                    .expect("the layout was just built or is cached");
168                if matches!(self.max_lines, Some(0 | 1)) {
169                    layout.truncate(self.max_lines, Some(0.0), &self.style);
170                }
171                self.min_content = Some(layout.size());
172                // Paint draws the lines the layout holds, so they go back to
173                // the width the label was last measured at, truncated as the
174                // label was last shown. A label measured nowhere yet goes
175                // back to max content, so the cache key the next measure
176                // reads never describes a state the label was not left in.
177                self.shape_and_show(painted.unwrap_or(None));
178            }
179            return self.min_content;
180        }
181        let max_width = wrap_width(input.available_width);
182        if self.broken_width != Some(max_width) {
183            self.shape_and_show(max_width);
184        }
185        let layout = self
186            .layout
187            .as_ref()
188            .expect("the layout was just built or is cached");
189        Some(layout.size())
190    }
191
192    /// Appends one [`PaintOp::Glyphs`] per run of the current layout.
193    ///
194    /// The runs are rebuilt when they were invalidated, when the node paints
195    /// at another position, or when the resolved text color changed, because
196    /// glyph positions and colors are baked into them. Each run carries the
197    /// color the node's text color or the label's style resolves to under the
198    /// current theme. When that color is a token, resolving it marks the node
199    /// to repaint on a theme switch, and a literal color does not.
200    ///
201    /// # Panics
202    ///
203    /// Panics when the glyph runs are already borrowed on this thread, which
204    /// the paint pass cannot do: it paints one node at a time and a widget
205    /// never paints its own node again from inside the call.
206    fn paint(&self, cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
207        let Some(layout) = &self.layout else {
208            return;
209        };
210        let color = self.text_color(cx);
211        let origin = cx.rect.origin();
212        let mut cache = self.runs.borrow_mut();
213        let rebuild = match &*cache {
214            Some(cached) => cached.origin != origin || cached.color != color,
215            None => true,
216        };
217        if rebuild {
218            // The color is baked into the runs here rather than into the
219            // layout, so a color change repaints the label without laying
220            // its text out again.
221            let runs = layout
222                .glyph_runs_at(origin, color)
223                .into_iter()
224                .map(Rc::new)
225                .collect();
226            *cache = Some(CachedRuns {
227                origin,
228                color,
229                runs,
230            });
231        }
232        if let Some(cached) = cache.as_ref() {
233            out.extend(cached.runs.iter().cloned().map(PaintOp::Glyphs));
234        }
235    }
236}
237
238impl Label {
239    /// Shapes [`Label::text`] with [`Label::style`] when no shaped layout is
240    /// kept, breaks its lines at `max_width`, and cuts the lines to
241    /// [`Label::max_lines`].
242    ///
243    /// Both outcomes change the lines, so the cached glyph runs, which bake
244    /// the line positions in, are dropped here.
245    fn shape_and_show(&mut self, max_width: Option<f32>) {
246        self.shape_and_break(max_width);
247        if let Some(layout) = &mut self.layout {
248            layout.truncate(self.max_lines, max_width, &self.style);
249        }
250    }
251
252    /// Shapes [`Label::text`] with [`Label::style`] when no shaped layout is
253    /// kept, and breaks its lines at `max_width` without truncating them.
254    ///
255    /// The min-content measure breaks at every opportunity to read the
256    /// longest unbreakable word off the lines, which truncation would
257    /// shorten, so it asks for the untruncated break.
258    fn shape_and_break(&mut self, max_width: Option<f32>) {
259        match &mut self.layout {
260            Some(layout) => layout.rebreak(max_width),
261            None => {
262                self.layout = Some(TextLayout::new(&self.text, &self.style, max_width));
263            }
264        }
265        self.broken_width = Some(max_width);
266        *self.runs.borrow_mut() = None;
267    }
268
269    /// Returns the color the glyphs paint with: the node's text color when it
270    /// sets one, otherwise the label's style color. Both may name a theme
271    /// token, so both resolve against the theme that is current at paint
272    /// time.
273    fn text_color(&self, cx: &PaintCx<'_>) -> Color {
274        match &cx.style.paint.text_color {
275            Some(color) => color.resolve(cx.theme),
276            None => self.style.color.resolve(cx.theme),
277        }
278    }
279}
280
281/// Creates a label that shows `text`.
282///
283/// The text can be fixed or read from signals:
284///
285/// ```
286/// use smudge_core::reactive::{prelude::*, signal_local};
287/// use smudge_text::label;
288///
289/// let (name, set_name) = signal_local("World".to_owned());
290/// let greeting = label(move || format!("Hello, {}", name.get()));
291/// set_name.set("Smudge".to_owned());
292/// ```
293pub fn label(text: impl Into<Value<String>>) -> Element<Label> {
294    // The ambient style binding is installed first, so its update runs before
295    // the updates of the builder methods and an explicit field wins over it.
296    Element::new(Label::new(String::new()))
297        .bind(AmbientStyleKey, current_text_style, apply_ambient_style)
298        .bind(TextKey, text, set_text)
299}
300
301/// Builder methods that set the text style of a [`label`] element.
302///
303/// Import the trait to use them. The style types the methods name come from
304/// this crate, so nothing else is needed:
305///
306/// ```
307/// use smudge_text::FontWeight;
308/// use smudge_text::LabelExt;
309/// use smudge_text::label;
310///
311/// let element = label("Hello").text_size(18.0).weight(FontWeight::SEMI_BOLD);
312/// ```
313///
314/// Every method sets one field of the label's text style; the remaining
315/// fields come from the ambient style at mount. The color the glyphs paint
316/// with is a paint property of the node, set with
317/// [`text_color`](smudge_core::element::Element::text_color), which needs no
318/// import.
319pub trait LabelExt: Sized {
320    /// Sets the font size in logical pixels.
321    ///
322    /// The size changes what the label measures, so the node is laid out
323    /// again.
324    fn text_size(self, size: impl Into<Value<f32>>) -> Self;
325
326    /// Sets the weight the family is matched with.
327    ///
328    /// The weight can change what the label measures, so the node is laid out
329    /// again.
330    fn weight(self, weight: FontWeight) -> Self;
331
332    /// Sets the font family the text resolves to.
333    ///
334    /// The family can change what the label measures, so the node is laid out
335    /// again.
336    fn family(self, family: FontFamily<'static>) -> Self;
337
338    /// Caps the lines the label shows at `max_lines`.
339    ///
340    /// A layout that breaks into more lines, or whose last kept line is
341    /// wider than the label, keeps its first `max_lines` and ends the last
342    /// one with an ellipsis, cut back on a cluster boundary so the whole fits
343    /// the width the label wraps at. A width below the ellipsis's own advance
344    /// leaves it standing alone and past the width. Right-to-left text keeps
345    /// its first lines without an ellipsis, and so does text in fonts that
346    /// map neither an ellipsis nor a full stop. `None`, the default, shows
347    /// every line. The accessible value keeps the full text either way. The
348    /// cap can change what the label measures, so the node is laid out
349    /// again, without shaping the text again.
350    ///
351    /// ```
352    /// use smudge_text::LabelExt;
353    /// use smudge_text::label;
354    ///
355    /// let capped = label("Hello").max_lines(Some(2));
356    /// let uncapped = label("Hello").max_lines(None);
357    /// ```
358    fn max_lines(self, max_lines: impl Into<Value<Option<u32>>>) -> Self;
359}
360
361/// The key of a label's ambient text style binding.
362struct AmbientStyleKey;
363
364/// The key of a label's text binding.
365struct TextKey;
366
367/// The key of a label's font size binding.
368struct TextSizeKey;
369
370/// The key of a label's weight binding.
371struct WeightKey;
372
373/// The key of a label's family binding.
374struct FamilyKey;
375
376/// The key of a label's line cap binding.
377struct MaxLinesKey;
378
379impl LabelExt for Element<Label> {
380    fn text_size(self, size: impl Into<Value<f32>>) -> Self {
381        self.bind(TextSizeKey, size, set_text_size)
382    }
383
384    fn weight(self, weight: FontWeight) -> Self {
385        self.bind(WeightKey, Value::Static(weight), set_weight)
386    }
387
388    fn family(self, family: FontFamily<'static>) -> Self {
389        self.bind(FamilyKey, Value::Static(family), set_family)
390    }
391
392    fn max_lines(self, max_lines: impl Into<Value<Option<u32>>>) -> Self {
393        self.bind(MaxLinesKey, max_lines, set_max_lines)
394    }
395}
396
397/// Which fields of the text style an explicit write has set.
398///
399/// The ambient style is read once at mount and fills only the other fields,
400/// so an explicit size keeps its value when the ambient style differs.
401#[derive(Clone, Copy, Default)]
402struct ExplicitStyle {
403    /// The family was set by [`LabelExt::family`].
404    family: bool,
405    /// The size was set by [`LabelExt::text_size`].
406    size: bool,
407    /// The weight was set by [`LabelExt::weight`].
408    weight: bool,
409}
410
411/// Writes `text` into the label and relays it out when it changed.
412fn set_text(label: &mut Label, text: String) -> Dirty {
413    if label.text == text {
414        return Dirty::Clean;
415    }
416    label.text = text;
417    label.invalidate_layout();
418    Dirty::Layout
419}
420
421/// Writes `size` into the label's style and relays it out when it changed.
422fn set_text_size(label: &mut Label, size: f32) -> Dirty {
423    label.explicit.size = true;
424    if label.style.size == size {
425        return Dirty::Clean;
426    }
427    label.style.size = size;
428    label.invalidate_layout();
429    Dirty::Layout
430}
431
432/// Writes `weight` into the label's style and relays it out when it changed.
433fn set_weight(label: &mut Label, weight: FontWeight) -> Dirty {
434    label.explicit.weight = true;
435    if label.style.weight == weight {
436        return Dirty::Clean;
437    }
438    label.style.weight = weight;
439    label.invalidate_layout();
440    Dirty::Layout
441}
442
443/// Writes `family` into the label's style and relays it out when it changed.
444fn set_family(label: &mut Label, family: FontFamily<'static>) -> Dirty {
445    label.explicit.family = true;
446    if label.style.family == family {
447        return Dirty::Clean;
448    }
449    label.style.family = family;
450    label.invalidate_layout();
451    Dirty::Layout
452}
453
454/// Writes the line cap into the label and relays it out when it changed.
455///
456/// The shaped text stays: only its lines are broken and cut again.
457fn set_max_lines(label: &mut Label, max_lines: Option<u32>) -> Dirty {
458    if label.max_lines == max_lines {
459        return Dirty::Clean;
460    }
461    label.max_lines = max_lines;
462    label.broken_width = None;
463    label.min_content = None;
464    *label.runs.borrow_mut() = None;
465    Dirty::Layout
466}
467
468/// Fills the fields of the label's style that no explicit write has set from
469/// the ambient style, which is read once at mount.
470fn apply_ambient_style(label: &mut Label, mut ambient: TextStyle) -> Dirty {
471    let explicit = label.explicit;
472    if explicit.family {
473        ambient.family = label.style.family.clone();
474    }
475    if explicit.size {
476        ambient.size = label.style.size;
477    }
478    if explicit.weight {
479        ambient.weight = label.style.weight;
480    }
481    if ambient == label.style {
482        return Dirty::Clean;
483    }
484    label.style = ambient;
485    label.invalidate_layout();
486    Dirty::Layout
487}
488
489/// The maximum width a layout wraps at for the space offered on the x axis.
490///
491/// A definite space wraps the text at that width, a max-content request lays
492/// it out on one line, and a min-content request wraps it at every
493/// opportunity. `measure` answers min-content from its own cache before it
494/// gets here, so the last arm only keeps the mapping right for any other
495/// caller.
496fn wrap_width(space: AvailableSpace) -> Option<f32> {
497    match space {
498        AvailableSpace::Definite(width) => Some(width),
499        AvailableSpace::MaxContent => None,
500        AvailableSpace::MinContent => Some(0.0),
501    }
502}
503
504#[cfg(test)]
505#[path = "label_tests.rs"]
506mod tests;