Skip to main content

cranpose_render_common/
font_layout.rs

1use ab_glyph::{Font, Glyph, OutlinedGlyph, Point, PxScale, ScaleFont, point};
2
3/// Vertical metrics of a font at one pixel size.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct FontVerticalMetrics {
6    /// Distance from the baseline to the top of the line in pixels.
7    pub ascent: f32,
8    /// Distance from the baseline to the bottom of the line in pixels, negative below the baseline.
9    pub descent: f32,
10    /// Ascent minus descent, rounded up to a whole pixel.
11    pub natural_line_height: f32,
12    /// `hhea.lineGap` at this size. Read only by a style that asks for
13    /// Android's `includeFontPadding`; every other line box ignores it.
14    pub line_gap: f32,
15}
16
17/// Integer pixel box that a rasterized glyph covers, relative to the glyph origin.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct GlyphPixelBounds {
20    /// Left edge of the box in pixels.
21    pub min_x: i32,
22    /// Top edge of the box in pixels.
23    pub min_y: i32,
24    /// Right edge of the box in pixels, exclusive.
25    pub max_x: i32,
26    /// Bottom edge of the box in pixels, exclusive.
27    pub max_y: i32,
28}
29
30impl GlyphPixelBounds {
31    /// Box width in pixels, zero when the box is empty.
32    pub fn width(self) -> usize {
33        self.max_x.saturating_sub(self.min_x) as usize
34    }
35
36    /// Box height in pixels, zero when the box is empty.
37    pub fn height(self) -> usize {
38        self.max_y.saturating_sub(self.min_y) as usize
39    }
40}
41
42/// Returns the vertical metrics of `font` at `font_size` pixels.
43pub fn vertical_metrics(font: &impl Font, font_size: f32) -> FontVerticalMetrics {
44    let scaled_font = font.as_scaled(PxScale::from(font_size));
45    let ascent = scaled_font.ascent();
46    let descent = scaled_font.descent();
47    FontVerticalMetrics {
48        ascent,
49        descent,
50        natural_line_height: (ascent - descent).ceil(),
51        line_gap: scaled_font.line_gap(),
52    }
53}
54
55/// Lays out `text` as one line of glyphs, applying kerning. `font_size` is in pixels and
56/// `origin` is the position of the first glyph on the baseline.
57pub fn layout_line_glyphs(
58    font: &impl Font,
59    text: &str,
60    font_size: f32,
61    origin: Point,
62) -> Vec<Glyph> {
63    let scale = PxScale::from(font_size);
64    let scaled_font = font.as_scaled(scale);
65    let mut caret_x = origin.x;
66    let mut previous = None;
67    let mut glyphs = Vec::with_capacity(text.chars().count());
68
69    for ch in text.chars() {
70        let glyph_id = scaled_font.glyph_id(ch);
71        if let Some(previous_id) = previous {
72            caret_x += scaled_font.kern(previous_id, glyph_id);
73        }
74        glyphs.push(glyph_id.with_scale_and_position(scale, point(caret_x, origin.y)));
75        caret_x += scaled_font.h_advance(glyph_id);
76        previous = Some(glyph_id);
77    }
78
79    glyphs
80}
81
82/// Returns the advance width of `text` as one line at `font_size` pixels, including kerning.
83pub fn line_advance_width(font: &impl Font, text: &str, font_size: f32) -> f32 {
84    // The scale reads the face's ascender and descender: once a line, not
85    // once a glyph.
86    let h_scale = font.as_scaled(PxScale::from(font_size)).h_scale_factor();
87    let mut width = 0.0;
88    let mut previous = None;
89
90    for ch in text.chars() {
91        let glyph_id = font.glyph_id(ch);
92        if let Some(previous_id) = previous {
93            width += font.kern_unscaled(previous_id, glyph_id) * h_scale;
94        }
95        width += font.h_advance_unscaled(glyph_id) * h_scale;
96        previous = Some(glyph_id);
97    }
98
99    width.max(0.0)
100}
101
102/// Rounds the glyph position to whole pixels when `static_text_motion` is true. Otherwise the
103/// glyph is returned unchanged.
104pub fn align_glyph_to_pixel_grid(mut glyph: Glyph, static_text_motion: bool) -> Glyph {
105    if static_text_motion {
106        glyph.position.x = cranpose_ui_graphics::whole::round(glyph.position.x);
107        glyph.position.y = cranpose_ui_graphics::whole::round(glyph.position.y);
108    }
109    glyph
110}
111
112/// Returns the pixel box that `glyph` covers once outlined, or `None` when it has no outline
113/// (for example a space).
114pub fn glyph_pixel_bounds(font: &impl Font, glyph: &Glyph) -> Option<GlyphPixelBounds> {
115    let outlined = font.outline_glyph(glyph.clone())?;
116    Some(pixel_bounds_from_outlined(&outlined))
117}
118
119pub(crate) fn pixel_bounds_from_outlined(outlined: &OutlinedGlyph) -> GlyphPixelBounds {
120    let bounds = outlined.px_bounds();
121    GlyphPixelBounds {
122        min_x: bounds.min.x as i32,
123        min_y: bounds.min.y as i32,
124        max_x: bounds.max.x as i32,
125        max_y: bounds.max.y as i32,
126    }
127}
128
129#[cfg(test)]
130#[path = "tests/font_layout_tests.rs"]
131mod tests;