Skip to main content

ferroui_base/rendering/composition/server/
diagnostic_text_renderer.rs

1use crate::media::{GlyphRun, GlyphTypeface, IBrush, IImmutableBrush, Typeface};
2use crate::platform::{IDrawingContextImpl, IGlyphRunImpl};
3use crate::utilities::ReadOnlyMemory;
4use crate::{Matrix, Rect, Size};
5use std::rc::Rc;
6
7const FIRST_CHAR: u16 = 32;
8const LAST_CHAR: u16 = 126;
9
10/// A glyph run as needed by [`DiagnosticTextRenderer`].
11#[derive(Clone)]
12pub struct DiagnosticGlyphRun {
13    /// The layout bounds of the run (the C# `GlyphRun.Bounds`: advance width
14    /// by line height), which is what text is measured and advanced with.
15    /// This is not the ink bounding box reported by
16    /// [`IGlyphRunImpl::bounds`].
17    pub bounds: Rect,
18    /// The platform glyph run that gets drawn.
19    pub platform_impl: std::sync::Arc<dyn IGlyphRunImpl>,
20}
21
22/// The seam between [`DiagnosticTextRenderer`] and the text stack.
23///
24/// The C# constructor takes a glyph typeface and an em size and builds one
25/// `GlyphRun` per printable ASCII character from them; `Shape` builds a
26/// glyph run of a whole text from the same typeface. Those steps are
27/// delegated to an implementation of this trait, which captures the glyph
28/// typeface and the font rendering em size:
29/// [`GlyphTypefaceDiagnosticGlyphRunSource`] is the implementation over the
30/// text stack, used by [`DiagnosticTextRenderer::from_glyph_typeface`].
31pub trait IDiagnosticGlyphRunSource {
32    /// Creates the glyph run consisting of the single glyph the typeface
33    /// maps `character` to. Called once for every character from `' '`
34    /// (32) to `'~'` (126), in order.
35    fn create_glyph_run(&self, character: char) -> DiagnosticGlyphRun;
36
37    /// Creates the glyph run of `text`, one glyph per UTF-16 code unit as
38    /// the typeface maps it. `text` is printable ASCII and not empty.
39    fn create_text_glyph_run(&self, text: &[u16]) -> DiagnosticGlyphRun;
40
41    /// The line spacing of the typeface scaled to the font rendering em
42    /// size (`Metrics.LineSpacing * fontRenderingEmSize / Metrics.DesignEmHeight`).
43    fn line_height(&self) -> f64;
44}
45
46/// Builds the glyph runs of a [`DiagnosticTextRenderer`] from a glyph
47/// typeface and a font rendering em size, as the C# constructor does.
48pub struct GlyphTypefaceDiagnosticGlyphRunSource {
49    glyph_typeface: Rc<GlyphTypeface>,
50    font_rendering_em_size: f64,
51    /// The printable ASCII characters; every run refers to its character
52    /// in this buffer.
53    chars: ReadOnlyMemory<u16>,
54}
55
56impl GlyphTypefaceDiagnosticGlyphRunSource {
57    pub fn new(glyph_typeface: Rc<GlyphTypeface>, font_rendering_em_size: f64) -> Self {
58        let chars = ReadOnlyMemory::from_vec((FIRST_CHAR..=LAST_CHAR).collect());
59        Self { glyph_typeface, font_rendering_em_size, chars }
60    }
61}
62
63impl IDiagnosticGlyphRunSource for GlyphTypefaceDiagnosticGlyphRunSource {
64    fn create_glyph_run(&self, character: char) -> DiagnosticGlyphRun {
65        let code_point = character as u32;
66        let effective_char =
67            if (FIRST_CHAR as u32..=LAST_CHAR as u32).contains(&code_point) { code_point as u16 } else { FIRST_CHAR };
68        let index = (effective_char - FIRST_CHAR) as usize;
69        let glyph = self.glyph_typeface.character_to_glyph_map().get_glyph(character as i32);
70        let run = GlyphRun::from_glyph_indices(
71            self.glyph_typeface.clone(),
72            self.font_rendering_em_size,
73            self.chars.slice(index, 1),
74            &[glyph],
75            None,
76            0,
77        );
78        DiagnosticGlyphRun { bounds: run.bounds(), platform_impl: run.platform_impl() }
79    }
80
81    fn create_text_glyph_run(&self, text: &[u16]) -> DiagnosticGlyphRun {
82        let character_to_glyph_map = self.glyph_typeface.character_to_glyph_map();
83        let glyphs: Vec<u16> = text.iter().map(|&c| character_to_glyph_map.get_glyph(c as i32)).collect();
84        let run = GlyphRun::from_glyph_indices(
85            self.glyph_typeface.clone(),
86            self.font_rendering_em_size,
87            ReadOnlyMemory::from_vec(text.to_vec()),
88            &glyphs,
89            None,
90            0,
91        );
92        DiagnosticGlyphRun { bounds: run.bounds(), platform_impl: run.platform_impl() }
93    }
94
95    fn line_height(&self) -> f64 {
96        let metrics = self.glyph_typeface.metrics();
97        metrics.line_spacing() as f64 * self.font_rendering_em_size / metrics.design_em_height as f64
98    }
99}
100
101/// A class used to render diagnostic strings (only!), with caching of ASCII glyph runs.
102/// Digits are laid out in equal-width cells so rapidly changing values do not make the text jump around.
103pub struct DiagnosticTextRenderer {
104    /// Labels shaped once and shared, by text ([`shape_label`](Self::shape_label)).
105    shaped_labels: std::cell::RefCell<Vec<(&'static str, Rc<ShapedText>)>>,
106    glyph_run_source: Rc<dyn IDiagnosticGlyphRunSource>,
107    runs: Vec<DiagnosticGlyphRun>,
108    digit_offsets: [f64; 10],
109    line_height: f64,
110    digit_cell_width: f64,
111}
112
113impl DiagnosticTextRenderer {
114    pub fn get_max_height(&self) -> f64 {
115        self.line_height
116    }
117
118    /// The width of the cell every digit is centered in.
119    pub fn digit_cell_width(&self) -> f64 {
120        self.digit_cell_width
121    }
122
123    pub fn new(glyph_run_source: Rc<dyn IDiagnosticGlyphRunSource>) -> Self {
124        let line_height = glyph_run_source.line_height();
125
126        let mut runs = Vec::with_capacity((LAST_CHAR - FIRST_CHAR + 1) as usize);
127        for c in FIRST_CHAR..=LAST_CHAR {
128            runs.push(glyph_run_source.create_glyph_run(c as u8 as char));
129        }
130
131        // Digits share the width of the widest one so changing values do not shift the text that follows
132        let mut digit_cell_width = 0.0_f64;
133        for c in b'0'..=b'9' {
134            digit_cell_width = digit_cell_width.max(runs[(c as u16 - FIRST_CHAR) as usize].bounds.width);
135        }
136
137        let mut digit_offsets = [0.0; 10];
138        for (digit, offset) in digit_offsets.iter_mut().enumerate() {
139            *offset = (digit_cell_width - runs[(b'0' as u16 + digit as u16 - FIRST_CHAR) as usize].bounds.width) / 2.0;
140        }
141
142        Self { glyph_run_source, runs, digit_offsets, line_height, digit_cell_width, shaped_labels: Default::default() }
143    }
144
145    /// Creates a renderer whose glyph runs are built from `glyph_typeface`
146    /// at `font_rendering_em_size` (the C# constructor).
147    pub fn from_glyph_typeface(glyph_typeface: Rc<GlyphTypeface>, font_rendering_em_size: f64) -> Self {
148        Self::new(Rc::new(GlyphTypefaceDiagnosticGlyphRunSource::new(glyph_typeface, font_rendering_em_size)))
149    }
150
151    /// Creates the renderer the compositor's debug overlays use: the glyph
152    /// typeface of the default typeface at an em size of 12.
153    pub fn create_default() -> Self {
154        Self::from_glyph_typeface(Typeface::default_typeface().glyph_typeface(), 12.0)
155    }
156
157    fn effective_char(c: u16) -> u16 {
158        if (FIRST_CHAR..=LAST_CHAR).contains(&c) {
159            c
160        } else {
161            b' ' as u16
162        }
163    }
164
165    fn run_for(&self, c: u16) -> &DiagnosticGlyphRun {
166        &self.runs[(Self::effective_char(c) - FIRST_CHAR) as usize]
167    }
168
169    fn is_digit(c: u16) -> bool {
170        (b'0' as u16..=b'9' as u16).contains(&c)
171    }
172
173    /// Shapes `text` into a single glyph run that can be drawn with
174    /// [`draw_shaped_text`](Self::draw_shaped_text) without any per-frame
175    /// measuring or shaping. Every UTF-16 code unit outside the printable
176    /// ASCII range is shaped as a space. Glyphs advance by their natural
177    /// width, so digits are not placed in cells; use
178    /// [`draw_digits`](Self::draw_digits) for values that change.
179    pub fn shape(&self, text: &str) -> ShapedText {
180        let chars: Vec<u16> = text.encode_utf16().map(Self::effective_char).collect();
181
182        if chars.is_empty() {
183            return ShapedText { glyph_run: None, width: 0.0 };
184        }
185
186        ShapedText::new(self.glyph_run_source.create_text_glyph_run(&chars))
187    }
188
189    /// The label `text` shaped by [`shape`](Self::shape), shaped the first
190    /// time and shared after that. Not in the reference: the port shapes the
191    /// fixed labels of the FPS counter on the UI thread with this, where the
192    /// glyph runs can be created (`ServerCompositor::diagnostic_text_renderer`).
193    pub fn shape_label(&self, text: &'static str) -> Rc<ShapedText> {
194        if let Some((_, shaped)) = self.shaped_labels.borrow().iter().find(|(label, _)| *label == text) {
195            return shaped.clone();
196        }
197        let shaped = Rc::new(self.shape(text));
198        self.shaped_labels.borrow_mut().push((text, shaped.clone()));
199        shaped
200    }
201
202    /// Draws text shaped by [`shape`](Self::shape) at the origin of the
203    /// current transform of `context`.
204    pub fn draw_shaped_text(
205        &self,
206        context: &mut dyn IDrawingContextImpl,
207        text: &ShapedText,
208        foreground: &dyn IImmutableBrush,
209    ) {
210        if let Some(glyph_run) = &text.glyph_run {
211            let foreground: &dyn IBrush = foreground;
212            context.draw_glyph_run(Some(foreground), &*glyph_run.platform_impl);
213        }
214    }
215
216    /// Draws a non-negative `value` zero padded to `digit_count` digits, each
217    /// in its own [`digit_cell_width`](Self::digit_cell_width) wide cell,
218    /// without formatting a string. Values that do not fit are clamped to the
219    /// largest representable value.
220    pub fn draw_digits(
221        &self,
222        context: &mut dyn IDrawingContextImpl,
223        value: i32,
224        digit_count: i32,
225        foreground: &dyn IImmutableBrush,
226    ) {
227        let foreground: &dyn IBrush = foreground;
228
229        let mut divisor = 1_i64;
230        for _ in 1..digit_count {
231            divisor *= 10;
232        }
233
234        let mut remaining = (value as i64).clamp(0, divisor * 10 - 1);
235
236        for i in 0..digit_count {
237            let digit = (remaining / divisor) as usize;
238            remaining %= divisor;
239            divisor /= 10;
240
241            let run = &self.runs[(b'0' as u16 + digit as u16 - FIRST_CHAR) as usize];
242            let old_transform = context.transform();
243            context.set_transform(
244                Matrix::create_translation(i as f64 * self.digit_cell_width + self.digit_offsets[digit], 0.0)
245                    * old_transform,
246            );
247            context.draw_glyph_run(Some(foreground), &*run.platform_impl);
248            context.set_transform(old_transform);
249        }
250    }
251
252    /// Measures `text`. Every UTF-16 code unit outside the printable ASCII
253    /// range is measured as a space.
254    pub fn measure_ascii_text(&self, text: &str) -> Size {
255        let mut width = 0.0;
256
257        for c in text.encode_utf16() {
258            let run = self.run_for(c);
259            width += if Self::is_digit(c) { self.digit_cell_width } else { run.bounds.width };
260        }
261
262        Size::new(width, self.line_height)
263    }
264
265    /// Draws `text` at the origin of the current transform of `context`.
266    /// Every UTF-16 code unit outside the printable ASCII range is drawn as
267    /// a space.
268    pub fn draw_ascii_text(&self, context: &mut dyn IDrawingContextImpl, text: &str, foreground: &dyn IImmutableBrush) {
269        let mut offset = 0.0;
270        let foreground: &dyn IBrush = foreground;
271
272        for c in text.encode_utf16() {
273            let char_is_number = Self::is_digit(c);
274            let run = self.run_for(c);
275
276            // Center digits inside the fixed-width digit cell
277            let centering_offset = if char_is_number { (self.digit_cell_width - run.bounds.width) / 2.0 } else { 0.0 };
278
279            let old_transform = context.transform();
280            context.set_transform(Matrix::create_translation(offset + centering_offset, 0.0) * old_transform);
281            context.draw_glyph_run(Some(foreground), &*run.platform_impl);
282            context.set_transform(old_transform);
283
284            offset += if char_is_number { self.digit_cell_width } else { run.bounds.width };
285        }
286    }
287}
288
289/// A piece of text shaped once by [`DiagnosticTextRenderer::shape`].
290pub struct ShapedText {
291    /// The glyph run of the text; `None` for an empty text, which draws
292    /// nothing.
293    glyph_run: Option<DiagnosticGlyphRun>,
294    width: f64,
295}
296
297impl ShapedText {
298    /// `ShapedText(GlyphRun glyphRun)`: the text of `glyph_run`, as wide as
299    /// its bounds.
300    pub(crate) fn new(glyph_run: DiagnosticGlyphRun) -> Self {
301        Self { width: glyph_run.bounds.width, glyph_run: Some(glyph_run) }
302    }
303
304    pub fn width(&self) -> f64 {
305        self.width
306    }
307
308    pub fn dispose(&self) {
309        if let Some(glyph_run) = &self.glyph_run {
310            glyph_run.platform_impl.dispose();
311        }
312    }
313}
314
315#[cfg(test)]
316pub(crate) mod tests {
317    use super::super::dirty_rects::test_mocks::MockDrawingContext;
318    use super::super::FpsCounter;
319    use super::*;
320    use crate::media::Brushes;
321    use crate::Point;
322    use std::cell::RefCell;
323    use std::time::Duration;
324
325    pub(crate) struct MockGlyphRunImpl(pub char);
326
327    impl IGlyphRunImpl for MockGlyphRunImpl {
328        fn font_rendering_em_size(&self) -> f64 {
329            // Lets the mock drawing context tell the glyphs apart.
330            self.0 as u32 as f64
331        }
332        fn baseline_origin(&self) -> Point {
333            Point::default()
334        }
335        fn bounds(&self) -> Rect {
336            Rect::default()
337        }
338        fn get_intersections(&self, _: f32, _: f32) -> Vec<f32> {
339            Vec::new()
340        }
341        fn dispose(&self) {}
342        fn as_any(&self) -> &dyn std::any::Any {
343            self
344        }
345    }
346
347    /// The glyph run of a shaped text; the mock drawing context logs it with
348    /// its text.
349    pub(crate) struct MockTextGlyphRunImpl(pub String);
350
351    impl IGlyphRunImpl for MockTextGlyphRunImpl {
352        fn font_rendering_em_size(&self) -> f64 {
353            0.0
354        }
355        fn baseline_origin(&self) -> Point {
356            Point::default()
357        }
358        fn bounds(&self) -> Rect {
359            Rect::default()
360        }
361        fn get_intersections(&self, _: f32, _: f32) -> Vec<f32> {
362            Vec::new()
363        }
364        fn dispose(&self) {}
365        fn as_any(&self) -> &dyn std::any::Any {
366            self
367        }
368    }
369
370    /// Every glyph is 6 wide and 10 high, except `'|'` (3 x 14); the line
371    /// height is 14. With `variable_digits`, the digit `d` is `4 + d / 2`
372    /// wide.
373    #[derive(Default)]
374    pub(crate) struct MockGlyphRunSource {
375        pub requested: RefCell<Vec<char>>,
376        pub variable_digits: bool,
377    }
378
379    impl MockGlyphRunSource {
380        fn width(&self, character: char) -> f64 {
381            match character {
382                '|' => 3.0,
383                '0'..='9' if self.variable_digits => 4.0 + (character as u32 - '0' as u32) as f64 / 2.0,
384                _ => 6.0,
385            }
386        }
387    }
388
389    impl IDiagnosticGlyphRunSource for MockGlyphRunSource {
390        fn create_glyph_run(&self, character: char) -> DiagnosticGlyphRun {
391            self.requested.borrow_mut().push(character);
392            let height = if character == '|' { 14.0 } else { 10.0 };
393            DiagnosticGlyphRun {
394                bounds: Rect::new(0.0, 0.0, self.width(character), height),
395                platform_impl: std::sync::Arc::new(MockGlyphRunImpl(character)),
396            }
397        }
398
399        fn create_text_glyph_run(&self, text: &[u16]) -> DiagnosticGlyphRun {
400            let text = String::from_utf16(text).unwrap();
401            let width = text.chars().map(|c| self.width(c)).sum();
402            DiagnosticGlyphRun {
403                bounds: Rect::new(0.0, 0.0, width, 10.0),
404                platform_impl: std::sync::Arc::new(MockTextGlyphRunImpl(text)),
405            }
406        }
407
408        fn line_height(&self) -> f64 {
409            14.0
410        }
411    }
412
413    pub(crate) fn create_renderer() -> DiagnosticTextRenderer {
414        DiagnosticTextRenderer::new(Rc::new(MockGlyphRunSource::default()))
415    }
416
417    /// A renderer whose digits differ in width (the font of the upstream
418    /// tests has proportional digits; the fonts of the test harness do not).
419    fn create_variable_digits_renderer() -> DiagnosticTextRenderer {
420        DiagnosticTextRenderer::new(Rc::new(MockGlyphRunSource { variable_digits: true, ..Default::default() }))
421    }
422
423    /// The glyph runs drawn by `draw`: the x of their origin and the glyph
424    /// (its character, or the text of a shaped run).
425    fn capture(draw: impl FnOnce(&mut MockDrawingContext)) -> Vec<(f64, String)> {
426        let mut context = MockDrawingContext::default();
427        draw(&mut context);
428        context
429            .log
430            .iter()
431            .filter_map(|line| {
432                let (glyph, rest) = if let Some(rest) = line.strip_prefix("glyph ") {
433                    let (code, rest) = rest.split_once(' ').unwrap();
434                    (char::from_u32(code.parse::<u32>().unwrap()).unwrap().to_string(), rest)
435                } else {
436                    let rest = line.strip_prefix("text ")?;
437                    let at = rest.rfind(" at ").unwrap();
438                    let (text, _) = rest[..at].rsplit_once(' ').unwrap();
439                    (text.to_string(), &rest[at..])
440                };
441                let at = rest.rfind(" at ").unwrap();
442                let x = rest[at + 4..].split(',').next().unwrap().parse::<f64>().unwrap();
443                Some((x, glyph))
444            })
445            .collect()
446    }
447
448    fn draw(renderer: &DiagnosticTextRenderer, text: &str) -> Vec<f64> {
449        capture(|context| renderer.draw_ascii_text(context, text, &*Brushes::white()))
450            .into_iter()
451            .map(|(x, _)| x)
452            .collect()
453    }
454
455    fn assert_close(expected: f64, actual: f64) {
456        assert!((expected - actual).abs() < 1e-6, "expected {expected}, actual {actual}");
457    }
458
459    // --- Ports of upstream's `Rendering/DiagnosticTextRendererTests.cs`.
460
461    #[test]
462    fn digits_measure_to_the_same_width() {
463        let renderer = create_variable_digits_renderer();
464
465        let width = renderer.measure_ascii_text("0").width;
466
467        for c in '1'..='9' {
468            assert_eq!(width, renderer.measure_ascii_text(&c.to_string()).width);
469        }
470
471        assert_eq!(width * 3.0, renderer.measure_ascii_text("108").width);
472    }
473
474    #[test]
475    fn height_is_the_scaled_line_spacing() {
476        use crate::media::text_formatting::testing::TextTestScope;
477
478        let _scope = TextTestScope::new();
479
480        let glyph_typeface = Typeface::default_typeface().glyph_typeface();
481        let renderer = DiagnosticTextRenderer::from_glyph_typeface(glyph_typeface.clone(), 12.0);
482        let metrics = glyph_typeface.metrics();
483        let expected = metrics.line_spacing() as f64 * 12.0 / metrics.design_em_height as f64;
484
485        assert_eq!(expected, renderer.get_max_height());
486        assert_eq!(expected, renderer.measure_ascii_text("FPS: 60").height);
487    }
488
489    #[test]
490    fn digits_are_centered_inside_the_digit_cell() {
491        let source = Rc::new(MockGlyphRunSource { variable_digits: true, ..Default::default() });
492        let renderer = DiagnosticTextRenderer::new(source.clone());
493        let cell_width = renderer.measure_ascii_text("0").width;
494
495        // Narrow digits shift to the center of their cell, they must never leave it.
496        const DIGITS: &str = "1234567890";
497        let drawn = draw(&renderer, DIGITS);
498
499        assert_eq!(DIGITS.len(), drawn.len());
500
501        for (i, digit) in DIGITS.chars().enumerate() {
502            let cell_start = i as f64 * cell_width;
503            let glyph_width = source.width(digit);
504
505            assert!(drawn[i] >= cell_start - 1e-6, "Digit {i} starts before its cell.");
506            assert!(drawn[i] + glyph_width <= cell_start + cell_width + 1e-6, "Digit {i} ends after its cell.");
507            assert_close(cell_start + (cell_width - glyph_width) / 2.0, drawn[i]);
508        }
509    }
510
511    #[test]
512    fn text_after_digits_does_not_depend_on_the_digit_values() {
513        let renderer = create_variable_digits_renderer();
514
515        let narrow = draw(&renderer, "111 ms");
516        let wide = draw(&renderer, "888 ms");
517
518        assert_eq!(narrow.len(), wide.len());
519        assert_close(*narrow.last().unwrap(), *wide.last().unwrap());
520    }
521
522    #[test]
523    fn draw_digits_matches_drawing_the_padded_text() {
524        let rows: [(i32, i32, &str); 5] =
525            [(42, 5, "00042"), (0, 3, "000"), (12345678, 8, "12345678"), (1000, 3, "999"), (-5, 3, "000")];
526
527        for (value, digit_count, expected_text) in rows {
528            let renderer = create_variable_digits_renderer();
529
530            let digits = capture(|c| renderer.draw_digits(c, value, digit_count, &*Brushes::white()));
531            let text = capture(|c| renderer.draw_ascii_text(c, expected_text, &*Brushes::white()));
532
533            assert_eq!(text.len(), digits.len(), "{value} {digit_count}");
534
535            for i in 0..text.len() {
536                assert_eq!(text[i].1, digits[i].1, "{value} {digit_count}: glyph {i}");
537                assert_close(text[i].0, digits[i].0);
538            }
539        }
540    }
541
542    #[test]
543    fn shaped_text_is_measured_once_and_drawn_as_a_single_run() {
544        let renderer = create_variable_digits_renderer();
545        let shaped = renderer.shape(" FPS: ");
546
547        assert_close(renderer.measure_ascii_text(" FPS: ").width, shaped.width());
548        assert_eq!(1, capture(|c| renderer.draw_shaped_text(c, &shaped, &*Brushes::white())).len());
549
550        shaped.dispose();
551    }
552
553    #[test]
554    fn fps_counter_layout_does_not_depend_on_the_numbers() {
555        let counter = FpsCounter::new(Rc::new(create_variable_digits_renderer()), Duration::ZERO);
556        let mut rects = Vec::new();
557
558        let first = capture(|c| {
559            rects.push(counter.render_fps(c, Some((1000, 1000)), 1, 1, false, None, Duration::from_millis(10)))
560        });
561        let second = capture(|c| {
562            rects.push(counter.render_fps(c, Some((1000, 1000)), 99999, 88888, false, None, Duration::from_millis(20)))
563        });
564
565        assert_eq!(rects[0], rects[1]);
566        assert_eq!(first.len(), second.len());
567
568        // Labels, and every digit cell, stay where they were no matter which digits are shown.
569        for i in 0..first.len() {
570            if first[i].1 == second[i].1 {
571                assert_close(first[i].0, second[i].0);
572            }
573        }
574    }
575
576    // --- Not upstream tests: the seam to the text stack and the mock.
577
578    #[test]
579    fn creates_one_run_per_printable_ascii_char_in_order() {
580        let source = Rc::new(MockGlyphRunSource::default());
581        let _renderer = DiagnosticTextRenderer::new(source.clone());
582        let requested = source.requested.borrow();
583        assert_eq!(95, requested.len());
584        assert_eq!(' ', requested[0]);
585        assert_eq!('~', requested[94]);
586        assert!(requested.windows(2).all(|w| w[0] as u32 + 1 == w[1] as u32));
587    }
588
589    #[test]
590    fn glyph_runs_are_built_from_a_glyph_typeface_and_an_em_size() {
591        use crate::media::text_formatting::testing::{advance, TextTestScope};
592        use crate::rendering::testing::{DrawingLog, MockDrawingContextImpl};
593
594        let _scope = TextTestScope::new();
595        let glyph_typeface = Typeface::default_typeface().glyph_typeface();
596
597        let source = GlyphTypefaceDiagnosticGlyphRunSource::new(glyph_typeface.clone(), 12.0);
598        let run = source.create_glyph_run('A');
599        // The layout bounds: the advance by the line height of the font.
600        let expected = GlyphRun::from_glyph_indices(
601            glyph_typeface.clone(),
602            12.0,
603            ReadOnlyMemory::from_vec(vec!['A' as u16]),
604            &[glyph_typeface.character_to_glyph_map().get_glyph('A' as i32)],
605            None,
606            0,
607        );
608        assert_eq!(run.bounds, expected.bounds());
609        assert_eq!(run.bounds.width, advance(12.0));
610        assert_eq!(run.platform_impl.font_rendering_em_size(), 12.0);
611        assert_eq!(run.platform_impl.bounds(), expected.platform_impl().bounds());
612
613        // A text is one run of its glyphs.
614        let text: Vec<u16> = "FPS".encode_utf16().collect();
615        let text_run = source.create_text_glyph_run(&text);
616        assert_eq!(text_run.bounds.width, 3.0 * advance(12.0));
617
618        let renderer = DiagnosticTextRenderer::from_glyph_typeface(glyph_typeface, 12.0);
619        assert_eq!(3.0 * advance(12.0), renderer.measure_ascii_text("FPS").width);
620        assert_eq!(renderer.shape("FPS").width(), renderer.measure_ascii_text("FPS").width);
621
622        // The default renderer uses the default typeface at an em size of 12.
623        let default_renderer = DiagnosticTextRenderer::create_default();
624        assert_eq!(renderer.measure_ascii_text("12 ms"), default_renderer.measure_ascii_text("12 ms"));
625
626        let log = DrawingLog::new();
627        let mut context = MockDrawingContextImpl::new(log.clone());
628        context.log_transforms = false;
629        default_renderer.draw_ascii_text(&mut context, "ab", &*Brushes::white());
630        assert_eq!(2, log.count("DrawGlyphRun White "));
631    }
632
633    #[test]
634    fn max_height_is_the_line_height() {
635        assert_eq!(14.0, create_renderer().get_max_height());
636    }
637
638    #[test]
639    fn measures_widths_and_the_line_height() {
640        let renderer = create_renderer();
641        assert_eq!(Size::new(0.0, 14.0), renderer.measure_ascii_text(""));
642        assert_eq!(Size::new(18.0, 14.0), renderer.measure_ascii_text("abc"));
643        assert_eq!(Size::new(15.0, 14.0), renderer.measure_ascii_text("a|b"));
644        // Non-printable and non-ASCII characters count as spaces; characters
645        // outside the BMP are two UTF-16 units and so two spaces.
646        assert_eq!(Size::new(12.0, 14.0), renderer.measure_ascii_text("\n\u{e9}"));
647        assert_eq!(Size::new(12.0, 14.0), renderer.measure_ascii_text("\u{1F600}"));
648    }
649
650    #[test]
651    fn draws_each_glyph_advanced_by_the_previous_widths() {
652        let renderer = create_renderer();
653        let mut ctx =
654            MockDrawingContext { transform: Matrix::create_translation(100.0, 50.0), ..Default::default() };
655        renderer.draw_ascii_text(&mut ctx, "a|\u{7f}b", &*Brushes::white());
656        assert_eq!(
657            vec![
658                "glyph 97 #ffffffff@1 at 100,50",
659                "glyph 124 #ffffffff@1 at 106,50",
660                "glyph 32 #ffffffff@1 at 109,50",
661                "glyph 98 #ffffffff@1 at 115,50",
662            ],
663            ctx.log
664        );
665        // The transform is restored.
666        assert_eq!(Matrix::create_translation(100.0, 50.0), ctx.transform);
667    }
668
669    #[test]
670    fn an_empty_shaped_text_draws_nothing() {
671        let renderer = create_renderer();
672        let shaped = renderer.shape("");
673
674        assert_eq!(0.0, shaped.width());
675        assert!(capture(|c| renderer.draw_shaped_text(c, &shaped, &*Brushes::white())).is_empty());
676    }
677}