cranpose_render_common/
font_layout.rs1use ab_glyph::{point, Font, Glyph, OutlinedGlyph, Point, PxScale, ScaleFont};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub struct FontVerticalMetrics {
5 pub ascent: f32,
6 pub descent: f32,
7 pub natural_line_height: f32,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct GlyphPixelBounds {
12 pub min_x: i32,
13 pub min_y: i32,
14 pub max_x: i32,
15 pub max_y: i32,
16}
17
18impl GlyphPixelBounds {
19 pub fn width(self) -> usize {
20 self.max_x.saturating_sub(self.min_x) as usize
21 }
22
23 pub fn height(self) -> usize {
24 self.max_y.saturating_sub(self.min_y) as usize
25 }
26}
27
28pub fn vertical_metrics(font: &impl Font, font_size: f32) -> FontVerticalMetrics {
29 let scaled_font = font.as_scaled(PxScale::from(font_size));
30 let ascent = scaled_font.ascent();
31 let descent = scaled_font.descent();
32 FontVerticalMetrics {
33 ascent,
34 descent,
35 natural_line_height: (ascent - descent).ceil(),
36 }
37}
38
39pub fn layout_line_glyphs(
40 font: &impl Font,
41 text: &str,
42 font_size: f32,
43 origin: Point,
44) -> Vec<Glyph> {
45 let scale = PxScale::from(font_size);
46 let scaled_font = font.as_scaled(scale);
47 let mut caret_x = origin.x;
48 let mut previous = None;
49 let mut glyphs = Vec::with_capacity(text.chars().count());
50
51 for ch in text.chars() {
52 let glyph_id = scaled_font.glyph_id(ch);
53 if let Some(previous_id) = previous {
54 caret_x += scaled_font.kern(previous_id, glyph_id);
55 }
56 glyphs.push(glyph_id.with_scale_and_position(scale, point(caret_x, origin.y)));
57 caret_x += scaled_font.h_advance(glyph_id);
58 previous = Some(glyph_id);
59 }
60
61 glyphs
62}
63
64pub fn align_glyph_to_pixel_grid(mut glyph: Glyph, static_text_motion: bool) -> Glyph {
65 if static_text_motion {
66 glyph.position.x = glyph.position.x.round();
67 glyph.position.y = glyph.position.y.round();
68 }
69 glyph
70}
71
72pub fn glyph_pixel_bounds(font: &impl Font, glyph: &Glyph) -> Option<GlyphPixelBounds> {
73 let outlined = font.outline_glyph(glyph.clone())?;
74 Some(pixel_bounds_from_outlined(&outlined))
75}
76
77pub(crate) fn pixel_bounds_from_outlined(outlined: &OutlinedGlyph) -> GlyphPixelBounds {
78 let bounds = outlined.px_bounds();
79 GlyphPixelBounds {
80 min_x: bounds.min.x as i32,
81 min_y: bounds.min.y as i32,
82 max_x: bounds.max.x as i32,
83 max_y: bounds.max.y as i32,
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use ab_glyph::FontRef;
91
92 fn test_font() -> FontRef<'static> {
93 FontRef::try_from_slice(include_bytes!(
94 "../../../../apps/desktop-demo/assets/NotoSansMerged.ttf"
95 ))
96 .expect("font")
97 }
98
99 #[test]
100 fn vertical_metrics_return_positive_line_height() {
101 let metrics = vertical_metrics(&test_font(), 24.0);
102 assert!(metrics.ascent > 0.0);
103 assert!(metrics.descent < 0.0);
104 assert!(metrics.natural_line_height >= (metrics.ascent - metrics.descent));
105 }
106
107 #[test]
108 fn layout_line_glyphs_starts_at_origin() {
109 let font = test_font();
110 let origin = point(5.0, 13.0);
111 let glyphs = layout_line_glyphs(&font, "AV", 18.0, origin);
112 assert_eq!(glyphs.len(), 2);
113 assert_eq!(glyphs[0].position, origin);
114 assert!(glyphs[1].position.x > glyphs[0].position.x);
115 }
116
117 #[test]
118 fn align_glyph_to_pixel_grid_only_changes_static_positions() {
119 let font = test_font();
120 let glyph = layout_line_glyphs(&font, "A", 17.0, point(0.0, 13.37))
121 .into_iter()
122 .next()
123 .expect("glyph");
124 let snapped = align_glyph_to_pixel_grid(glyph.clone(), true);
125 let unchanged = align_glyph_to_pixel_grid(glyph, false);
126
127 assert_eq!(snapped.position.x, snapped.position.x.round());
128 assert_eq!(snapped.position.y, snapped.position.y.round());
129 assert!((unchanged.position.y - 13.37).abs() < 1e-3);
130 }
131
132 #[test]
133 fn glyph_pixel_bounds_reports_positive_size() {
134 let font = test_font();
135 let glyph = layout_line_glyphs(&font, "A", 24.0, point(0.0, 20.0))
136 .into_iter()
137 .next()
138 .expect("glyph");
139 let bounds = glyph_pixel_bounds(&font, &glyph).expect("bounds");
140
141 assert!(bounds.width() > 0);
142 assert!(bounds.height() > 0);
143 }
144}