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#[derive(Clone)]
12pub struct DiagnosticGlyphRun {
13 pub bounds: Rect,
18 pub platform_impl: std::sync::Arc<dyn IGlyphRunImpl>,
20}
21
22pub trait IDiagnosticGlyphRunSource {
32 fn create_glyph_run(&self, character: char) -> DiagnosticGlyphRun;
36
37 fn create_text_glyph_run(&self, text: &[u16]) -> DiagnosticGlyphRun;
40
41 fn line_height(&self) -> f64;
44}
45
46pub struct GlyphTypefaceDiagnosticGlyphRunSource {
49 glyph_typeface: Rc<GlyphTypeface>,
50 font_rendering_em_size: f64,
51 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
101pub struct DiagnosticTextRenderer {
104 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 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 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 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 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 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 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 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 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 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 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 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
289pub struct ShapedText {
291 glyph_run: Option<DiagnosticGlyphRun>,
294 width: f64,
295}
296
297impl ShapedText {
298 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 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 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 #[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 fn create_variable_digits_renderer() -> DiagnosticTextRenderer {
420 DiagnosticTextRenderer::new(Rc::new(MockGlyphRunSource { variable_digits: true, ..Default::default() }))
421 }
422
423 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 #[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 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 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 #[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 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 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 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 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 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}