Skip to main content

euv_chart/text/
impl.rs

1use super::*;
2
3/// Inherent implementation of [`TextShaper`].
4impl TextShaper {
5    /// Creates a shaper with the given measurer and atlas.
6    ///
7    /// # Arguments
8    ///
9    /// - `TextMeasurer` - A `TextMeasurer` parameter.
10    /// - `GlyphAtlas` - A `GlyphAtlas` parameter.
11    pub fn new(measurer: TextMeasurer, atlas: GlyphAtlas) -> Self {
12        Self { measurer, atlas }
13    }
14
15    /// Returns the measurer.
16    ///
17    /// # Returns
18    ///
19    /// - `&TextMeasurer` - A `TextMeasurer` value.
20    pub fn measurer(&self) -> &TextMeasurer {
21        self.get_measurer()
22    }
23
24    /// Returns the atlas.
25    ///
26    /// # Returns
27    ///
28    /// - `&GlyphAtlas` - A `GlyphAtlas` value.
29    pub fn atlas(&self) -> &GlyphAtlas {
30        self.get_atlas()
31    }
32
33    /// Returns the atlas as mutable, so a backend can upload it.
34    ///
35    /// # Returns
36    ///
37    /// - `&mut GlyphAtlas` - A `mut GlyphAtlas` value.
38    pub fn atlas_mut(&mut self) -> &mut GlyphAtlas {
39        self.get_mut_atlas()
40    }
41
42    /// Shapes a run of text.
43    ///
44    /// `x` and `y` are the anchor of the text; the alignment and the
45    /// vertical alignment of the style decide which part of the text
46    /// lands on the anchor.
47    ///
48    /// A text that carries rich-text markup — `{name|content}` — is
49    /// split into its runs, and each run is shaped with the style that
50    /// the name refers to.
51    ///
52    /// # Arguments
53    ///
54    /// - `&str` - Shared reference to a `str`.
55    /// - `&TextStyle` - Shared reference to a `TextStyle`.
56    /// - `f64` - A 64-bit float (`f64`).
57    ///
58    /// # Returns
59    ///
60    /// - `TextRun` - A `TextRun` value.
61    pub fn shape(&mut self, text: &str, style: &TextStyle, x: f64, y: f64) -> TextRun {
62        if !style.rich.is_empty() && text.contains('{') {
63            return self.shape_rich(text, style, x, y);
64        }
65        self.shape_plain(text, style, x, y)
66    }
67    /// Shapes a run of plain text.
68    ///
69    /// # Arguments
70    ///
71    /// - `&str` - Shared reference to a `str`.
72    /// - `&TextStyle` - Shared reference to a `TextStyle`.
73    /// - `f64` - A 64-bit float (`f64`).
74    ///
75    /// # Returns
76    ///
77    /// - `TextRun` - A `TextRun` value.
78    fn shape_plain(&mut self, text: &str, style: &TextStyle, x: f64, y: f64) -> TextRun {
79        let lines: Vec<&str> = text.split('\n').collect();
80        let font: String = style.font();
81        let mut quads: Vec<GlyphQuad> = Vec::new();
82        let mut widths: Vec<f64> = Vec::with_capacity(lines.len());
83        let mut ascent: f64 = 0.0_f64;
84        let mut descent: f64 = 0.0_f64;
85        let line_height: f64 = style.line_height_px();
86        for line in &lines {
87            let metrics: TextMetrics = self.get_measurer().measure(line, &font);
88            widths.push(metrics.width);
89            ascent = ascent.max(metrics.ascent);
90            descent = descent.max(metrics.descent);
91        }
92        let block_height: f64 = line_height * lines.len() as f64;
93        let top: f64 = match style.vertical_align {
94            TextVerticalAlign::Top => y,
95            TextVerticalAlign::Middle => y - block_height * 0.5,
96            TextVerticalAlign::Bottom => y - block_height,
97            TextVerticalAlign::Baseline => y - ascent,
98        };
99        let mut shaped_lines: Vec<TextLine> = Vec::with_capacity(lines.len());
100        for (index, line) in lines.iter().enumerate() {
101            let line_width: f64 = widths[index];
102            let left: f64 = match style.align {
103                TextAlign::Left => x,
104                TextAlign::Center => x - line_width * 0.5,
105                TextAlign::Right => x - line_width,
106            };
107            let baseline: f64 = top + line_height * index as f64 + ascent;
108            self.shape_line(line, &font, left, baseline, 0, &mut quads);
109            shaped_lines.push(TextLine {
110                text: (*line).to_string(),
111                left,
112                baseline,
113                width: line_width,
114            });
115        }
116        TextRun {
117            quads,
118            lines: shaped_lines,
119            width: widths.iter().copied().fold(0.0_f64, f64::max),
120            height: block_height,
121            ascent,
122            descent,
123            line_height,
124            line_count: lines.len(),
125        }
126    }
127    /// Shapes a run of rich text.
128    ///
129    /// Every run keeps its own font and colour; the runs of one line
130    /// share the line's baseline, so they sit next to each other the way
131    /// they would in a paragraph.
132    ///
133    /// # Arguments
134    ///
135    /// - `&str` - Shared reference to a `str`.
136    /// - `&TextStyle` - Shared reference to a `TextStyle`.
137    /// - `f64` - A 64-bit float (`f64`).
138    ///
139    /// # Returns
140    ///
141    /// - `TextRun` - A `TextRun` value.
142    fn shape_rich(&mut self, text: &str, style: &TextStyle, x: f64, y: f64) -> TextRun {
143        let runs: Vec<(Option<String>, String)> = split_rich_text(text);
144        let mut quads: Vec<GlyphQuad> = Vec::new();
145        let mut shaped_lines: Vec<TextLine> = Vec::new();
146        let mut total_width: f64 = 0.0_f64;
147        let mut ascent: f64 = 0.0_f64;
148        let mut descent: f64 = 0.0_f64;
149        let mut pen_x: f64 = x;
150        for (name, content) in runs {
151            let mut run_style: TextStyle = style.clone();
152            if let Some(name) = &name
153                && let Some((_, rich)) = style
154                    .rich
155                    .iter()
156                    .find(|(key, _): &&(String, RichTextStyle)| key == name)
157            {
158                if let Some(family) = &rich.family {
159                    run_style.family = family.clone();
160                }
161                if let Some(size) = rich.size {
162                    run_style.size = size;
163                }
164                if let Some(weight) = &rich.weight {
165                    run_style.weight = weight.clone();
166                }
167                if let Some(value) = &rich.style {
168                    run_style.style = value.clone();
169                }
170                if let Some(color) = rich.color {
171                    run_style.color = color;
172                }
173            }
174            let color: u32 = run_style.color.to_u32();
175            let font: String = run_style.font();
176            let metrics: TextMetrics = self.get_measurer().measure(&content, &font);
177            ascent = ascent.max(metrics.ascent);
178            descent = descent.max(metrics.descent);
179            total_width += metrics.width;
180            let baseline: f64 = y + metrics.ascent;
181            self.shape_line(&content, &font, pen_x, baseline, color, &mut quads);
182            shaped_lines.push(TextLine {
183                text: content,
184                left: pen_x,
185                baseline,
186                width: metrics.width,
187            });
188            pen_x += metrics.width;
189        }
190        let line_count: usize = shaped_lines.len();
191        TextRun {
192            quads,
193            lines: shaped_lines,
194            width: total_width,
195            height: ascent + descent,
196            ascent,
197            descent,
198            line_height: style.line_height_px(),
199            line_count,
200        }
201    }
202    /// Shapes one line of text into glyph quads.
203    ///
204    /// # Arguments
205    ///
206    /// - `&str` - Shared reference to a `str`.
207    /// - `f64` - A 64-bit float (`f64`).
208    /// - `u32` - A 32-bit unsigned integer (`u32`).
209    /// - `&mut Vec<GlyphQuad>` - Mutable reference to a `Vec<GlyphQuad>` (mutated in place).
210    fn shape_line(
211        &mut self,
212        line: &str,
213        font: &str,
214        left: f64,
215        baseline: f64,
216        color: u32,
217        quads: &mut Vec<GlyphQuad>,
218    ) {
219        let mut pen: f32 = left as f32;
220        for character in line.chars() {
221            let entry: GlyphEntry = self.get_mut_atlas().glyph(character, font);
222            if entry.width > 0.0 && entry.height > 0.0 {
223                quads.push(GlyphQuad {
224                    x: pen + entry.bearing_x,
225                    y: baseline as f32 - entry.bearing_y,
226                    width: entry.width,
227                    height: entry.height,
228                    u: entry.u,
229                    v: entry.v,
230                    u_size: entry.width / self.get_atlas().width() as f32,
231                    v_size: entry.height / self.get_atlas().height() as f32,
232                    color,
233                });
234            }
235            pen += entry.advance;
236        }
237    }
238}
239
240/// Inherent implementation of [`GlyphAtlas`].
241impl GlyphAtlas {
242    /// Creates an atlas of the given size, in pixels.
243    ///
244    /// Returns `None` when the host has no DOM.
245    ///
246    /// # Arguments
247    ///
248    /// - `u32` - A 32-bit unsigned integer (`u32`).
249    ///
250    /// # Returns
251    ///
252    /// - `Option<Self>` - `Some(...)` on success, `None` otherwise.
253    pub fn new(width: u32, height: u32) -> Option<Self> {
254        let document: Document = web_sys::window()?.document()?;
255        let canvas: HtmlCanvasElement = document
256            .create_element(CANVAS_TAG)
257            .ok()?
258            .dyn_into::<HtmlCanvasElement>()
259            .ok()?;
260        canvas.set_width(width);
261        canvas.set_height(height);
262        let context: CanvasRenderingContext2d = canvas
263            .get_context("2d")
264            .ok()
265            .flatten()?
266            .dyn_into::<CanvasRenderingContext2d>()
267            .ok()?;
268        Some(Self {
269            canvas,
270            context,
271            entries: HashMap::new(),
272            cursor_x: 0.0,
273            cursor_y: 0.0,
274            shelf_height: 0.0,
275            width,
276            height,
277            dirty: false,
278        })
279    }
280
281    /// Returns the canvas the glyphs are rasterised into.
282    ///
283    /// The GPU backends upload this canvas straight into a texture.
284    ///
285    /// # Returns
286    ///
287    /// - `&HtmlCanvasElement` - A `HtmlCanvasElement` value.
288    pub fn canvas(&self) -> &HtmlCanvasElement {
289        self.get_canvas()
290    }
291
292    /// Returns the width of the atlas, in pixels.
293    ///
294    /// # Returns
295    ///
296    /// - `u32` - A 32-bit unsigned integer.
297    pub fn width(&self) -> u32 {
298        self.get_width()
299    }
300
301    /// Returns the height of the atlas, in pixels.
302    ///
303    /// # Returns
304    ///
305    /// - `u32` - A 32-bit unsigned integer.
306    pub fn height(&self) -> u32 {
307        self.get_height()
308    }
309
310    /// Returns `true` when the atlas changed since the last upload.
311    ///
312    /// # Returns
313    ///
314    /// - `bool` - A boolean.
315    pub fn is_dirty(&self) -> bool {
316        self.get_dirty()
317    }
318
319    /// Marks the atlas as uploaded.
320    pub fn mark_clean(&mut self) {
321        self.set_dirty(false);
322    }
323
324    /// Returns the number of rasterised glyphs.
325    ///
326    /// # Returns
327    ///
328    /// - `usize` - A non-negative integer.
329    pub fn len(&self) -> usize {
330        self.get_entries().len()
331    }
332
333    /// Returns `true` when the atlas holds no glyphs.
334    ///
335    /// # Returns
336    ///
337    /// - `bool` - A boolean.
338    pub fn is_empty(&self) -> bool {
339        self.get_entries().is_empty()
340    }
341
342    /// Returns the entry for the given character and font, rasterising it
343    /// on first use.
344    ///
345    /// # Arguments
346    ///
347    /// - `char` - A `char` parameter.
348    /// - `&str` - Shared reference to a `str`.
349    ///
350    /// # Returns
351    ///
352    /// - `GlyphEntry` - A `GlyphEntry` value.
353    pub fn glyph(&mut self, character: char, font: &str) -> GlyphEntry {
354        let key: GlyphKey = GlyphKey {
355            character,
356            font: font.to_string(),
357        };
358        if let Some(entry) = self.get_entries().get(&key) {
359            return *entry;
360        }
361        let entry: GlyphEntry = self.rasterize(character, font);
362        self.get_mut_entries().insert(key, entry);
363        entry
364    }
365
366    /// Rasterises one glyph into the atlas.
367    ///
368    /// # Arguments
369    ///
370    /// - `char` - A `char` parameter.
371    /// - `&str` - Shared reference to a `str`.
372    ///
373    /// # Returns
374    ///
375    /// - `GlyphEntry` - A `GlyphEntry` value.
376    fn rasterize(&mut self, character: char, font: &str) -> GlyphEntry {
377        self.get_context().set_font(font);
378        let Ok(metrics) = self.get_context().measure_text(&character.to_string()) else {
379            return GlyphEntry {
380                u: 0.0,
381                v: 0.0,
382                width: 0.0,
383                height: 0.0,
384                bearing_x: 0.0,
385                bearing_y: 0.0,
386                advance: 0.0,
387            };
388        };
389        let advance: f32 = metrics.width() as f32;
390        let left: f32 = metrics.actual_bounding_box_left() as f32;
391        let right: f32 = metrics.actual_bounding_box_right() as f32;
392        let ascent: f32 = metrics.actual_bounding_box_ascent() as f32;
393        let descent: f32 = metrics.actual_bounding_box_descent() as f32;
394        let glyph_width: f32 = (left + right).ceil().max(0.0);
395        let glyph_height: f32 = (ascent + descent).ceil().max(0.0);
396        // A space has no ink, so it occupies no space in the atlas.
397        if glyph_width <= 0.0 || glyph_height <= 0.0 {
398            return GlyphEntry {
399                u: 0.0,
400                v: 0.0,
401                width: 0.0,
402                height: 0.0,
403                bearing_x: 0.0,
404                bearing_y: 0.0,
405                advance,
406            };
407        }
408        let padding: f32 = GLYPH_PADDING;
409        let cell_width: f32 = glyph_width + padding * 2.0;
410        let cell_height: f32 = glyph_height + padding * 2.0;
411        if self.get_cursor_x() + cell_width > self.get_width() as f32 {
412            self.set_cursor_x(0.0);
413            self.set_cursor_y(self.get_cursor_y() + self.get_shelf_height());
414            self.set_shelf_height(0.0);
415        }
416        if self.get_cursor_y() + cell_height > self.get_height() as f32 {
417            // The atlas is full. Starting over is cheaper than a
418            // compaction pass, and the atlas only fills up when a chart
419            // uses more distinct glyphs than fit — at which point the
420            // chart is redrawn from scratch anyway.
421            self.reset();
422        }
423        let x: f32 = self.get_cursor_x();
424        let y: f32 = self.get_cursor_y();
425        self.set_cursor_x(self.get_cursor_x() + cell_width);
426        self.set_shelf_height(self.get_shelf_height().max(cell_height));
427        // The glyph is drawn with its pen at the cell's left padding and
428        // its baseline at the cell's bottom padding.
429        let pen_x: f32 = x + padding + left;
430        let baseline: f32 = y + padding + ascent;
431        self.get_context().set_font(font);
432        self.get_context().set_fill_style_str("#ffffff");
433        let _: Result<(), JsValue> =
434            self.get_context()
435                .fill_text(&character.to_string(), pen_x as f64, baseline as f64);
436        self.set_dirty(true);
437        GlyphEntry {
438            u: x / self.get_width() as f32,
439            v: y / self.get_height() as f32,
440            width: glyph_width,
441            height: glyph_height,
442            bearing_x: -(left + padding),
443            bearing_y: ascent + padding,
444            advance,
445        }
446    }
447
448    /// Empties the atlas and rewinds the packer.
449    fn reset(&mut self) {
450        self.get_context()
451            .clear_rect(0.0, 0.0, self.get_width() as f64, self.get_height() as f64);
452        self.get_mut_entries().clear();
453        self.set_cursor_x(0.0);
454        self.set_cursor_y(0.0);
455        self.set_shelf_height(0.0);
456        self.set_dirty(true);
457    }
458}
459
460/// Implements `impl std::fmt::Debug for TextMeasurer`.
461impl Debug for TextMeasurer {
462    /// Formats the [`TextMeasurer`] via the supplied formatter.
463    ///
464    /// # Arguments
465    ///
466    /// - `&mut std::fmt::Formatter<'_>` - The formatter receiving the formatted output.
467    ///
468    /// # Returns
469    ///
470    /// - `std::fmt::Result` - A `std::fmt::Result` value.
471    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        formatter
473            .debug_struct("TextMeasurer")
474            .field("cached", &self.get_cache().borrow().len())
475            .finish()
476    }
477}
478
479/// Inherent implementation of [`TextMeasurer`].
480impl TextMeasurer {
481    /// Creates a measurer backed by a fresh offscreen canvas.
482    ///
483    /// Returns `None` when the host has no DOM, which is the case for a
484    /// native build.
485    ///
486    /// # Returns
487    ///
488    /// - `Option<Self>` - `Some(...)` on success, `None` otherwise.
489    pub fn new() -> Option<Self> {
490        let document: Document = web_sys::window()?.document()?;
491        let canvas: HtmlCanvasElement = document
492            .create_element(CANVAS_TAG)
493            .ok()?
494            .dyn_into::<HtmlCanvasElement>()
495            .ok()?;
496        canvas.set_width(1);
497        canvas.set_height(1);
498        let context: CanvasRenderingContext2d = canvas
499            .get_context("2d")
500            .ok()
501            .flatten()?
502            .dyn_into::<CanvasRenderingContext2d>()
503            .ok()?;
504        Some(Self {
505            context: Some(context),
506            cache: Rc::new(RefCell::new(HashMap::new())),
507        })
508    }
509
510    /// Creates a measurer that estimates the text instead of measuring
511    /// it.
512    ///
513    /// The estimate is the average advance of a Latin character, which is
514    /// close enough for a layout that is not going to be drawn — a
515    /// server-side render, or a native build.
516    pub fn approximate() -> Self {
517        Self {
518            context: None,
519            cache: Rc::new(RefCell::new(HashMap::new())),
520        }
521    }
522
523    /// Measures a run of text with the given CSS `font` shorthand.
524    ///
525    /// # Arguments
526    ///
527    /// - `&str` - Shared reference to a `str`.
528    ///
529    /// # Returns
530    ///
531    /// - `TextMetrics` - A `TextMetrics` value.
532    pub fn measure(&self, text: &str, font: &str) -> TextMetrics {
533        if text.is_empty() {
534            return TextMetrics::default();
535        }
536        let key: (String, String) = (text.to_string(), font.to_string());
537        if let Some(metrics) = self.get_cache().borrow().get(&key) {
538            return *metrics;
539        }
540        let metrics: TextMetrics = self.measure_uncached(text, font);
541        self.get_cache().borrow_mut().insert(key, metrics);
542        metrics
543    }
544
545    /// Measures a run of text, bypassing the cache.
546    ///
547    /// # Arguments
548    ///
549    /// - `&str` - Shared reference to a `str`.
550    ///
551    /// # Returns
552    ///
553    /// - `TextMetrics` - A `TextMetrics` value.
554    fn measure_uncached(&self, text: &str, font: &str) -> TextMetrics {
555        let size: f64 = font_size_of(font);
556        let Some(context) = self.try_get_context() else {
557            return approximate_metrics(text, size);
558        };
559        context.set_font(font);
560        let Ok(metrics) = context.measure_text(text) else {
561            return approximate_metrics(text, size);
562        };
563        let ascent: f64 = metrics.actual_bounding_box_ascent();
564        let descent: f64 = metrics.actual_bounding_box_descent();
565        TextMetrics {
566            width: metrics.width(),
567            height: ascent + descent,
568            ascent,
569            descent,
570            line_height: size * 1.2,
571        }
572    }
573
574    /// Returns the number of cached measurements.
575    ///
576    /// # Returns
577    ///
578    /// - `usize` - A non-negative integer.
579    pub fn cache_size(&self) -> usize {
580        self.get_cache().borrow().len()
581    }
582
583    /// Empties the measurement cache.
584    pub fn clear_cache(&self) {
585        self.get_cache().borrow_mut().clear();
586    }
587}