epaint/text/
font.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use emath::{GuiRounding as _, Vec2, vec2};
5
6use crate::{
7    TextureAtlas,
8    mutex::{Mutex, RwLock},
9    text::FontTweak,
10};
11
12// ----------------------------------------------------------------------------
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
16pub struct UvRect {
17    /// X/Y offset for nice rendering (unit: points).
18    pub offset: Vec2,
19
20    /// Screen size (in points) of this glyph.
21    /// Note that the height is different from the font height.
22    pub size: Vec2,
23
24    /// Top left corner UV in texture.
25    pub min: [u16; 2],
26
27    /// Bottom right corner (exclusive).
28    pub max: [u16; 2],
29}
30
31impl UvRect {
32    pub fn is_nothing(&self) -> bool {
33        self.min == self.max
34    }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct GlyphInfo {
39    /// Used for pair-kerning.
40    ///
41    /// Doesn't need to be unique.
42    /// Use `ab_glyph::GlyphId(0)` if you just want to have an id, and don't care.
43    pub(crate) id: ab_glyph::GlyphId,
44
45    /// Unit: points.
46    pub advance_width: f32,
47
48    /// Texture coordinates.
49    pub uv_rect: UvRect,
50}
51
52impl Default for GlyphInfo {
53    /// Basically a zero-width space.
54    fn default() -> Self {
55        Self {
56            id: ab_glyph::GlyphId(0),
57            advance_width: 0.0,
58            uv_rect: Default::default(),
59        }
60    }
61}
62
63// ----------------------------------------------------------------------------
64
65/// A specific font with a size.
66/// The interface uses points as the unit for everything.
67pub struct FontImpl {
68    name: String,
69    ab_glyph_font: ab_glyph::FontArc,
70
71    /// Maximum character height
72    scale_in_pixels: u32,
73
74    height_in_points: f32,
75
76    // move each character by this much (hack)
77    y_offset_in_points: f32,
78
79    ascent: f32,
80    pixels_per_point: f32,
81    glyph_info_cache: RwLock<ahash::HashMap<char, GlyphInfo>>, // TODO(emilk): standard Mutex
82    atlas: Arc<Mutex<TextureAtlas>>,
83}
84
85impl FontImpl {
86    pub fn new(
87        atlas: Arc<Mutex<TextureAtlas>>,
88        pixels_per_point: f32,
89        name: String,
90        ab_glyph_font: ab_glyph::FontArc,
91        scale_in_pixels: f32,
92        tweak: FontTweak,
93    ) -> Self {
94        assert!(
95            scale_in_pixels > 0.0,
96            "scale_in_pixels is smaller than 0, got: {scale_in_pixels:?}"
97        );
98        assert!(
99            pixels_per_point > 0.0,
100            "pixels_per_point must be greater than 0, got: {pixels_per_point:?}"
101        );
102
103        use ab_glyph::{Font as _, ScaleFont as _};
104        let scaled = ab_glyph_font.as_scaled(scale_in_pixels);
105        let ascent = (scaled.ascent() / pixels_per_point).round_ui();
106        let descent = (scaled.descent() / pixels_per_point).round_ui();
107        let line_gap = (scaled.line_gap() / pixels_per_point).round_ui();
108
109        // Tweak the scale as the user desired
110        let scale_in_pixels = scale_in_pixels * tweak.scale;
111        let scale_in_points = scale_in_pixels / pixels_per_point;
112
113        let baseline_offset = (scale_in_points * tweak.baseline_offset_factor).round_ui();
114
115        let y_offset_points =
116            ((scale_in_points * tweak.y_offset_factor) + tweak.y_offset).round_ui();
117
118        // Center scaled glyphs properly:
119        let height = ascent + descent;
120        let y_offset_points = y_offset_points - (1.0 - tweak.scale) * 0.5 * height;
121
122        // Round to an even number of physical pixels to get even kerning.
123        // See https://github.com/emilk/egui/issues/382
124        let scale_in_pixels = scale_in_pixels.round() as u32;
125
126        // Round to closest pixel:
127        let y_offset_in_points = (y_offset_points * pixels_per_point).round() / pixels_per_point;
128
129        Self {
130            name,
131            ab_glyph_font,
132            scale_in_pixels,
133            height_in_points: ascent - descent + line_gap,
134            y_offset_in_points,
135            ascent: ascent + baseline_offset,
136            pixels_per_point,
137            glyph_info_cache: Default::default(),
138            atlas,
139        }
140    }
141
142    /// Code points that will always be replaced by the replacement character.
143    ///
144    /// See also [`invisible_char`].
145    fn ignore_character(&self, chr: char) -> bool {
146        use crate::text::FontDefinitions;
147
148        if !FontDefinitions::builtin_font_names().contains(&self.name.as_str()) {
149            return false;
150        }
151
152        matches!(
153            chr,
154            // Strip out a religious symbol with secondary nefarious interpretation:
155            '\u{534d}' | '\u{5350}' |
156
157            // Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
158            '\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
159        )
160    }
161
162    /// An un-ordered iterator over all supported characters.
163    fn characters(&self) -> impl Iterator<Item = char> + '_ {
164        use ab_glyph::Font as _;
165        self.ab_glyph_font
166            .codepoint_ids()
167            .map(|(_, chr)| chr)
168            .filter(|&chr| !self.ignore_character(chr))
169    }
170
171    /// `\n` will result in `None`
172    fn glyph_info(&self, c: char) -> Option<GlyphInfo> {
173        {
174            if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) {
175                return Some(*glyph_info);
176            }
177        }
178
179        if self.ignore_character(c) {
180            return None; // these will result in the replacement character when rendering
181        }
182
183        if c == '\t' {
184            if let Some(space) = self.glyph_info(' ') {
185                let glyph_info = GlyphInfo {
186                    advance_width: crate::text::TAB_SIZE as f32 * space.advance_width,
187                    ..space
188                };
189                self.glyph_info_cache.write().insert(c, glyph_info);
190                return Some(glyph_info);
191            }
192        }
193
194        if c == '\u{2009}' {
195            // Thin space, often used as thousands deliminator: 1 234 567 890
196            // https://www.compart.com/en/unicode/U+2009
197            // https://en.wikipedia.org/wiki/Thin_space
198
199            if let Some(space) = self.glyph_info(' ') {
200                let em = self.height_in_points; // TODO(emilk): is this right?
201                let advance_width = f32::min(em / 6.0, space.advance_width * 0.5);
202                let glyph_info = GlyphInfo {
203                    advance_width,
204                    ..space
205                };
206                self.glyph_info_cache.write().insert(c, glyph_info);
207                return Some(glyph_info);
208            }
209        }
210
211        if invisible_char(c) {
212            let glyph_info = GlyphInfo::default();
213            self.glyph_info_cache.write().insert(c, glyph_info);
214            return Some(glyph_info);
215        }
216
217        // Add new character:
218        use ab_glyph::Font as _;
219        let glyph_id = self.ab_glyph_font.glyph_id(c);
220
221        if glyph_id.0 == 0 {
222            None // unsupported character
223        } else {
224            let glyph_info = self.allocate_glyph(glyph_id);
225            self.glyph_info_cache.write().insert(c, glyph_info);
226            Some(glyph_info)
227        }
228    }
229
230    #[inline]
231    pub fn pair_kerning(
232        &self,
233        last_glyph_id: ab_glyph::GlyphId,
234        glyph_id: ab_glyph::GlyphId,
235    ) -> f32 {
236        use ab_glyph::{Font as _, ScaleFont as _};
237        self.ab_glyph_font
238            .as_scaled(self.scale_in_pixels as f32)
239            .kern(last_glyph_id, glyph_id)
240            / self.pixels_per_point
241    }
242
243    /// Height of one row of text in points.
244    ///
245    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
246    #[inline(always)]
247    pub fn row_height(&self) -> f32 {
248        self.height_in_points
249    }
250
251    #[inline(always)]
252    pub fn pixels_per_point(&self) -> f32 {
253        self.pixels_per_point
254    }
255
256    /// This is the distance from the top to the baseline.
257    ///
258    /// Unit: points.
259    #[inline(always)]
260    pub fn ascent(&self) -> f32 {
261        self.ascent
262    }
263
264    fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo {
265        assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0");
266        use ab_glyph::{Font as _, ScaleFont as _};
267
268        let glyph = glyph_id.with_scale_and_position(
269            self.scale_in_pixels as f32,
270            ab_glyph::Point { x: 0.0, y: 0.0 },
271        );
272
273        let uv_rect = self.ab_glyph_font.outline_glyph(glyph).map(|glyph| {
274            let bb = glyph.px_bounds();
275            let glyph_width = bb.width() as usize;
276            let glyph_height = bb.height() as usize;
277            if glyph_width == 0 || glyph_height == 0 {
278                UvRect::default()
279            } else {
280                let glyph_pos = {
281                    let atlas = &mut self.atlas.lock();
282                    let text_alpha_from_coverage = atlas.text_alpha_from_coverage;
283                    let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
284                    glyph.draw(|x, y, v| {
285                        if 0.0 < v {
286                            let px = glyph_pos.0 + x as usize;
287                            let py = glyph_pos.1 + y as usize;
288                            image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v);
289                        }
290                    });
291                    glyph_pos
292                };
293
294                let offset_in_pixels = vec2(bb.min.x, bb.min.y);
295                let offset =
296                    offset_in_pixels / self.pixels_per_point + self.y_offset_in_points * Vec2::Y;
297                UvRect {
298                    offset,
299                    size: vec2(glyph_width as f32, glyph_height as f32) / self.pixels_per_point,
300                    min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
301                    max: [
302                        (glyph_pos.0 + glyph_width) as u16,
303                        (glyph_pos.1 + glyph_height) as u16,
304                    ],
305                }
306            }
307        });
308        let uv_rect = uv_rect.unwrap_or_default();
309
310        let advance_width_in_points = self
311            .ab_glyph_font
312            .as_scaled(self.scale_in_pixels as f32)
313            .h_advance(glyph_id)
314            / self.pixels_per_point;
315
316        GlyphInfo {
317            id: glyph_id,
318            advance_width: advance_width_in_points,
319            uv_rect,
320        }
321    }
322}
323
324type FontIndex = usize;
325
326// TODO(emilk): rename?
327/// Wrapper over multiple [`FontImpl`] (e.g. a primary + fallbacks for emojis)
328pub struct Font {
329    fonts: Vec<Arc<FontImpl>>,
330
331    /// Lazily calculated.
332    characters: Option<BTreeMap<char, Vec<String>>>,
333
334    replacement_glyph: (FontIndex, GlyphInfo),
335    pixels_per_point: f32,
336    row_height: f32,
337    glyph_info_cache: ahash::HashMap<char, (FontIndex, GlyphInfo)>,
338}
339
340impl Font {
341    pub fn new(fonts: Vec<Arc<FontImpl>>) -> Self {
342        if fonts.is_empty() {
343            return Self {
344                fonts,
345                characters: None,
346                replacement_glyph: Default::default(),
347                pixels_per_point: 1.0,
348                row_height: 0.0,
349                glyph_info_cache: Default::default(),
350            };
351        }
352
353        let pixels_per_point = fonts[0].pixels_per_point();
354        let row_height = fonts[0].row_height();
355
356        let mut slf = Self {
357            fonts,
358            characters: None,
359            replacement_glyph: Default::default(),
360            pixels_per_point,
361            row_height,
362            glyph_info_cache: Default::default(),
363        };
364
365        const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
366        const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
367
368        let replacement_glyph = slf
369            .glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR)
370            .or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR))
371            .unwrap_or_else(|| {
372                #[cfg(feature = "log")]
373                log::warn!(
374                    "Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
375                );
376                (0, GlyphInfo::default())
377            });
378        slf.replacement_glyph = replacement_glyph;
379
380        slf
381    }
382
383    pub fn preload_characters(&mut self, s: &str) {
384        for c in s.chars() {
385            self.glyph_info(c);
386        }
387    }
388
389    pub fn preload_common_characters(&mut self) {
390        // Preload the printable ASCII characters [32, 126] (which excludes control codes):
391        const FIRST_ASCII: usize = 32; // 32 == space
392        const LAST_ASCII: usize = 126;
393        for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) {
394            self.glyph_info(c);
395        }
396        self.glyph_info('°');
397        self.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR);
398    }
399
400    /// All supported characters, and in which font they are available in.
401    pub fn characters(&mut self) -> &BTreeMap<char, Vec<String>> {
402        self.characters.get_or_insert_with(|| {
403            let mut characters: BTreeMap<char, Vec<String>> = Default::default();
404            for font in &self.fonts {
405                for chr in font.characters() {
406                    characters.entry(chr).or_default().push(font.name.clone());
407                }
408            }
409            characters
410        })
411    }
412
413    #[inline(always)]
414    pub fn round_to_pixel(&self, point: f32) -> f32 {
415        (point * self.pixels_per_point).round() / self.pixels_per_point
416    }
417
418    /// Height of one row of text. In points.
419    ///
420    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
421    #[inline(always)]
422    pub fn row_height(&self) -> f32 {
423        self.row_height
424    }
425
426    pub fn uv_rect(&self, c: char) -> UvRect {
427        self.glyph_info_cache
428            .get(&c)
429            .map(|gi| gi.1.uv_rect)
430            .unwrap_or_default()
431    }
432
433    /// Width of this character in points.
434    pub fn glyph_width(&mut self, c: char) -> f32 {
435        self.glyph_info(c).1.advance_width
436    }
437
438    /// Can we display this glyph?
439    pub fn has_glyph(&mut self, c: char) -> bool {
440        self.glyph_info(c) != self.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦‍♂️
441    }
442
443    /// Can we display all the glyphs in this text?
444    pub fn has_glyphs(&mut self, s: &str) -> bool {
445        s.chars().all(|c| self.has_glyph(c))
446    }
447
448    /// `\n` will (intentionally) show up as the replacement character.
449    fn glyph_info(&mut self, c: char) -> (FontIndex, GlyphInfo) {
450        if let Some(font_index_glyph_info) = self.glyph_info_cache.get(&c) {
451            return *font_index_glyph_info;
452        }
453
454        let font_index_glyph_info = self.glyph_info_no_cache_or_fallback(c);
455        let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph);
456        self.glyph_info_cache.insert(c, font_index_glyph_info);
457        font_index_glyph_info
458    }
459
460    #[inline]
461    pub(crate) fn font_impl_and_glyph_info(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) {
462        if self.fonts.is_empty() {
463            return (None, self.replacement_glyph.1);
464        }
465        let (font_index, glyph_info) = self.glyph_info(c);
466        let font_impl = &self.fonts[font_index];
467        (Some(font_impl), glyph_info)
468    }
469
470    pub(crate) fn ascent(&self) -> f32 {
471        if let Some(first) = self.fonts.first() {
472            first.ascent()
473        } else {
474            self.row_height
475        }
476    }
477
478    fn glyph_info_no_cache_or_fallback(&mut self, c: char) -> Option<(FontIndex, GlyphInfo)> {
479        for (font_index, font_impl) in self.fonts.iter().enumerate() {
480            if let Some(glyph_info) = font_impl.glyph_info(c) {
481                self.glyph_info_cache.insert(c, (font_index, glyph_info));
482                return Some((font_index, glyph_info));
483            }
484        }
485        None
486    }
487}
488
489/// Code points that will always be invisible (zero width).
490///
491/// See also [`FontImpl::ignore_character`].
492#[inline]
493fn invisible_char(c: char) -> bool {
494    if c == '\r' {
495        // A character most vile and pernicious. Don't display it.
496        return true;
497    }
498
499    // See https://github.com/emilk/egui/issues/336
500
501    // From https://www.fileformat.info/info/unicode/category/Cf/list.htm
502
503    // TODO(emilk): heed bidi characters
504
505    matches!(
506        c,
507        '\u{200B}' // ZERO WIDTH SPACE
508            | '\u{200C}' // ZERO WIDTH NON-JOINER
509            | '\u{200D}' // ZERO WIDTH JOINER
510            | '\u{200E}' // LEFT-TO-RIGHT MARK
511            | '\u{200F}' // RIGHT-TO-LEFT MARK
512            | '\u{202A}' // LEFT-TO-RIGHT EMBEDDING
513            | '\u{202B}' // RIGHT-TO-LEFT EMBEDDING
514            | '\u{202C}' // POP DIRECTIONAL FORMATTING
515            | '\u{202D}' // LEFT-TO-RIGHT OVERRIDE
516            | '\u{202E}' // RIGHT-TO-LEFT OVERRIDE
517            | '\u{2060}' // WORD JOINER
518            | '\u{2061}' // FUNCTION APPLICATION
519            | '\u{2062}' // INVISIBLE TIMES
520            | '\u{2063}' // INVISIBLE SEPARATOR
521            | '\u{2064}' // INVISIBLE PLUS
522            | '\u{2066}' // LEFT-TO-RIGHT ISOLATE
523            | '\u{2067}' // RIGHT-TO-LEFT ISOLATE
524            | '\u{2068}' // FIRST STRONG ISOLATE
525            | '\u{2069}' // POP DIRECTIONAL ISOLATE
526            | '\u{206A}' // INHIBIT SYMMETRIC SWAPPING
527            | '\u{206B}' // ACTIVATE SYMMETRIC SWAPPING
528            | '\u{206C}' // INHIBIT ARABIC FORM SHAPING
529            | '\u{206D}' // ACTIVATE ARABIC FORM SHAPING
530            | '\u{206E}' // NATIONAL DIGIT SHAPES
531            | '\u{206F}' // NOMINAL DIGIT SHAPES
532            | '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
533    )
534}