Skip to main content

zintl_render/
text.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use zintl_render_math::{
5    Alignment, InLogicalScale, InPhysicalScale, LogicalPixels, LogicalPixelsRect, PhysicalPixels,
6    PhysicalPixelsF, PhysicalPixelsFPoint, PhysicalPixelsFRect, PhysicalPixelsFSize,
7    PhysicalPixelsRect, PhysicalPixelsSize, ScaleFactor,
8};
9
10use crate::texture::Atlas;
11use ab_glyph::{Font as _, ScaleFont};
12
13/// A rectangle and texture coordinates for a glyph.
14#[derive(Clone, Copy, Debug, Default, PartialEq)]
15pub struct GlyphRect {
16    /// The width of the glyph in pixels.
17    pub width: PhysicalPixelsF,
18    /// The height of the glyph in pixels.
19    pub height: PhysicalPixelsF,
20    /// Mesh bounds on the glyph rectangle. NOT FOR LAYOUTING.
21    pub bounds: PhysicalPixelsFRect,
22    /// The texture bounds of the glyph in the atlas.
23    pub texture_bounds: PhysicalPixelsRect,
24}
25
26/// A single glyph data with size and coordinates.
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
28pub struct Glyph {
29    /// ab_glyph glyph id.
30    pub id: ab_glyph::GlyphId,
31    pub rect: GlyphRect,
32}
33
34/// A font with a specific size.
35#[derive(Clone, Debug)]
36pub struct Font {
37    pub ab_font: ab_glyph::FontArc,
38    pub type_face: String,
39    /// The atlas containing the rendered glyphs.
40    pub atlas: Arc<Mutex<Atlas>>,
41    /// The scale factor for the font.
42    pub scale: PhysicalPixelsF,
43    pub height: PhysicalPixelsF,
44    /// https://docs.rs/ab_glyph/latest/ab_glyph/trait.Font.html#glyph-layout-concepts
45    pub ascent: PhysicalPixelsF,
46    /// https://docs.rs/ab_glyph/latest/ab_glyph/trait.Font.html#glyph-layout-concepts
47    pub descent: PhysicalPixelsF,
48    /// https://docs.rs/ab_glyph/latest/ab_glyph/trait.Font.html#glyph-layout-concepts
49    pub line_gap: PhysicalPixelsF,
50    /// A cached list of glyphs in the atlas.
51    pub glyphs: Arc<Mutex<HashMap<char, Glyph>>>,
52}
53
54impl Font {
55    pub fn new(
56        ab_font: ab_glyph::FontArc,
57        type_face: String,
58        scale: LogicalPixels,
59        scale_factor: ScaleFactor,
60    ) -> Self {
61        let scale_physical: PhysicalPixels = scale.in_physical_scale(&scale_factor);
62        //let scale = scale.in_device_pixels(&scale_factor);
63        let atlas = Atlas::new(
64            scale_physical.max(1024.into()),
65            scale_physical.max(32.into()),
66        );
67        let scale: PhysicalPixelsF = scale_physical.into();
68        // Init the font with PHYSICAL scale.
69        let scaled = ab_font.as_scaled(scale.value());
70        let height: PhysicalPixelsF = scaled.height().into();
71        let line_gap: PhysicalPixelsF = scaled.line_gap().into();
72        let ascent: PhysicalPixelsF = scaled.ascent().into();
73        let descent: PhysicalPixelsF = scaled.descent().into();
74
75        Font {
76            ab_font,
77            type_face,
78            atlas: Mutex::new(atlas).into(),
79            scale,
80            height,
81            ascent,
82            descent,
83            line_gap,
84            glyphs: Mutex::new(HashMap::new()).into(),
85        }
86    }
87
88    pub fn get_glyph(&self, c: char) -> Glyph {
89        // TODO: Error handling
90        if let Some(glyph) = self.glyphs.lock().unwrap().get(&c) {
91            return *glyph;
92        }
93
94        let atlas = &mut self.atlas.lock().unwrap();
95        // Init the font with PHYSICAL scale.
96        let id = self.ab_font.glyph_id(c);
97
98        if id.0 == 0 {
99            return Glyph::default();
100        }
101
102        let scaled = self.ab_font.as_scaled(self.scale.value());
103        let h_advance: PhysicalPixelsF = scaled.h_advance(id).into();
104
105        let g = id.with_scale_and_position(
106            self.scale.value(),
107            ab_glyph::Point {
108                x: 0.0,
109                y: scaled.ascent(),
110            },
111        );
112
113        let rect = {
114            if let Some(g) = self.ab_font.outline_glyph(g) {
115                let px_bounds = g.px_bounds();
116
117                // TODO: Properly scale the bounds
118                let px_width: PhysicalPixels = (px_bounds.width() as u32).into();
119                let px_height: PhysicalPixels = (px_bounds.height() as u32).into();
120                // base: the absolute position in the atlas where the glyph will be drawn.
121                let (texture_bounds, atlas_width, pixels) = atlas.create_image(px_width, px_height);
122                g.draw(|x, y, c| {
123                    if c == 0.0 {
124                        return; // Skip transparent pixels
125                    }
126                    let start = (texture_bounds.min.y + y) * atlas_width * 4
127                        + (texture_bounds.min.x + x) * 4;
128                    let start = start.value() as usize;
129                    pixels[start] = ((1. - c) * 255.) as u8;
130                    pixels[start + 1] = ((1. - c) * 255.) as u8;
131                    pixels[start + 2] = ((1. - c) * 255.) as u8;
132                    pixels[start + 3] = (c * 255.) as u8;
133                });
134
135                GlyphRect {
136                    width: h_advance,
137                    height: self.height,
138                    bounds: PhysicalPixelsFRect {
139                        min: PhysicalPixelsFPoint::new(
140                            px_bounds.min.x.into(),
141                            px_bounds.min.y.into(),
142                        ),
143                        max: PhysicalPixelsFPoint::new(
144                            px_bounds.max.x.into(),
145                            px_bounds.max.y.into(),
146                        ),
147                    },
148                    texture_bounds,
149                }
150            } else {
151                return Glyph::default();
152            }
153        };
154
155        let glyph = Glyph { id, rect };
156
157        // Cache the glyph
158        // TODO: Error handling
159        self.glyphs.lock().unwrap().insert(c, glyph);
160
161        glyph
162    }
163
164    pub fn kern(&self, left: &Glyph, right: &Glyph) -> PhysicalPixelsF {
165        let scaled = self.ab_font.as_scaled(self.scale.value());
166        scaled.kern(left.id, right.id).into()
167    }
168
169    pub fn get_atlas_pixels(&self) -> Vec<u8> {
170        self.atlas.lock().unwrap().pixels()
171    }
172
173    pub fn get_atlas_size(&self) -> PhysicalPixelsSize {
174        let atlas = self.atlas.lock().unwrap();
175        PhysicalPixelsSize::new(atlas.width, atlas.height)
176    }
177}
178
179#[derive(Clone, Debug, Default, PartialEq, Hash, Eq)]
180pub struct FontProperties {
181    pub name: String,
182    /// f32 does not implement Hash, so we use a String instead.
183    pub scale_string: String,
184}
185
186#[derive(Clone, Debug)]
187pub struct Typecase {
188    pub fonts: HashMap<String, ab_glyph::FontArc>,
189    pub sized_fonts: HashMap<FontProperties, Font>,
190    pub scale_factor: ScaleFactor,
191}
192
193impl Typecase {
194    pub fn new(scale_factor: ScaleFactor) -> Self {
195        Typecase {
196            fonts: HashMap::new(),
197            sized_fonts: HashMap::new(),
198            scale_factor,
199        }
200    }
201
202    pub fn load_font(&mut self, name: String, data: Vec<u8>) {
203        let font = ab_glyph::FontVec::try_from_vec(data)
204            .map(ab_glyph::FontArc::from)
205            .unwrap();
206        self.fonts.insert(name.clone(), font.clone());
207    }
208
209    pub fn get_font(&mut self, font: FontProperties) -> Option<&Font> {
210        let f = self.sized_fonts.entry(font.clone()).or_insert({
211            if let Some(ab_font) = self.fonts.get(&font.name) {
212                let scale = font
213                    .scale_string
214                    .parse::<f32>()
215                    .expect("Invalid scale string");
216                let new_font = Font::new(
217                    ab_font.clone(),
218                    font.name.clone(),
219                    scale.into(),
220                    self.scale_factor,
221                );
222                new_font
223            } else {
224                return None;
225            }
226        });
227        Some(f)
228    }
229}
230
231#[derive(Clone, Debug)]
232pub struct PositionedGlyph {
233    pub glyph: Glyph,
234    pub rect: PhysicalPixelsFRect,
235}
236
237#[derive(Clone, Copy, Debug, Default, PartialEq)]
238pub enum TextAlignment {
239    #[default]
240    Left,
241    Center,
242    Right,
243}
244
245/// Composed glyphs, ready for rendering.
246#[derive(Clone, Debug)]
247pub struct Galley {
248    pub glyphs: Vec<PositionedGlyph>,
249    pub rect: LogicalPixelsRect,
250}
251
252#[derive(Clone, Debug)]
253pub struct Typesetter {}
254
255impl Typesetter {
256    pub fn new() -> Self {
257        Typesetter {}
258    }
259
260    /// Layouts a text
261    pub fn compose(
262        &self,
263        text: &str,
264        font: &Font,
265        bounds: LogicalPixelsRect,
266        _text_alignment: TextAlignment,
267        alignment: Alignment,
268        scale_factor: &ScaleFactor,
269    ) -> Galley {
270        let mut glyphs = Vec::new();
271        let mut cursor = PhysicalPixelsFPoint::zero();
272        let mut size: PhysicalPixelsFSize = PhysicalPixelsFSize::default();
273        let mut last_glyph_id = None;
274        for c in text.chars() {
275            let glyph = font.get_glyph(c);
276            if let Some(last) = last_glyph_id {
277                cursor.x += font.kern(&last, &glyph);
278            }
279            if size.width < glyph.rect.height {
280                size.height = glyph.rect.height;
281            }
282            glyphs.push(PositionedGlyph {
283                glyph,
284                rect: PhysicalPixelsFRect::with_size(
285                    cursor,
286                    PhysicalPixelsFSize::new(glyph.rect.width, glyph.rect.height),
287                ),
288            });
289            last_glyph_id = Some(glyph);
290            cursor.x += glyph.rect.width;
291            size.width += glyph.rect.width;
292        }
293
294        Galley {
295            glyphs,
296            rect: alignment.align_size(bounds, size.in_logical_scale(&scale_factor)),
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use zintl_render_math::{LogicalPixelsPoint, ScaleFactor};
305
306    #[test]
307    fn galley_logical_rect() {
308        let scale_factor = ScaleFactor::new(1.0, 1.5);
309        let font = Font::new(
310            ab_glyph::FontArc::try_from_slice(include_bytes!(
311                "../../../assets/inter/Inter-Regular.ttf"
312            ))
313            .unwrap(),
314            "Inter".to_string(),
315            16.0.into(),
316            scale_factor,
317        );
318        let typesetter = Typesetter::new();
319        let bounds = LogicalPixelsRect::new(
320            LogicalPixelsPoint::new(0.0.into(), 0.0.into()),
321            LogicalPixelsPoint::new(100.0.into(), 100.0.into()),
322        );
323        let galley = typesetter.compose(
324            "Hello",
325            &font,
326            bounds,
327            TextAlignment::Left,
328            Alignment::TopLeft,
329            &scale_factor,
330        );
331        assert_eq!(galley.rect.height(), 16.0.into());
332        assert_eq!(galley.glyphs[0].glyph.rect.height, (16.0 * 1.5).into());
333    }
334}