epaint/text/
font.rs

1#![expect(clippy::mem_forget)]
2
3use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
4use self_cell::self_cell;
5use skrifa::{
6    MetadataProvider as _,
7    raw::{TableProvider as _, tables::kern::SubtableKind},
8};
9use std::collections::BTreeMap;
10use vello_cpu::{color, kurbo};
11
12use crate::{
13    TextOptions, TextureAtlas,
14    text::{
15        FontTweak, VariationCoords,
16        fonts::{Blob, CachedFamily, FontFaceKey},
17    },
18};
19
20// ----------------------------------------------------------------------------
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
24pub struct UvRect {
25    /// X/Y offset for nice rendering (unit: points).
26    pub offset: Vec2,
27
28    /// Screen size (in points) of this glyph.
29    /// Note that the height is different from the font height.
30    pub size: Vec2,
31
32    /// Top left corner UV in texture.
33    pub min: [u16; 2],
34
35    /// Bottom right corner (exclusive).
36    pub max: [u16; 2],
37}
38
39impl UvRect {
40    pub fn is_nothing(&self) -> bool {
41        self.min == self.max
42    }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct GlyphInfo {
47    /// Used for pair-kerning.
48    ///
49    /// Doesn't need to be unique.
50    ///
51    /// Is `None` for a special "invisible" glyph.
52    pub(crate) id: Option<skrifa::GlyphId>,
53
54    /// In [`skrifa`]s "unscaled" coordinate system.
55    pub advance_width_unscaled: OrderedFloat<f32>,
56}
57
58impl GlyphInfo {
59    /// A valid, but invisible, glyph of zero-width.
60    pub const INVISIBLE: Self = Self {
61        id: None,
62        advance_width_unscaled: OrderedFloat(0.0),
63    };
64}
65
66// Subpixel binning, taken from cosmic-text:
67// https://github.com/pop-os/cosmic-text/blob/974ddaed96b334f560b606ebe5d2ca2d2f9f23ef/src/glyph_cache.rs
68
69/// Bin for subpixel positioning of glyphs.
70///
71/// For accurate glyph positioning, we want to render each glyph at a subpixel coordinate. However, we also want to
72/// cache each glyph's bitmap. As a compromise, we bin each subpixel offset into one of four fractional values. This
73/// means one glyph can have up to four subpixel-positioned bitmaps in the cache.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
75pub(super) enum SubpixelBin {
76    #[default]
77    Zero,
78    One,
79    Two,
80    Three,
81}
82
83impl SubpixelBin {
84    /// Bin the given position and return the new integral coordinate.
85    fn new(pos: f32) -> (i32, Self) {
86        let trunc = pos as i32;
87        let fract = pos - trunc as f32;
88
89        #[expect(clippy::collapsible_else_if)]
90        if pos.is_sign_negative() {
91            if fract > -0.125 {
92                (trunc, Self::Zero)
93            } else if fract > -0.375 {
94                (trunc - 1, Self::Three)
95            } else if fract > -0.625 {
96                (trunc - 1, Self::Two)
97            } else if fract > -0.875 {
98                (trunc - 1, Self::One)
99            } else {
100                (trunc - 1, Self::Zero)
101            }
102        } else {
103            if fract < 0.125 {
104                (trunc, Self::Zero)
105            } else if fract < 0.375 {
106                (trunc, Self::One)
107            } else if fract < 0.625 {
108                (trunc, Self::Two)
109            } else if fract < 0.875 {
110                (trunc, Self::Three)
111            } else {
112                (trunc + 1, Self::Zero)
113            }
114        }
115    }
116
117    pub fn as_float(&self) -> f32 {
118        match self {
119            Self::Zero => 0.0,
120            Self::One => 0.25,
121            Self::Two => 0.5,
122            Self::Three => 0.75,
123        }
124    }
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Default)]
128pub struct GlyphAllocation {
129    /// Used for pair-kerning.
130    ///
131    /// Doesn't need to be unique.
132    /// Use [`skrifa::GlyphId::NOTDEF`] if you just want to have an id, and don't care.
133    pub(crate) id: skrifa::GlyphId,
134
135    /// Unit: screen pixels.
136    pub advance_width_px: f32,
137
138    /// UV rectangle for drawing.
139    pub uv_rect: UvRect,
140}
141
142#[derive(Hash, PartialEq, Eq)]
143struct GlyphCacheKey(u64);
144
145impl nohash_hasher::IsEnabled for GlyphCacheKey {}
146
147impl GlyphCacheKey {
148    fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
149        let StyledMetrics {
150            pixels_per_point,
151            px_scale_factor,
152            ..
153        } = *metrics;
154        debug_assert!(
155            0.0 < pixels_per_point && pixels_per_point.is_finite(),
156            "Bad pixels_per_point {pixels_per_point}"
157        );
158        debug_assert!(
159            0.0 < px_scale_factor && px_scale_factor.is_finite(),
160            "Bad px_scale_factor: {px_scale_factor}"
161        );
162        Self(crate::util::hash((
163            glyph_id,
164            pixels_per_point.to_bits(),
165            px_scale_factor.to_bits(),
166            bin,
167        )))
168    }
169}
170
171// ----------------------------------------------------------------------------
172
173struct DependentFontData<'a> {
174    skrifa: skrifa::FontRef<'a>,
175    charmap: skrifa::charmap::Charmap<'a>,
176    outline_glyphs: skrifa::outline::OutlineGlyphCollection<'a>,
177    metrics: skrifa::metrics::Metrics,
178    glyph_metrics: skrifa::metrics::GlyphMetrics<'a>,
179    hinting_instance: Option<skrifa::outline::HintingInstance>,
180}
181
182self_cell! {
183    struct FontCell {
184        owner: Blob,
185
186        #[covariant]
187        dependent: DependentFontData,
188    }
189}
190
191impl FontCell {
192    fn px_scale_factor(&self, scale: f32) -> f32 {
193        let units_per_em = self.borrow_dependent().metrics.units_per_em as f32;
194        scale / units_per_em
195    }
196
197    fn allocate_glyph_uncached(
198        &mut self,
199        atlas: &mut TextureAtlas,
200        metrics: &StyledMetrics,
201        glyph_info: &GlyphInfo,
202        bin: SubpixelBin,
203        location: skrifa::instance::LocationRef<'_>,
204    ) -> Option<GlyphAllocation> {
205        let glyph_id = glyph_info.id?;
206
207        debug_assert!(
208            glyph_id != skrifa::GlyphId::NOTDEF,
209            "Can't allocate glyph for id 0"
210        );
211
212        let mut path = kurbo::BezPath::new();
213        let mut pen = VelloPen {
214            path: &mut path,
215            x_offset: bin.as_float() as f64,
216        };
217
218        self.with_dependent_mut(|_, font_data| {
219            let outline = font_data.outline_glyphs.get(glyph_id)?;
220
221            if let Some(hinting_instance) = &mut font_data.hinting_instance {
222                let size = skrifa::instance::Size::new(metrics.scale);
223                if hinting_instance.size() != size {
224                    hinting_instance
225                        .reconfigure(
226                            &font_data.outline_glyphs,
227                            size,
228                            location,
229                            skrifa::outline::Target::Smooth {
230                                mode: skrifa::outline::SmoothMode::Normal,
231                                symmetric_rendering: true,
232                                preserve_linear_metrics: true,
233                            },
234                        )
235                        .ok()?;
236                }
237                let draw_settings = skrifa::outline::DrawSettings::hinted(hinting_instance, false);
238                outline.draw(draw_settings, &mut pen).ok()?;
239            } else {
240                let draw_settings = skrifa::outline::DrawSettings::unhinted(
241                    skrifa::instance::Size::new(metrics.scale),
242                    location,
243                );
244                outline.draw(draw_settings, &mut pen).ok()?;
245            }
246
247            Some(())
248        })?;
249
250        let bounds = path.control_box().expand();
251        let width = bounds.width() as u16;
252        let height = bounds.height() as u16;
253
254        let mut ctx = vello_cpu::RenderContext::new(width, height);
255        ctx.set_transform(kurbo::Affine::translate((-bounds.x0, -bounds.y0)));
256        ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
257        ctx.fill_path(&path);
258        let mut dest = vello_cpu::Pixmap::new(width, height);
259        ctx.render_to_pixmap(&mut dest);
260        let uv_rect = if width == 0 || height == 0 {
261            UvRect::default()
262        } else {
263            let glyph_pos = {
264                let alpha_from_coverage = atlas.options().alpha_from_coverage;
265                let (glyph_pos, image) = atlas.allocate((width as usize, height as usize));
266                let pixels = dest.data_as_u8_slice();
267                for y in 0..height as usize {
268                    for x in 0..width as usize {
269                        image[(x + glyph_pos.0, y + glyph_pos.1)] = alpha_from_coverage
270                            .color_from_coverage(
271                                pixels[((y * width as usize) + x) * 4 + 3] as f32 / 255.0,
272                            );
273                    }
274                }
275                glyph_pos
276            };
277            let offset_in_pixels = vec2(bounds.x0 as f32, bounds.y0 as f32);
278            let offset =
279                offset_in_pixels / metrics.pixels_per_point + metrics.y_offset_in_points * Vec2::Y;
280            UvRect {
281                offset,
282                size: vec2(width as f32, height as f32) / metrics.pixels_per_point,
283                min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
284                max: [
285                    (glyph_pos.0 + width as usize) as u16,
286                    (glyph_pos.1 + height as usize) as u16,
287                ],
288            }
289        };
290
291        Some(GlyphAllocation {
292            id: glyph_id,
293            advance_width_px: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor,
294            uv_rect,
295        })
296    }
297}
298
299struct VelloPen<'a> {
300    path: &'a mut kurbo::BezPath,
301    x_offset: f64,
302}
303
304impl skrifa::outline::OutlinePen for VelloPen<'_> {
305    fn move_to(&mut self, x: f32, y: f32) {
306        self.path.move_to((x as f64 + self.x_offset, -y as f64));
307    }
308
309    fn line_to(&mut self, x: f32, y: f32) {
310        self.path.line_to((x as f64 + self.x_offset, -y as f64));
311    }
312
313    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
314        self.path.quad_to(
315            (cx0 as f64 + self.x_offset, -cy0 as f64),
316            (x as f64 + self.x_offset, -y as f64),
317        );
318    }
319
320    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
321        self.path.curve_to(
322            (cx0 as f64 + self.x_offset, -cy0 as f64),
323            (cx1 as f64 + self.x_offset, -cy1 as f64),
324            (x as f64 + self.x_offset, -y as f64),
325        );
326    }
327
328    fn close(&mut self) {
329        self.path.close_path();
330    }
331}
332
333/// A specific font face.
334/// The interface uses points as the unit for everything.
335pub struct FontFace {
336    name: String,
337    font: FontCell,
338    tweak: FontTweak,
339
340    glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
341    glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
342}
343
344impl FontFace {
345    pub fn new(
346        options: TextOptions,
347        name: String,
348        font_data: Blob,
349        index: u32,
350        tweak: FontTweak,
351    ) -> Result<Self, Box<dyn std::error::Error>> {
352        let font = FontCell::try_new(font_data, |font_data| {
353            let skrifa_font =
354                skrifa::FontRef::from_index(AsRef::<[u8]>::as_ref(font_data.as_ref()), index)?;
355
356            let charmap = skrifa_font.charmap();
357            let glyphs = skrifa_font.outline_glyphs();
358
359            // Note: We use default location here during initialization because
360            // the actual weight will be applied via the stored location during rendering.
361            // The metrics won't be significantly different at this unscaled size.
362            let metrics = skrifa_font.metrics(
363                skrifa::instance::Size::unscaled(),
364                skrifa::instance::LocationRef::default(),
365            );
366            let glyph_metrics = skrifa_font.glyph_metrics(
367                skrifa::instance::Size::unscaled(),
368                skrifa::instance::LocationRef::default(),
369            );
370
371            let hinting_enabled = tweak.hinting_override.unwrap_or(options.font_hinting);
372            let hinting_instance = hinting_enabled
373                .then(|| {
374                    // It doesn't really matter what we put here for options. Since the size is `unscaled()`, we will
375                    // always reconfigure this hinting instance with the real options when rendering for the first time.
376                    skrifa::outline::HintingInstance::new(
377                        &glyphs,
378                        skrifa::instance::Size::unscaled(),
379                        skrifa::instance::LocationRef::default(),
380                        skrifa::outline::Target::default(),
381                    )
382                    .ok()
383                })
384                .flatten();
385
386            Ok::<DependentFontData<'_>, Box<dyn std::error::Error>>(DependentFontData {
387                skrifa: skrifa_font,
388                charmap,
389                outline_glyphs: glyphs,
390                metrics,
391                glyph_metrics,
392                hinting_instance,
393            })
394        })?;
395
396        Ok(Self {
397            name,
398            font,
399            tweak,
400            glyph_info_cache: Default::default(),
401            glyph_alloc_cache: Default::default(),
402        })
403    }
404
405    /// Code points that will always be replaced by the replacement character.
406    ///
407    /// See also [`invisible_char`].
408    fn ignore_character(&self, chr: char) -> bool {
409        use crate::text::FontDefinitions;
410
411        if !FontDefinitions::builtin_font_names().contains(&self.name.as_str()) {
412            return false;
413        }
414
415        matches!(
416            chr,
417            // Strip out a religious symbol with secondary nefarious interpretation:
418            '\u{534d}' | '\u{5350}' |
419
420            // Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
421            '\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
422        )
423    }
424
425    /// An un-ordered iterator over all supported characters.
426    fn characters(&self) -> impl Iterator<Item = char> + '_ {
427        self.font
428            .borrow_dependent()
429            .charmap
430            .mappings()
431            .filter_map(|(chr, _)| char::from_u32(chr).filter(|c| !self.ignore_character(*c)))
432    }
433
434    /// `\n` will result in `None`
435    pub(super) fn glyph_info(&mut self, c: char) -> Option<GlyphInfo> {
436        if let Some(glyph_info) = self.glyph_info_cache.get(&c) {
437            return Some(*glyph_info);
438        }
439
440        if self.ignore_character(c) {
441            return None; // these will result in the replacement character when rendering
442        }
443
444        if c == '\t'
445            && let Some(space) = self.glyph_info(' ')
446        {
447            let glyph_info = GlyphInfo {
448                advance_width_unscaled: (crate::text::TAB_SIZE as f32
449                    * space.advance_width_unscaled.0)
450                    .into(),
451                ..space
452            };
453            self.glyph_info_cache.insert(c, glyph_info);
454            return Some(glyph_info);
455        }
456
457        if c == '\u{2009}' {
458            // Thin space, often used as thousands deliminator: 1 234 567 890
459            // https://www.compart.com/en/unicode/U+2009
460            // https://en.wikipedia.org/wiki/Thin_space
461
462            if let Some(space) = self.glyph_info(' ') {
463                let em = self.font.borrow_dependent().metrics.units_per_em as f32;
464                let advance_width = f32::min(em / 6.0, space.advance_width_unscaled.0 * 0.5); // TODO(emilk): make configurable
465                let glyph_info = GlyphInfo {
466                    advance_width_unscaled: advance_width.into(),
467                    ..space
468                };
469                self.glyph_info_cache.insert(c, glyph_info);
470                return Some(glyph_info);
471            }
472        }
473
474        if invisible_char(c) {
475            let glyph_info = GlyphInfo::INVISIBLE;
476            self.glyph_info_cache.insert(c, glyph_info);
477            return Some(glyph_info);
478        }
479
480        let font_data = self.font.borrow_dependent();
481
482        // Add new character:
483        let glyph_id = font_data
484            .charmap
485            .map(c)
486            .filter(|id| *id != skrifa::GlyphId::NOTDEF)?;
487
488        let glyph_info = GlyphInfo {
489            id: Some(glyph_id),
490            advance_width_unscaled: font_data
491                .glyph_metrics
492                .advance_width(glyph_id)
493                .unwrap_or_default()
494                .into(),
495        };
496        self.glyph_info_cache.insert(c, glyph_info);
497        Some(glyph_info)
498    }
499
500    #[inline]
501    pub(super) fn pair_kerning_pixels(
502        &self,
503        metrics: &StyledMetrics,
504        last_glyph_id: skrifa::GlyphId,
505        glyph_id: skrifa::GlyphId,
506    ) -> f32 {
507        let skrifa_font = &self.font.borrow_dependent().skrifa;
508        let Ok(kern) = skrifa_font.kern() else {
509            return 0.0;
510        };
511        kern.subtables()
512            .find_map(|st| match st.ok()?.kind().ok()? {
513                SubtableKind::Format0(table_ref) => table_ref.kerning(last_glyph_id, glyph_id),
514                SubtableKind::Format1(_) => None,
515                SubtableKind::Format2(subtable2) => subtable2.kerning(last_glyph_id, glyph_id),
516                SubtableKind::Format3(table_ref) => table_ref.kerning(last_glyph_id, glyph_id),
517            })
518            .unwrap_or_default() as f32
519            * metrics.px_scale_factor
520    }
521
522    #[inline]
523    pub fn pair_kerning(
524        &self,
525        metrics: &StyledMetrics,
526        last_glyph_id: skrifa::GlyphId,
527        glyph_id: skrifa::GlyphId,
528    ) -> f32 {
529        self.pair_kerning_pixels(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point
530    }
531
532    #[inline(always)]
533    pub fn styled_metrics(
534        &self,
535        pixels_per_point: f32,
536        font_size: f32,
537        coords: &VariationCoords,
538    ) -> StyledMetrics {
539        let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
540        let font_data = self.font.borrow_dependent();
541        let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
542        let descent = (font_data.metrics.descent * pt_scale_factor).round_ui();
543        let line_gap = (font_data.metrics.leading * pt_scale_factor).round_ui();
544
545        let scale = font_size * self.tweak.scale * pixels_per_point;
546        let px_scale_factor = self.font.px_scale_factor(scale);
547
548        let y_offset_in_points = ((font_size * self.tweak.scale * self.tweak.y_offset_factor)
549            + self.tweak.y_offset)
550            .round_ui();
551
552        let axes = font_data.skrifa.axes();
553        // Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the
554        // argument (probably from TextFormat).
555        let settings = self
556            .tweak
557            .coords
558            .as_ref()
559            .iter()
560            .chain(coords.as_ref().iter());
561        let location = axes.location(settings);
562
563        StyledMetrics {
564            pixels_per_point,
565            px_scale_factor,
566            scale,
567            y_offset_in_points,
568            ascent,
569            row_height: ascent - descent + line_gap,
570            location,
571        }
572    }
573
574    pub fn allocate_glyph(
575        &mut self,
576        atlas: &mut TextureAtlas,
577        metrics: &StyledMetrics,
578        glyph_info: GlyphInfo,
579        chr: char,
580        h_pos: f32,
581    ) -> (GlyphAllocation, i32) {
582        let advance_width_px = glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor;
583
584        let Some(glyph_id) = glyph_info.id else {
585            // Invisible.
586            return (GlyphAllocation::default(), h_pos as i32);
587        };
588
589        // CJK scripts contain a lot of characters and could hog the glyph atlas if we stored 4 subpixel offsets per
590        // glyph.
591        let (h_pos_round, bin) = if is_cjk(chr) {
592            (h_pos.round() as i32, SubpixelBin::Zero)
593        } else {
594            SubpixelBin::new(h_pos)
595        };
596
597        let entry = match self
598            .glyph_alloc_cache
599            .entry(GlyphCacheKey::new(glyph_id, metrics, bin))
600        {
601            std::collections::hash_map::Entry::Occupied(glyph_alloc) => {
602                let mut glyph_alloc = *glyph_alloc.get();
603                glyph_alloc.advance_width_px = advance_width_px; // Hack to get `\t` and thin space to work, since they use the same glyph id as ` ` (space).
604                return (glyph_alloc, h_pos_round);
605            }
606            std::collections::hash_map::Entry::Vacant(entry) => entry,
607        };
608
609        let allocation = self
610            .font
611            .allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into())
612            .unwrap_or_default();
613
614        entry.insert(allocation);
615        (allocation, h_pos_round)
616    }
617}
618
619// TODO(emilk): rename?
620/// Wrapper over multiple [`FontFace`] (e.g. a primary + fallbacks for emojis)
621pub struct Font<'a> {
622    pub(super) fonts_by_id: &'a mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
623    pub(super) cached_family: &'a mut CachedFamily,
624    pub(super) atlas: &'a mut TextureAtlas,
625}
626
627impl Font<'_> {
628    pub fn preload_characters(&mut self, s: &str) {
629        for c in s.chars() {
630            self.glyph_info(c);
631        }
632    }
633
634    /// All supported characters, and in which font they are available in.
635    pub fn characters(&mut self) -> &BTreeMap<char, Vec<String>> {
636        self.cached_family.characters.get_or_insert_with(|| {
637            let mut characters: BTreeMap<char, Vec<String>> = Default::default();
638            for font_id in &self.cached_family.fonts {
639                let font = self.fonts_by_id.get(font_id).expect("Nonexistent font ID");
640                for chr in font.characters() {
641                    characters.entry(chr).or_default().push(font.name.clone());
642                }
643            }
644            characters
645        })
646    }
647
648    pub fn styled_metrics(
649        &self,
650        pixels_per_point: f32,
651        font_size: f32,
652        coords: &VariationCoords,
653    ) -> StyledMetrics {
654        self.cached_family
655            .fonts
656            .first()
657            .and_then(|key| self.fonts_by_id.get(key))
658            .map(|font_face| font_face.styled_metrics(pixels_per_point, font_size, coords))
659            .unwrap_or_default()
660    }
661
662    /// Width of this character in points.
663    pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 {
664        let (key, glyph_info) = self.glyph_info(c);
665        if let Some(font) = &self.fonts_by_id.get(&key) {
666            glyph_info.advance_width_unscaled.0 * font.font.px_scale_factor(font_size)
667        } else {
668            0.0
669        }
670    }
671
672    /// Can we display this glyph?
673    pub fn has_glyph(&mut self, c: char) -> bool {
674        self.glyph_info(c) != self.cached_family.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself đŸ€Šâ€â™‚ïž
675    }
676
677    /// Can we display all the glyphs in this text?
678    pub fn has_glyphs(&mut self, s: &str) -> bool {
679        s.chars().all(|c| self.has_glyph(c))
680    }
681
682    /// `\n` will (intentionally) show up as the replacement character.
683    pub(crate) fn glyph_info(&mut self, c: char) -> (FontFaceKey, GlyphInfo) {
684        if let Some(font_index_glyph_info) = self.cached_family.glyph_info_cache.get(&c) {
685            return *font_index_glyph_info;
686        }
687
688        let font_index_glyph_info = self
689            .cached_family
690            .glyph_info_no_cache_or_fallback(c, self.fonts_by_id);
691        let font_index_glyph_info =
692            font_index_glyph_info.unwrap_or(self.cached_family.replacement_glyph);
693        self.cached_family
694            .glyph_info_cache
695            .insert(c, font_index_glyph_info);
696        font_index_glyph_info
697    }
698}
699
700/// Metrics for a font at a specific screen-space scale.
701#[derive(Clone, Debug, PartialEq, Default)]
702pub struct StyledMetrics {
703    /// The DPI part of the screen-space scale.
704    pub pixels_per_point: f32,
705
706    /// Scale factor, relative to the font's units per em (so, probably much less than 1).
707    ///
708    /// Translates "unscaled" units to physical (screen) pixels.
709    pub px_scale_factor: f32,
710
711    /// Absolute scale in screen pixels, for skrifa.
712    pub scale: f32,
713
714    /// Vertical offset, in UI points (not screen-space).
715    pub y_offset_in_points: f32,
716
717    /// This is the distance from the top to the baseline.
718    ///
719    /// Unit: points.
720    pub ascent: f32,
721
722    /// Height of one row of text in points.
723    ///
724    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
725    pub row_height: f32,
726
727    /// Resolved variation coordinates.
728    pub location: skrifa::instance::Location,
729}
730
731/// Code points that will always be invisible (zero width).
732///
733/// See also [`FontFace::ignore_character`].
734#[inline]
735fn invisible_char(c: char) -> bool {
736    if c == '\r' {
737        // A character most vile and pernicious. Don't display it.
738        return true;
739    }
740
741    // See https://github.com/emilk/egui/issues/336
742
743    // From https://www.fileformat.info/info/unicode/category/Cf/list.htm
744
745    // TODO(emilk): heed bidi characters
746
747    matches!(
748        c,
749        '\u{200B}' // ZERO WIDTH SPACE
750            | '\u{200C}' // ZERO WIDTH NON-JOINER
751            | '\u{200D}' // ZERO WIDTH JOINER
752            | '\u{200E}' // LEFT-TO-RIGHT MARK
753            | '\u{200F}' // RIGHT-TO-LEFT MARK
754            | '\u{202A}' // LEFT-TO-RIGHT EMBEDDING
755            | '\u{202B}' // RIGHT-TO-LEFT EMBEDDING
756            | '\u{202C}' // POP DIRECTIONAL FORMATTING
757            | '\u{202D}' // LEFT-TO-RIGHT OVERRIDE
758            | '\u{202E}' // RIGHT-TO-LEFT OVERRIDE
759            | '\u{2060}' // WORD JOINER
760            | '\u{2061}' // FUNCTION APPLICATION
761            | '\u{2062}' // INVISIBLE TIMES
762            | '\u{2063}' // INVISIBLE SEPARATOR
763            | '\u{2064}' // INVISIBLE PLUS
764            | '\u{2066}' // LEFT-TO-RIGHT ISOLATE
765            | '\u{2067}' // RIGHT-TO-LEFT ISOLATE
766            | '\u{2068}' // FIRST STRONG ISOLATE
767            | '\u{2069}' // POP DIRECTIONAL ISOLATE
768            | '\u{206A}' // INHIBIT SYMMETRIC SWAPPING
769            | '\u{206B}' // ACTIVATE SYMMETRIC SWAPPING
770            | '\u{206C}' // INHIBIT ARABIC FORM SHAPING
771            | '\u{206D}' // ACTIVATE ARABIC FORM SHAPING
772            | '\u{206E}' // NATIONAL DIGIT SHAPES
773            | '\u{206F}' // NOMINAL DIGIT SHAPES
774            | '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
775    )
776}
777
778#[inline]
779pub(super) fn is_cjk_ideograph(c: char) -> bool {
780    ('\u{4E00}' <= c && c <= '\u{9FFF}')
781        || ('\u{3400}' <= c && c <= '\u{4DBF}')
782        || ('\u{2B740}' <= c && c <= '\u{2B81F}')
783}
784
785#[inline]
786pub(super) fn is_kana(c: char) -> bool {
787    ('\u{3040}' <= c && c <= '\u{309F}') // Hiragana block
788        || ('\u{30A0}' <= c && c <= '\u{30FF}') // Katakana block
789}
790
791#[inline]
792pub(super) fn is_cjk(c: char) -> bool {
793    // TODO(bigfarts): Add support for Korean Hangul.
794    is_cjk_ideograph(c) || is_kana(c)
795}
796
797#[inline]
798pub(super) fn is_cjk_break_allowed(c: char) -> bool {
799    // See: https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages#Characters_not_permitted_on_the_start_of_a_line.
800    !")]ïœă€•ă€‰ă€‹ă€ă€ă€‘ă€™ă€—ă€Ÿ'\"ïœ Â»ăƒœăƒŸăƒŒă‚Ąă‚Łă‚„ă‚§ă‚©ăƒƒăƒŁăƒ„ăƒ§ăƒźăƒ”ăƒ¶ăăƒă…ă‡ă‰ăŁă‚ƒă‚…ă‚‡ă‚Žă‚•ă‚–ă‡°ă‡±ă‡Čㇳ㇎㇔ㇶㇷ㇞ă‡čă‡șă‡»ă‡Œă‡œă‡Ÿă‡żă€…ă€»â€ă‚ â€“ă€œ?!â€Œâ‡âˆâ‰ăƒ»ă€:;,。.".contains(c)
801}