Skip to main content

telar_renderer_core/
metrics.rs

1use std::sync::{Arc, RwLock};
2
3use crate::{TextRun, TextStyle};
4
5/// How much room a string takes — all a widget tree needs to know about text before anything is drawn.
6///
7/// A seam rather than a call into the shaper, because the answer belongs to the target: on a raster surface it is
8/// cosmic-text's shaped advance, on a terminal it is `unicode-width` times a cell.
9pub trait TextMetrics: Send + Sync + 'static {
10    /// The logical `(width, height)` of `text` wrapped to `max_width` under `style`. Weight, slant, `max_lines`
11    /// and `ellipsis` all change the extent, so measuring and drawing must be handed the same style.
12    fn measure(&self, text: &str, max_width: f32, style: &TextStyle) -> (f32, f32);
13
14    /// The same for a paragraph of styled runs, each measured against `base` wherever it overrides nothing.
15    fn measure_runs(&self, runs: &[TextRun], max_width: f32, base: &TextStyle) -> (f32, f32);
16
17    /// The drawn glyph extent `(ink_top, ink_height)` from the top of the layout rect, so a widget can optically
18    /// centre a short run against something that is not text.
19    fn ink_bounds(&self, text: &str, max_width: f32, style: &TextStyle) -> (f32, f32);
20
21    /// The height of one line at `font_size`. A question rather than a constant because a terminal's line height
22    /// is a cell, not a multiple of a font size.
23    fn line_height(&self, font_size: f32) -> f32;
24}
25
26static TEXT_METRICS: RwLock<Option<Arc<dyn TextMetrics>>> = RwLock::new(None);
27
28/// Installs the process-wide text measurer, replacing whatever was there.
29pub fn set_text_metrics(metrics: impl TextMetrics) {
30    *TEXT_METRICS.write().expect("text metrics lock") = Some(Arc::new(metrics));
31}
32
33/// Installs `metrics` only if nothing is installed yet, and reports whether it took.
34///
35/// What a runtime uses, so a frontend that already installed metrics of its own — cells for a terminal, a fixed
36/// advance for a test — keeps them when the raster default is offered later.
37pub fn set_default_text_metrics(metrics: impl TextMetrics) -> bool {
38    let mut slot = TEXT_METRICS.write().expect("text metrics lock");
39    if slot.is_some() {
40        return false;
41    }
42    *slot = Some(Arc::new(metrics));
43    true
44}
45
46fn metrics() -> Arc<dyn TextMetrics> {
47    TEXT_METRICS
48        .read()
49        .expect("text metrics lock")
50        .as_ref()
51        .cloned()
52        .expect(
53            "no TextMetrics installed, so nothing can size text: install one with \
54             renderer_core::set_text_metrics (renderer_text::ShaperMetrics is the raster default, and \
55             renderer_text::set_measure_font_config installs it for you)",
56        )
57}
58
59/// Measures the logical `(width, height)` of `text` wrapped to `max_width`. See [`TextMetrics::measure`].
60pub fn measure_text(text: &str, max_width: f32, style: &TextStyle) -> (f32, f32) {
61    metrics().measure(text, max_width, style)
62}
63
64/// Measures a paragraph of styled runs. See [`TextMetrics::measure_runs`].
65pub fn measure_rich_text(runs: &[TextRun], max_width: f32, base: &TextStyle) -> (f32, f32) {
66    metrics().measure_runs(runs, max_width, base)
67}
68
69/// The text's drawn glyph extent `(ink_top, ink_height)`. See [`TextMetrics::ink_bounds`].
70pub fn measure_ink_bounds(text: &str, max_width: f32, style: &TextStyle) -> (f32, f32) {
71    metrics().ink_bounds(text, max_width, style)
72}
73
74/// The height of one line of text at `font_size`. See [`TextMetrics::line_height`].
75pub fn line_height(font_size: f32) -> f32 {
76    metrics().line_height(font_size)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    struct Fixed(f32);
84
85    impl TextMetrics for Fixed {
86        fn measure(&self, text: &str, _max_width: f32, _style: &TextStyle) -> (f32, f32) {
87            (text.chars().count() as f32 * self.0, self.0)
88        }
89        fn measure_runs(
90            &self,
91            _runs: &[TextRun],
92            _max_width: f32,
93            _base: &TextStyle,
94        ) -> (f32, f32) {
95            (0.0, self.0)
96        }
97        fn ink_bounds(&self, _text: &str, _max_width: f32, _style: &TextStyle) -> (f32, f32) {
98            (0.0, self.0)
99        }
100        fn line_height(&self, _font_size: f32) -> f32 {
101            self.0
102        }
103    }
104
105    // One test rather than three: the installed measurer is process-wide, so separate tests would race over it.
106    #[test]
107    fn a_frontends_own_metrics_survive_the_runtimes_default() {
108        let style = TextStyle::new(14.0, crate::Color::BLACK);
109
110        assert!(
111            set_default_text_metrics(Fixed(10.0)),
112            "the first default install takes"
113        );
114        assert_eq!(measure_text("ab", 1.0e6, &style).0, 20.0);
115
116        assert!(
117            !set_default_text_metrics(Fixed(1.0)),
118            "a second default must not displace metrics already in force — that is the whole point of it"
119        );
120        assert_eq!(line_height(14.0), 10.0);
121
122        set_text_metrics(Fixed(2.0));
123        assert_eq!(
124            line_height(14.0),
125            2.0,
126            "an explicit install replaces, so a frontend can change its mind"
127        );
128    }
129}