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