epaint/text/
font.rs

1use std::collections::BTreeMap;
2
3use ab_glyph::{Font as _, OutlinedGlyph, PxScale};
4use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
5
6use crate::{
7    TextureAtlas,
8    text::{
9        FontTweak,
10        fonts::{CachedFamily, FontFaceKey},
11    },
12};
13
14// ----------------------------------------------------------------------------
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
18pub struct UvRect {
19    /// X/Y offset for nice rendering (unit: points).
20    pub offset: Vec2,
21
22    /// Screen size (in points) of this glyph.
23    /// Note that the height is different from the font height.
24    pub size: Vec2,
25
26    /// Top left corner UV in texture.
27    pub min: [u16; 2],
28
29    /// Bottom right corner (exclusive).
30    pub max: [u16; 2],
31}
32
33impl UvRect {
34    pub fn is_nothing(&self) -> bool {
35        self.min == self.max
36    }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub struct GlyphInfo {
41    /// Used for pair-kerning.
42    ///
43    /// Doesn't need to be unique.
44    ///
45    /// Is `None` for a special "invisible" glyph.
46    pub(crate) id: Option<ab_glyph::GlyphId>,
47
48    /// In [`ab_glyph`]s "unscaled" coordinate system.
49    pub advance_width_unscaled: OrderedFloat<f32>,
50}
51
52impl GlyphInfo {
53    /// A valid, but invisible, glyph of zero-width.
54    pub const INVISIBLE: Self = Self {
55        id: None,
56        advance_width_unscaled: OrderedFloat(0.0),
57    };
58}
59
60// Subpixel binning, taken from cosmic-text:
61// https://github.com/pop-os/cosmic-text/blob/974ddaed96b334f560b606ebe5d2ca2d2f9f23ef/src/glyph_cache.rs
62
63/// Bin for subpixel positioning of glyphs.
64///
65/// For accurate glyph positioning, we want to render each glyph at a subpixel coordinate. However, we also want to
66/// cache each glyph's bitmap. As a compromise, we bin each subpixel offset into one of four fractional values. This
67/// means one glyph can have up to four subpixel-positioned bitmaps in the cache.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
69pub(super) enum SubpixelBin {
70    #[default]
71    Zero,
72    One,
73    Two,
74    Three,
75}
76
77impl SubpixelBin {
78    /// Bin the given position and return the new integral coordinate.
79    fn new(pos: f32) -> (i32, Self) {
80        let trunc = pos as i32;
81        let fract = pos - trunc as f32;
82
83        #[expect(clippy::collapsible_else_if)]
84        if pos.is_sign_negative() {
85            if fract > -0.125 {
86                (trunc, Self::Zero)
87            } else if fract > -0.375 {
88                (trunc - 1, Self::Three)
89            } else if fract > -0.625 {
90                (trunc - 1, Self::Two)
91            } else if fract > -0.875 {
92                (trunc - 1, Self::One)
93            } else {
94                (trunc - 1, Self::Zero)
95            }
96        } else {
97            if fract < 0.125 {
98                (trunc, Self::Zero)
99            } else if fract < 0.375 {
100                (trunc, Self::One)
101            } else if fract < 0.625 {
102                (trunc, Self::Two)
103            } else if fract < 0.875 {
104                (trunc, Self::Three)
105            } else {
106                (trunc + 1, Self::Zero)
107            }
108        }
109    }
110
111    pub fn as_float(&self) -> f32 {
112        match self {
113            Self::Zero => 0.0,
114            Self::One => 0.25,
115            Self::Two => 0.5,
116            Self::Three => 0.75,
117        }
118    }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Default)]
122pub struct GlyphAllocation {
123    /// Used for pair-kerning.
124    ///
125    /// Doesn't need to be unique.
126    /// Use `ab_glyph::GlyphId(0)` if you just want to have an id, and don't care.
127    pub(crate) id: ab_glyph::GlyphId,
128
129    /// Unit: screen pixels.
130    pub advance_width_px: f32,
131
132    /// UV rectangle for drawing.
133    pub uv_rect: UvRect,
134}
135
136#[derive(Hash, PartialEq, Eq)]
137struct GlyphCacheKey(u64);
138
139impl nohash_hasher::IsEnabled for GlyphCacheKey {}
140
141impl GlyphCacheKey {
142    fn new(glyph_id: ab_glyph::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
143        let ScaledMetrics {
144            pixels_per_point,
145            px_scale_factor,
146            ..
147        } = *metrics;
148        debug_assert!(
149            0.0 < pixels_per_point && pixels_per_point.is_finite(),
150            "Bad pixels_per_point {pixels_per_point}"
151        );
152        debug_assert!(
153            0.0 < px_scale_factor && px_scale_factor.is_finite(),
154            "Bad px_scale_factor: {px_scale_factor}"
155        );
156        Self(crate::util::hash((
157            glyph_id,
158            pixels_per_point.to_bits(),
159            px_scale_factor.to_bits(),
160            bin,
161        )))
162    }
163}
164
165// ----------------------------------------------------------------------------
166
167/// A specific font face.
168/// The interface uses points as the unit for everything.
169pub struct FontImpl {
170    name: String,
171    ab_glyph_font: ab_glyph::FontArc,
172    tweak: FontTweak,
173    glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
174    glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
175}
176
177trait FontExt {
178    fn px_scale_factor(&self, scale: f32) -> f32;
179}
180
181impl<T> FontExt for T
182where
183    T: ab_glyph::Font,
184{
185    fn px_scale_factor(&self, scale: f32) -> f32 {
186        let units_per_em = self.units_per_em().unwrap_or_else(|| {
187            panic!("The font unit size exceeds the expected range (16..=16384)")
188        });
189        scale / units_per_em
190    }
191}
192
193impl FontImpl {
194    pub fn new(name: String, ab_glyph_font: ab_glyph::FontArc, tweak: FontTweak) -> Self {
195        Self {
196            name,
197            ab_glyph_font,
198            tweak,
199            glyph_info_cache: Default::default(),
200            glyph_alloc_cache: Default::default(),
201        }
202    }
203
204    /// Code points that will always be replaced by the replacement character.
205    ///
206    /// See also [`invisible_char`].
207    fn ignore_character(&self, chr: char) -> bool {
208        use crate::text::FontDefinitions;
209
210        if !FontDefinitions::builtin_font_names().contains(&self.name.as_str()) {
211            return false;
212        }
213
214        matches!(
215            chr,
216            // Strip out a religious symbol with secondary nefarious interpretation:
217            '\u{534d}' | '\u{5350}' |
218
219            // Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
220            '\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
221        )
222    }
223
224    /// An un-ordered iterator over all supported characters.
225    fn characters(&self) -> impl Iterator<Item = char> + '_ {
226        self.ab_glyph_font
227            .codepoint_ids()
228            .map(|(_, chr)| chr)
229            .filter(|&chr| !self.ignore_character(chr))
230    }
231
232    /// `\n` will result in `None`
233    pub(super) fn glyph_info(&mut self, c: char) -> Option<GlyphInfo> {
234        if let Some(glyph_info) = self.glyph_info_cache.get(&c) {
235            return Some(*glyph_info);
236        }
237
238        if self.ignore_character(c) {
239            return None; // these will result in the replacement character when rendering
240        }
241
242        if c == '\t'
243            && let Some(space) = self.glyph_info(' ')
244        {
245            let glyph_info = GlyphInfo {
246                advance_width_unscaled: (crate::text::TAB_SIZE as f32
247                    * space.advance_width_unscaled.0)
248                    .into(),
249                ..space
250            };
251            self.glyph_info_cache.insert(c, glyph_info);
252            return Some(glyph_info);
253        }
254
255        if c == '\u{2009}' {
256            // Thin space, often used as thousands deliminator: 1 234 567 890
257            // https://www.compart.com/en/unicode/U+2009
258            // https://en.wikipedia.org/wiki/Thin_space
259
260            if let Some(space) = self.glyph_info(' ') {
261                let em = self.ab_glyph_font.units_per_em().unwrap_or(1.0);
262                let advance_width = f32::min(em / 6.0, space.advance_width_unscaled.0 * 0.5); // TODO(emilk): make configurable
263                let glyph_info = GlyphInfo {
264                    advance_width_unscaled: advance_width.into(),
265                    ..space
266                };
267                self.glyph_info_cache.insert(c, glyph_info);
268                return Some(glyph_info);
269            }
270        }
271
272        if invisible_char(c) {
273            let glyph_info = GlyphInfo::INVISIBLE;
274            self.glyph_info_cache.insert(c, glyph_info);
275            return Some(glyph_info);
276        }
277
278        // Add new character:
279        let glyph_id = self.ab_glyph_font.glyph_id(c);
280
281        if glyph_id.0 == 0 {
282            None // unsupported character
283        } else {
284            let glyph_info = GlyphInfo {
285                id: Some(glyph_id),
286                advance_width_unscaled: self.ab_glyph_font.h_advance_unscaled(glyph_id).into(),
287            };
288            self.glyph_info_cache.insert(c, glyph_info);
289            Some(glyph_info)
290        }
291    }
292
293    #[inline]
294    pub(super) fn pair_kerning_pixels(
295        &self,
296        metrics: &ScaledMetrics,
297        last_glyph_id: ab_glyph::GlyphId,
298        glyph_id: ab_glyph::GlyphId,
299    ) -> f32 {
300        self.ab_glyph_font.kern_unscaled(last_glyph_id, glyph_id) * metrics.px_scale_factor
301    }
302
303    #[inline]
304    pub fn pair_kerning(
305        &self,
306        metrics: &ScaledMetrics,
307        last_glyph_id: ab_glyph::GlyphId,
308        glyph_id: ab_glyph::GlyphId,
309    ) -> f32 {
310        self.pair_kerning_pixels(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point
311    }
312
313    #[inline(always)]
314    pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
315        let pt_scale_factor = self
316            .ab_glyph_font
317            .px_scale_factor(font_size * self.tweak.scale);
318        let ascent = (self.ab_glyph_font.ascent_unscaled() * pt_scale_factor).round_ui();
319        let descent = (self.ab_glyph_font.descent_unscaled() * pt_scale_factor).round_ui();
320        let line_gap = (self.ab_glyph_font.line_gap_unscaled() * pt_scale_factor).round_ui();
321
322        let scale = font_size * self.tweak.scale * pixels_per_point;
323        let px_scale_factor = self.ab_glyph_font.px_scale_factor(scale);
324
325        let y_offset_in_points = ((font_size * self.tweak.scale * self.tweak.y_offset_factor)
326            + self.tweak.y_offset)
327            .round_ui();
328
329        ScaledMetrics {
330            pixels_per_point,
331            px_scale_factor,
332            y_offset_in_points,
333            ascent,
334            row_height: ascent - descent + line_gap,
335        }
336    }
337
338    pub fn allocate_glyph(
339        &mut self,
340        atlas: &mut TextureAtlas,
341        metrics: &ScaledMetrics,
342        glyph_info: GlyphInfo,
343        chr: char,
344        h_pos: f32,
345    ) -> (GlyphAllocation, i32) {
346        let advance_width_px = glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor;
347
348        let Some(glyph_id) = glyph_info.id else {
349            // Invisible.
350            return (GlyphAllocation::default(), h_pos as i32);
351        };
352
353        // CJK scripts contain a lot of characters and could hog the glyph atlas if we stored 4 subpixel offsets per
354        // glyph.
355        let (h_pos_round, bin) = if is_cjk(chr) {
356            (h_pos.round() as i32, SubpixelBin::Zero)
357        } else {
358            SubpixelBin::new(h_pos)
359        };
360
361        let entry = match self
362            .glyph_alloc_cache
363            .entry(GlyphCacheKey::new(glyph_id, metrics, bin))
364        {
365            std::collections::hash_map::Entry::Occupied(glyph_alloc) => {
366                let mut glyph_alloc = *glyph_alloc.get();
367                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).
368                return (glyph_alloc, h_pos_round);
369            }
370            std::collections::hash_map::Entry::Vacant(entry) => entry,
371        };
372
373        debug_assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0");
374
375        let uv_rect = self.ab_glyph_font.outline(glyph_id).map(|outline| {
376            let glyph = ab_glyph::Glyph {
377                id: glyph_id,
378                // We bypass ab-glyph's scaling method because it uses the wrong scale
379                // (https://github.com/alexheretic/ab-glyph/issues/15), and this field is never accessed when
380                // rasterizing. We can just put anything here.
381                scale: PxScale::from(0.0),
382                position: ab_glyph::Point {
383                    x: bin.as_float(),
384                    y: 0.0,
385                },
386            };
387            let outlined = OutlinedGlyph::new(
388                glyph,
389                outline,
390                ab_glyph::PxScaleFactor {
391                    horizontal: metrics.px_scale_factor,
392                    vertical: metrics.px_scale_factor,
393                },
394            );
395            let bb = outlined.px_bounds();
396            let glyph_width = bb.width() as usize;
397            let glyph_height = bb.height() as usize;
398            if glyph_width == 0 || glyph_height == 0 {
399                UvRect::default()
400            } else {
401                let glyph_pos = {
402                    let text_alpha_from_coverage = atlas.text_alpha_from_coverage;
403                    let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
404                    outlined.draw(|x, y, v| {
405                        if 0.0 < v {
406                            let px = glyph_pos.0 + x as usize;
407                            let py = glyph_pos.1 + y as usize;
408                            image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v);
409                        }
410                    });
411                    glyph_pos
412                };
413
414                let offset_in_pixels = vec2(bb.min.x, bb.min.y);
415                let offset = offset_in_pixels / metrics.pixels_per_point
416                    + metrics.y_offset_in_points * Vec2::Y;
417                UvRect {
418                    offset,
419                    size: vec2(glyph_width as f32, glyph_height as f32) / metrics.pixels_per_point,
420                    min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
421                    max: [
422                        (glyph_pos.0 + glyph_width) as u16,
423                        (glyph_pos.1 + glyph_height) as u16,
424                    ],
425                }
426            }
427        });
428        let uv_rect = uv_rect.unwrap_or_default();
429
430        let allocation = GlyphAllocation {
431            id: glyph_id,
432            advance_width_px,
433            uv_rect,
434        };
435        entry.insert(allocation);
436        (allocation, h_pos_round)
437    }
438}
439
440// TODO(emilk): rename?
441/// Wrapper over multiple [`FontImpl`] (e.g. a primary + fallbacks for emojis)
442pub struct Font<'a> {
443    pub(super) fonts_by_id: &'a mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
444    pub(super) cached_family: &'a mut CachedFamily,
445    pub(super) atlas: &'a mut TextureAtlas,
446}
447
448impl Font<'_> {
449    pub fn preload_characters(&mut self, s: &str) {
450        for c in s.chars() {
451            self.glyph_info(c);
452        }
453    }
454
455    /// All supported characters, and in which font they are available in.
456    pub fn characters(&mut self) -> &BTreeMap<char, Vec<String>> {
457        self.cached_family.characters.get_or_insert_with(|| {
458            let mut characters: BTreeMap<char, Vec<String>> = Default::default();
459            for font_id in &self.cached_family.fonts {
460                let font = self.fonts_by_id.get(font_id).expect("Nonexistent font ID");
461                for chr in font.characters() {
462                    characters.entry(chr).or_default().push(font.name.clone());
463                }
464            }
465            characters
466        })
467    }
468
469    pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
470        self.cached_family
471            .fonts
472            .first()
473            .and_then(|key| self.fonts_by_id.get(key))
474            .map(|font_impl| font_impl.scaled_metrics(pixels_per_point, font_size))
475            .unwrap_or_default()
476    }
477
478    /// Width of this character in points.
479    pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 {
480        let (key, glyph_info) = self.glyph_info(c);
481        if let Some(font) = &self.fonts_by_id.get(&key) {
482            glyph_info.advance_width_unscaled.0 * font.ab_glyph_font.px_scale_factor(font_size)
483        } else {
484            0.0
485        }
486    }
487
488    /// Can we display this glyph?
489    pub fn has_glyph(&mut self, c: char) -> bool {
490        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 đŸ€Šâ€â™‚ïž
491    }
492
493    /// Can we display all the glyphs in this text?
494    pub fn has_glyphs(&mut self, s: &str) -> bool {
495        s.chars().all(|c| self.has_glyph(c))
496    }
497
498    /// `\n` will (intentionally) show up as the replacement character.
499    pub(crate) fn glyph_info(&mut self, c: char) -> (FontFaceKey, GlyphInfo) {
500        if let Some(font_index_glyph_info) = self.cached_family.glyph_info_cache.get(&c) {
501            return *font_index_glyph_info;
502        }
503
504        let font_index_glyph_info = self
505            .cached_family
506            .glyph_info_no_cache_or_fallback(c, self.fonts_by_id);
507        let font_index_glyph_info =
508            font_index_glyph_info.unwrap_or(self.cached_family.replacement_glyph);
509        self.cached_family
510            .glyph_info_cache
511            .insert(c, font_index_glyph_info);
512        font_index_glyph_info
513    }
514}
515
516/// Metrics for a font at a specific screen-space scale.
517#[derive(Clone, Copy, Debug, PartialEq, Default)]
518pub struct ScaledMetrics {
519    /// The DPI part of the screen-space scale.
520    pub pixels_per_point: f32,
521
522    /// Scale factor, relative to the font's units per em (so, probably much less than 1).
523    ///
524    /// Translates "unscaled" units to physical (screen) pixels.
525    pub px_scale_factor: f32,
526
527    /// Vertical offset, in UI points.
528    pub y_offset_in_points: f32,
529
530    /// This is the distance from the top to the baseline.
531    ///
532    /// Unit: points.
533    pub ascent: f32,
534
535    /// Height of one row of text in points.
536    ///
537    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
538    pub row_height: f32,
539}
540
541/// Code points that will always be invisible (zero width).
542///
543/// See also [`FontImpl::ignore_character`].
544#[inline]
545fn invisible_char(c: char) -> bool {
546    if c == '\r' {
547        // A character most vile and pernicious. Don't display it.
548        return true;
549    }
550
551    // See https://github.com/emilk/egui/issues/336
552
553    // From https://www.fileformat.info/info/unicode/category/Cf/list.htm
554
555    // TODO(emilk): heed bidi characters
556
557    matches!(
558        c,
559        '\u{200B}' // ZERO WIDTH SPACE
560            | '\u{200C}' // ZERO WIDTH NON-JOINER
561            | '\u{200D}' // ZERO WIDTH JOINER
562            | '\u{200E}' // LEFT-TO-RIGHT MARK
563            | '\u{200F}' // RIGHT-TO-LEFT MARK
564            | '\u{202A}' // LEFT-TO-RIGHT EMBEDDING
565            | '\u{202B}' // RIGHT-TO-LEFT EMBEDDING
566            | '\u{202C}' // POP DIRECTIONAL FORMATTING
567            | '\u{202D}' // LEFT-TO-RIGHT OVERRIDE
568            | '\u{202E}' // RIGHT-TO-LEFT OVERRIDE
569            | '\u{2060}' // WORD JOINER
570            | '\u{2061}' // FUNCTION APPLICATION
571            | '\u{2062}' // INVISIBLE TIMES
572            | '\u{2063}' // INVISIBLE SEPARATOR
573            | '\u{2064}' // INVISIBLE PLUS
574            | '\u{2066}' // LEFT-TO-RIGHT ISOLATE
575            | '\u{2067}' // RIGHT-TO-LEFT ISOLATE
576            | '\u{2068}' // FIRST STRONG ISOLATE
577            | '\u{2069}' // POP DIRECTIONAL ISOLATE
578            | '\u{206A}' // INHIBIT SYMMETRIC SWAPPING
579            | '\u{206B}' // ACTIVATE SYMMETRIC SWAPPING
580            | '\u{206C}' // INHIBIT ARABIC FORM SHAPING
581            | '\u{206D}' // ACTIVATE ARABIC FORM SHAPING
582            | '\u{206E}' // NATIONAL DIGIT SHAPES
583            | '\u{206F}' // NOMINAL DIGIT SHAPES
584            | '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
585    )
586}
587
588#[inline]
589pub(super) fn is_cjk_ideograph(c: char) -> bool {
590    ('\u{4E00}' <= c && c <= '\u{9FFF}')
591        || ('\u{3400}' <= c && c <= '\u{4DBF}')
592        || ('\u{2B740}' <= c && c <= '\u{2B81F}')
593}
594
595#[inline]
596pub(super) fn is_kana(c: char) -> bool {
597    ('\u{3040}' <= c && c <= '\u{309F}') // Hiragana block
598        || ('\u{30A0}' <= c && c <= '\u{30FF}') // Katakana block
599}
600
601#[inline]
602pub(super) fn is_cjk(c: char) -> bool {
603    // TODO(bigfarts): Add support for Korean Hangul.
604    is_cjk_ideograph(c) || is_kana(c)
605}
606
607#[inline]
608pub(super) fn is_cjk_break_allowed(c: char) -> bool {
609    // See: https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages#Characters_not_permitted_on_the_start_of_a_line.
610    !")]ïœă€•ă€‰ă€‹ă€ă€ă€‘ă€™ă€—ă€Ÿ'\"ïœ Â»ăƒœăƒŸăƒŒă‚Ąă‚Łă‚„ă‚§ă‚©ăƒƒăƒŁăƒ„ăƒ§ăƒźăƒ”ăƒ¶ăăƒă…ă‡ă‰ăŁă‚ƒă‚…ă‚‡ă‚Žă‚•ă‚–ă‡°ă‡±ă‡Čㇳ㇎㇔ㇶㇷ㇞ă‡čă‡șă‡»ă‡Œă‡œă‡Ÿă‡żă€…ă€»â€ă‚ â€“ă€œ?!â€Œâ‡âˆâ‰ăƒ»ă€:;,。.".contains(c)
611}