Skip to main content

cranpose_render_common/
font_layout.rs

1use 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 line_advance_width(font: &impl Font, text: &str, font_size: f32) -> f32 {
65    let scale = PxScale::from(font_size);
66    let scaled_font = font.as_scaled(scale);
67    let mut width = 0.0;
68    let mut previous = None;
69
70    for ch in text.chars() {
71        let glyph_id = scaled_font.glyph_id(ch);
72        if let Some(previous_id) = previous {
73            width += scaled_font.kern(previous_id, glyph_id);
74        }
75        width += scaled_font.h_advance(glyph_id);
76        previous = Some(glyph_id);
77    }
78
79    width.max(0.0)
80}
81
82pub fn align_glyph_to_pixel_grid(mut glyph: Glyph, static_text_motion: bool) -> Glyph {
83    if static_text_motion {
84        glyph.position.x = glyph.position.x.round();
85        glyph.position.y = glyph.position.y.round();
86    }
87    glyph
88}
89
90pub fn glyph_pixel_bounds(font: &impl Font, glyph: &Glyph) -> Option<GlyphPixelBounds> {
91    let outlined = font.outline_glyph(glyph.clone())?;
92    Some(pixel_bounds_from_outlined(&outlined))
93}
94
95pub(crate) fn pixel_bounds_from_outlined(outlined: &OutlinedGlyph) -> GlyphPixelBounds {
96    let bounds = outlined.px_bounds();
97    GlyphPixelBounds {
98        min_x: bounds.min.x as i32,
99        min_y: bounds.min.y as i32,
100        max_x: bounds.max.x as i32,
101        max_y: bounds.max.y as i32,
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use ab_glyph::FontRef;
109
110    fn test_font() -> FontRef<'static> {
111        FontRef::try_from_slice(include_bytes!("../assets/NotoSansMerged.ttf")).expect("font")
112    }
113
114    #[test]
115    fn vertical_metrics_return_positive_line_height() {
116        let metrics = vertical_metrics(&test_font(), 24.0);
117        assert!(metrics.ascent > 0.0);
118        assert!(metrics.descent < 0.0);
119        assert!(metrics.natural_line_height >= (metrics.ascent - metrics.descent));
120    }
121
122    #[test]
123    fn layout_line_glyphs_starts_at_origin() {
124        let font = test_font();
125        let origin = point(5.0, 13.0);
126        let glyphs = layout_line_glyphs(&font, "AV", 18.0, origin);
127        assert_eq!(glyphs.len(), 2);
128        assert_eq!(glyphs[0].position, origin);
129        assert!(glyphs[1].position.x > glyphs[0].position.x);
130    }
131
132    #[test]
133    fn align_glyph_to_pixel_grid_only_changes_static_positions() {
134        let font = test_font();
135        let glyph = layout_line_glyphs(&font, "A", 17.0, point(0.0, 13.37))
136            .into_iter()
137            .next()
138            .expect("glyph");
139        let snapped = align_glyph_to_pixel_grid(glyph.clone(), true);
140        let unchanged = align_glyph_to_pixel_grid(glyph, false);
141
142        assert_eq!(snapped.position.x, snapped.position.x.round());
143        assert_eq!(snapped.position.y, snapped.position.y.round());
144        assert!((unchanged.position.y - 13.37).abs() < 1e-3);
145    }
146
147    #[test]
148    fn line_advance_width_uses_font_advances() {
149        let font = test_font();
150        let width = line_advance_width(&font, "Counter App", 18.0);
151        assert!(width > 0.0);
152    }
153
154    #[test]
155    fn glyph_pixel_bounds_reports_positive_size() {
156        let font = test_font();
157        let glyph = layout_line_glyphs(&font, "A", 24.0, point(0.0, 20.0))
158            .into_iter()
159            .next()
160            .expect("glyph");
161        let bounds = glyph_pixel_bounds(&font, &glyph).expect("bounds");
162
163        assert!(bounds.width() > 0);
164        assert!(bounds.height() > 0);
165    }
166}