Skip to main content

epaint/text/
fonts.rs

1use core::sync::atomic::{AtomicUsize, Ordering};
2use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
3
4use crate::{
5    TextureAtlas,
6    text::{
7        ByteIndex, Galley, LayoutJob, LayoutSection, Tag, TextOptions, VariationCoords,
8        font::{Font, FontFace},
9    },
10};
11use emath::{NumExt as _, OrderedFloat, Rangef};
12
13#[cfg(feature = "default_fonts")]
14use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT};
15
16// ----------------------------------------------------------------------------
17
18/// How to select a sized font.
19#[derive(Clone, Debug, PartialEq)]
20#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
21pub struct FontId {
22    /// Height in points.
23    pub size: f32,
24
25    /// What font family to use.
26    pub family: FontFamily,
27    // TODO(emilk): weight (bold), italics, …
28}
29
30impl Default for FontId {
31    #[inline]
32    fn default() -> Self {
33        Self {
34            size: 14.0,
35            family: FontFamily::Proportional,
36        }
37    }
38}
39
40impl FontId {
41    #[inline]
42    pub const fn new(size: f32, family: FontFamily) -> Self {
43        Self { size, family }
44    }
45
46    #[inline]
47    pub const fn proportional(size: f32) -> Self {
48        Self::new(size, FontFamily::Proportional)
49    }
50
51    #[inline]
52    pub const fn monospace(size: f32) -> Self {
53        Self::new(size, FontFamily::Monospace)
54    }
55}
56
57impl core::hash::Hash for FontId {
58    #[inline(always)]
59    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
60        let Self { size, family } = self;
61        emath::OrderedFloat(*size).hash(state);
62        family.hash(state);
63    }
64}
65
66// ----------------------------------------------------------------------------
67
68/// Font of unknown size.
69///
70/// Which style of font: [`Monospace`][`FontFamily::Monospace`], [`Proportional`][`FontFamily::Proportional`],
71/// or by user-chosen name.
72#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
73#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
74pub enum FontFamily {
75    /// A font where some characters are wider than other (e.g. 'w' is wider than 'i').
76    ///
77    /// Proportional fonts are easier to read and should be the preferred choice in most situations.
78    #[default]
79    Proportional,
80
81    /// A font where each character is the same width (`w` is the same width as `i`).
82    ///
83    /// Useful for code snippets, or when you need to align numbers or text.
84    Monospace,
85
86    /// One of the names in [`FontDefinitions::families`].
87    ///
88    /// ```
89    /// # use epaint::FontFamily;
90    /// // User-chosen names:
91    /// FontFamily::Name("arial".into());
92    /// FontFamily::Name("serif".into());
93    /// ```
94    Name(Arc<str>),
95}
96
97impl core::fmt::Display for FontFamily {
98    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        match self {
100            Self::Monospace => "Monospace".fmt(f),
101            Self::Proportional => "Proportional".fmt(f),
102            Self::Name(name) => (*name).fmt(f),
103        }
104    }
105}
106
107// ----------------------------------------------------------------------------
108
109/// A `.ttf` or `.otf` file and a font face index.
110#[derive(Clone, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
112pub struct FontData {
113    /// The content of a `.ttf` or `.otf` file.
114    pub font: Cow<'static, [u8]>,
115
116    /// Which font face in the file to use.
117    /// When in doubt, use `0`.
118    pub index: u32,
119
120    /// Extra scale and vertical tweak to apply to all text of this font.
121    pub tweak: FontTweak,
122}
123
124impl FontData {
125    pub fn from_static(font: &'static [u8]) -> Self {
126        Self {
127            font: Cow::Borrowed(font),
128            index: 0,
129            tweak: Default::default(),
130        }
131    }
132
133    pub fn from_owned(font: Vec<u8>) -> Self {
134        Self {
135            font: Cow::Owned(font),
136            index: 0,
137            tweak: Default::default(),
138        }
139    }
140
141    pub fn tweak(self, tweak: FontTweak) -> Self {
142        Self { tweak, ..self }
143    }
144
145    /// The variation axes of this font, e.g. `wght` (weight) and `wdth` (width).
146    ///
147    /// Use this to discover which axes a variable font supports, and their valid
148    /// ranges, so a UI can offer the right knobs instead of making the user guess
149    /// tags and values for [`FontTweak::coords`].
150    ///
151    /// Returns an empty list for non-variable (static) fonts, or if the font data
152    /// fails to parse.
153    pub fn variation_axes(&self) -> Vec<FontVariationAxis> {
154        use skrifa::MetadataProvider as _;
155
156        let Ok(font) = skrifa::FontRef::from_index(self.font.as_ref(), self.index) else {
157            return Vec::new();
158        };
159
160        font.axes()
161            .iter()
162            .map(|axis| FontVariationAxis {
163                tag: axis.tag(),
164                name: font
165                    .localized_strings(axis.name_id())
166                    .english_or_first()
167                    .map(|name| name.chars().collect()),
168                range: Rangef::new(axis.min_value(), axis.max_value()),
169                default: axis.default_value(),
170                hidden: axis.is_hidden(),
171            })
172            .collect()
173    }
174}
175
176/// A single variation axis of a variable font, e.g. weight (`wght`) or width (`wdth`).
177///
178/// Obtained via [`FontData::variation_axes`].
179#[derive(Clone, Debug, PartialEq)]
180pub struct FontVariationAxis {
181    /// The axis tag, e.g. `wght` or `wdth`.
182    pub tag: Tag,
183
184    /// Human-readable axis name, if the font provides one (e.g. "Weight").
185    pub name: Option<String>,
186
187    /// Valid range of values for this axis, `min..=max`.
188    pub range: Rangef,
189
190    /// The value used when the axis is not overridden.
191    pub default: f32,
192
193    /// Whether the font recommends hiding this axis from user interfaces.
194    pub hidden: bool,
195}
196
197impl AsRef<[u8]> for FontData {
198    fn as_ref(&self) -> &[u8] {
199        self.font.as_ref()
200    }
201}
202
203// ----------------------------------------------------------------------------
204
205/// Extra scale and vertical tweak to apply to all text of a certain font.
206#[derive(Clone, Debug, PartialEq)]
207#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
208pub struct FontTweak {
209    /// Scale the font's glyphs by this much.
210    /// this is only a visual effect and does not affect the text layout.
211    ///
212    /// Default: `1.0` (no scaling).
213    pub scale: f32,
214
215    /// Shift font's glyphs downwards by this fraction of the font size (in points).
216    /// this is only a visual effect and does not affect the text layout.
217    ///
218    /// Affects larger font sizes more.
219    ///
220    /// A positive value shifts the text downwards.
221    /// A negative value shifts it upwards.
222    ///
223    /// Example value: `-0.2`.
224    pub y_offset_factor: f32,
225
226    /// Shift font's glyphs downwards by this amount of logical points.
227    /// this is only a visual effect and does not affect the text layout.
228    ///
229    /// Affects all font sizes equally.
230    ///
231    /// Example value: `2.0`.
232    pub y_offset: f32,
233
234    /// Override the global font hinting setting for this specific font.
235    ///
236    /// `None` means use the global setting in [`TextOptions::font_hinting`].
237    pub hinting: Option<bool>,
238
239    /// How to grid-fit the glyph outlines when hinting is enabled.
240    ///
241    /// Has no effect when hinting is disabled (see [`Self::hinting`]).
242    pub hinting_target: HintingTarget,
243
244    /// Override the global sub-pixel binning setting for this specific font.
245    ///
246    /// `None` means use the global setting in [`TextOptions::subpixel_binning`].
247    pub subpixel_binning: Option<bool>,
248
249    /// Override the font's default variation coordinates for its axes ("wght", etc.).
250    pub coords: VariationCoords,
251
252    /// Width of a thin space (`\u{2009}`) and narrow no-break space (`\u{202F}`),
253    /// as a fraction of the normal space width.
254    ///
255    /// Thin space is often used as a thousands separator: `1 234 567`.
256    ///
257    /// Default: `0.5` (half a normal space).
258    pub thin_space_width: f32,
259
260    /// Width of a tab character (`\t`), measured in number of space widths.
261    ///
262    /// Default: `4.0`.
263    pub tab_size: f32,
264}
265
266impl Default for FontTweak {
267    fn default() -> Self {
268        Self {
269            scale: 1.0,
270            y_offset_factor: 0.0,
271            y_offset: 0.0,
272            hinting: None,
273            hinting_target: HintingTarget::default(),
274            subpixel_binning: None,
275            coords: VariationCoords::default(),
276            thin_space_width: 0.5,
277            tab_size: 4.0,
278        }
279    }
280}
281
282// ----------------------------------------------------------------------------
283
284/// How to *hint* glyph outlines, i.e. how aggressively to nudge them onto the
285/// pixel grid before rasterizing. Mirrors [`skrifa::outline::Target`].
286///
287/// Hinting trades shape fidelity for sharpness: snapping stems to whole pixels
288/// makes text crisp at small sizes / low dpi, at the cost of slightly distorting
289/// the designer's outlines. At high dpi it matters little.
290///
291/// This only has an effect if the font is actually hinted — either it ships
292/// TrueType instructions, or it was auto-hinted (see [`FontTweak::hinting`]).
293///
294/// Used by [`FontTweak::hinting_target`].
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
297pub enum HintingTarget {
298    /// Strongest hinting, designed for aliased 1-bit (black & white) rendering.
299    ///
300    /// Snaps stems hard to the pixel grid for maximum sharpness. egui always
301    /// renders anti-aliased, so in practice this looks much like [`Self::Smooth`]
302    /// here; it mostly exists for completeness. Maps to `skrifa`'s `Target::Mono`.
303    Mono,
304
305    /// Hinting tuned for anti-aliased rendering. This is what you normally want,
306    /// and what egui uses by default. Maps to `skrifa`'s `Target::Smooth`.
307    Smooth(SmoothHinting),
308}
309
310impl Default for HintingTarget {
311    fn default() -> Self {
312        Self::Smooth(SmoothHinting::default())
313    }
314}
315
316impl From<HintingTarget> for skrifa::outline::Target {
317    fn from(hinting_target: HintingTarget) -> Self {
318        use skrifa::outline::SmoothMode;
319        match hinting_target {
320            HintingTarget::Mono => Self::Mono,
321            HintingTarget::Smooth(SmoothHinting {
322                light,
323                symmetric_rendering,
324                preserve_linear_metrics,
325            }) => Self::Smooth {
326                mode: if light {
327                    SmoothMode::Light
328                } else {
329                    SmoothMode::Normal
330                },
331                symmetric_rendering,
332                preserve_linear_metrics,
333            },
334        }
335    }
336}
337
338/// Tuning for [`HintingTarget::Smooth`], mirroring `skrifa`'s `Target::Smooth`.
339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
340#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
341pub struct SmoothHinting {
342    /// Hint only lightly: snap stems vertically but leave horizontal shapes
343    /// alone (`FreeType`'s "light" mode). Preserves the font's proportions
344    /// better, at the cost of a little horizontal sharpness.
345    ///
346    /// `false` uses the "normal" mode, which also fits horizontally.
347    /// Maps to `SmoothMode::Light` (`true`) vs `SmoothMode::Normal` (`false`).
348    pub light: bool,
349
350    /// Render a glyph the same way regardless of its sub-pixel position.
351    ///
352    /// `true` makes glyphs position-independent (better for caching and
353    /// animation), but a font's instructions may then widen stems and look
354    /// slightly blurrier under an analytic rasterizer like egui's.
355    ///
356    /// **Only affects fonts hinted via the TrueType interpreter** (i.e. fonts
357    /// that ship their own instructions). It has no effect on the auto-hinter.
358    /// Mirrors `Target::Smooth { symmetric_rendering }`.
359    pub symmetric_rendering: bool,
360
361    /// Keep advance widths independent of hinting (don't grid-fit horizontally
362    /// in a way that changes spacing).
363    ///
364    /// `true` keeps inter-glyph spacing identical to the unhinted font, so
365    /// layout never depends on hinting — but it also prevents horizontal
366    /// grid-fitting, leaving vertical stems softer on low-dpi screens.
367    ///
368    /// `false` lets the (auto)hinter snap horizontally for crisper stems.
369    /// egui positions glyphs from the shaper's advances, not the hinted
370    /// outline, so this mainly affects sharpness here, not layout.
371    /// Mirrors `Target::Smooth { preserve_linear_metrics }`.
372    pub preserve_linear_metrics: bool,
373}
374
375impl Default for SmoothHinting {
376    fn default() -> Self {
377        // Matches the behavior egui had before the hinting target was configurable.
378        // Note this means horizontal grid-fitting is opt-in (see `preserve_linear_metrics`).
379        Self {
380            light: false,
381            symmetric_rendering: true,
382            preserve_linear_metrics: true,
383        }
384    }
385}
386
387// ----------------------------------------------------------------------------
388
389pub type Blob = Arc<dyn AsRef<[u8]> + Send + Sync>;
390
391fn blob_from_font_data(data: &FontData) -> Blob {
392    match data.clone().font {
393        Cow::Borrowed(bytes) => Arc::new(bytes) as Blob,
394        Cow::Owned(bytes) => Arc::new(bytes) as Blob,
395    }
396}
397
398/// Describes the font data and the sizes to use.
399///
400/// Often you would start with [`FontDefinitions::default()`] and then add/change the contents.
401///
402/// This is how you install your own custom fonts:
403/// ```
404/// # use {epaint::text::{FontDefinitions, FontFamily, FontData}};
405/// # struct FakeEguiCtx {};
406/// # impl FakeEguiCtx { fn set_fonts(&self, _: FontDefinitions) {} }
407/// # let egui_ctx = FakeEguiCtx {};
408/// let mut fonts = FontDefinitions::default();
409///
410/// // Install my own font (maybe supporting non-latin characters):
411/// fonts.font_data.insert("my_font".to_owned(),
412///    std::sync::Arc::new(
413///        // .ttf and .otf supported
414///        FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf"))
415///    )
416/// );
417///
418/// // Put my font first (highest priority):
419/// fonts.families.get_mut(&FontFamily::Proportional).unwrap()
420///     .insert(0, "my_font".to_owned());
421///
422/// // Put my font as last fallback for monospace:
423/// fonts.families.get_mut(&FontFamily::Monospace).unwrap()
424///     .push("my_font".to_owned());
425///
426/// egui_ctx.set_fonts(fonts);
427/// ```
428#[derive(Clone, Debug, PartialEq)]
429#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
430#[cfg_attr(feature = "serde", serde(default))]
431pub struct FontDefinitions {
432    /// List of font names and their definitions.
433    ///
434    /// `epaint` has built-in-default for these, but you can override them if you like.
435    pub font_data: BTreeMap<String, Arc<FontData>>,
436
437    /// Which fonts (names) to use for each [`FontFamily`].
438    ///
439    /// The list should be a list of keys into [`Self::font_data`].
440    /// When looking for a character glyph `epaint` will start with
441    /// the first font and then move to the second, and so on.
442    /// So the first font is the primary, and then comes a list of fallbacks in order of priority.
443    pub families: BTreeMap<FontFamily, Vec<String>>,
444}
445
446#[derive(Debug, Clone)]
447pub struct FontInsert {
448    /// Font name
449    pub name: String,
450
451    /// A `.ttf` or `.otf` file and a font face index.
452    pub data: FontData,
453
454    /// Sets the font family and priority
455    pub families: Vec<InsertFontFamily>,
456}
457
458#[derive(Debug, Clone)]
459pub struct InsertFontFamily {
460    /// Font family
461    pub family: FontFamily,
462
463    /// Fallback or Primary font
464    pub priority: FontPriority,
465}
466
467#[derive(Debug, Clone)]
468pub enum FontPriority {
469    /// Prefer this font before all existing ones.
470    ///
471    /// If a desired glyph exists in this font, it will be used.
472    Highest,
473
474    /// Use this font as a fallback, after all existing ones.
475    ///
476    /// This font will only be used if the glyph is not found in any of the previously installed fonts.
477    Lowest,
478}
479
480impl FontInsert {
481    pub fn new(name: &str, data: FontData, families: Vec<InsertFontFamily>) -> Self {
482        Self {
483            name: name.to_owned(),
484            data,
485            families,
486        }
487    }
488}
489
490impl Default for FontDefinitions {
491    /// Specifies the default fonts if the feature `default_fonts` is enabled,
492    /// otherwise this is the same as [`Self::empty`].
493    #[cfg(not(feature = "default_fonts"))]
494    fn default() -> Self {
495        Self::empty()
496    }
497
498    /// Specifies the default fonts if the feature `default_fonts` is enabled,
499    /// otherwise this is the same as [`Self::empty`].
500    #[cfg(feature = "default_fonts")]
501    fn default() -> Self {
502        let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
503
504        let mut families = BTreeMap::new();
505
506        font_data.insert(
507            "Hack".to_owned(),
508            Arc::new(FontData::from_static(HACK_REGULAR)),
509        );
510
511        // Some good looking emojis. Use as first priority:
512        font_data.insert(
513            "NotoEmoji-Regular".to_owned(),
514            Arc::new(FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak {
515                scale: 0.81, // Make smaller
516                ..Default::default()
517            })),
518        );
519
520        font_data.insert(
521            "Ubuntu-Light".to_owned(),
522            Arc::new(FontData::from_static(UBUNTU_LIGHT)),
523        );
524
525        // Bigger emojis, and more. <http://jslegers.github.io/emoji-icon-font/>:
526        font_data.insert(
527            "emoji-icon-font".to_owned(),
528            Arc::new(FontData::from_static(EMOJI_ICON).tweak(FontTweak {
529                scale: 0.90, // Make smaller
530                ..Default::default()
531            })),
532        );
533
534        families.insert(
535            FontFamily::Monospace,
536            vec![
537                "Hack".to_owned(),
538                "Ubuntu-Light".to_owned(), // fallback for √ etc
539                "NotoEmoji-Regular".to_owned(),
540                "emoji-icon-font".to_owned(),
541            ],
542        );
543        families.insert(
544            FontFamily::Proportional,
545            vec![
546                "Ubuntu-Light".to_owned(),
547                "NotoEmoji-Regular".to_owned(),
548                "emoji-icon-font".to_owned(),
549            ],
550        );
551
552        Self {
553            font_data,
554            families,
555        }
556    }
557}
558
559impl FontDefinitions {
560    /// No fonts.
561    pub fn empty() -> Self {
562        let mut families = BTreeMap::new();
563        families.insert(FontFamily::Monospace, vec![]);
564        families.insert(FontFamily::Proportional, vec![]);
565
566        Self {
567            font_data: Default::default(),
568            families,
569        }
570    }
571
572    /// List of all the builtin font names used by `epaint`.
573    #[cfg(feature = "default_fonts")]
574    pub fn builtin_font_names() -> &'static [&'static str] {
575        &[
576            "Ubuntu-Light",
577            "NotoEmoji-Regular",
578            "emoji-icon-font",
579            "Hack",
580        ]
581    }
582
583    /// List of all the builtin font names used by `epaint`.
584    #[cfg(not(feature = "default_fonts"))]
585    pub fn builtin_font_names() -> &'static [&'static str] {
586        &[]
587    }
588}
589
590/// Unique ID for looking up a single font face/file.
591#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
592pub(crate) struct FontFaceKey(u64);
593
594impl FontFaceKey {
595    pub const INVALID: Self = Self(0);
596
597    fn new() -> Self {
598        static KEY_COUNTER: AtomicUsize = AtomicUsize::new(1);
599        Self(crate::util::hash(
600            KEY_COUNTER.fetch_add(1, Ordering::Relaxed),
601        ))
602    }
603}
604
605// Safe, because we hash the value in the constructor.
606impl nohash_hasher::IsEnabled for FontFaceKey {}
607
608/// Cached data for working with a font family (e.g. doing character lookups).
609#[derive(Debug)]
610pub(super) struct CachedFamily {
611    pub fonts: Vec<FontFaceKey>,
612
613    /// Lazily calculated.
614    pub characters: Option<BTreeMap<char, Vec<String>>>,
615
616    /// The face used when no face in [`Self::fonts`] supports a char.
617    pub replacement_face_key: FontFaceKey,
618
619    /// The char that [`Self::replacement_face_key`] actually contains.
620    ///
621    /// When the user asks about a char that no fallback face supports we
622    /// render this char in its place.
623    pub replacement_char: char,
624
625    /// Cache: `char → which face in the fallback chain owns this char`.
626    ///
627    /// Location-independent (fallback choice depends only on charmap support,
628    /// not on variation coordinates).
629    pub face_cache: ahash::HashMap<char, FontFaceKey>,
630}
631
632impl CachedFamily {
633    fn new(
634        fonts: Vec<FontFaceKey>,
635        fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
636    ) -> Self {
637        const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
638        const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
639
640        if fonts.is_empty() {
641            return Self {
642                fonts,
643                characters: None,
644                replacement_face_key: FontFaceKey::INVALID,
645                replacement_char: PRIMARY_REPLACEMENT_CHAR,
646                face_cache: Default::default(),
647            };
648        }
649
650        let mut slf = Self {
651            fonts,
652            characters: None,
653            replacement_face_key: FontFaceKey::INVALID,
654            replacement_char: PRIMARY_REPLACEMENT_CHAR,
655            face_cache: Default::default(),
656        };
657
658        let (replacement_face_key, replacement_char) = slf
659            .find_face_for_char(PRIMARY_REPLACEMENT_CHAR, fonts_by_id)
660            .map(|key| (key, PRIMARY_REPLACEMENT_CHAR))
661            .or_else(|| {
662                slf.find_face_for_char(FALLBACK_REPLACEMENT_CHAR, fonts_by_id)
663                    .map(|key| (key, FALLBACK_REPLACEMENT_CHAR))
664            })
665            .unwrap_or_else(|| {
666                log::warn!(
667                    "Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
668                );
669                (FontFaceKey::INVALID, PRIMARY_REPLACEMENT_CHAR)
670            });
671        slf.replacement_face_key = replacement_face_key;
672        slf.replacement_char = replacement_char;
673
674        slf
675    }
676
677    /// Walk the fallback chain and return the first face whose charmap supports `c`.
678    ///
679    /// Pure — does not touch any cache. Callers that want memoisation should
680    /// insert into [`Self::face_cache`] themselves.
681    pub(crate) fn find_face_for_char(
682        &self,
683        c: char,
684        fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
685    ) -> Option<FontFaceKey> {
686        for font_key in &self.fonts {
687            let font_face = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID");
688            if font_face.glyph_id_resolution(c).is_some() {
689                return Some(*font_key);
690            }
691        }
692        None
693    }
694}
695
696// ----------------------------------------------------------------------------
697
698/// The collection of fonts used by `epaint`.
699///
700/// Required in order to paint text. Create one and reuse. Cheap to clone.
701///
702/// Each [`Fonts`] comes with a font atlas textures that needs to be used when painting.
703///
704/// If you are using `egui`, use `egui::Context::set_fonts` and `egui::Context::fonts`.
705///
706/// You need to call [`Self::begin_pass`] and [`Self::font_image_delta`] once every frame.
707pub struct Fonts {
708    pub fonts: FontsImpl,
709    galley_cache: GalleyCache,
710}
711
712impl Fonts {
713    /// Create a new [`Fonts`] for text layout.
714    /// This call is expensive, so only create one [`Fonts`] and then reuse it.
715    pub fn new(options: TextOptions, definitions: FontDefinitions) -> Self {
716        Self {
717            fonts: FontsImpl::new(options, definitions),
718            galley_cache: Default::default(),
719        }
720    }
721
722    /// Call at the start of each frame with the latest known [`TextOptions`].
723    ///
724    /// Call after painting the previous frame, but before using [`Fonts`] for the new frame.
725    ///
726    /// This function will react to changes in [`TextOptions`],
727    /// as well as notice when the font atlas is getting full, and handle that.
728    pub fn begin_pass(&mut self, options: TextOptions) {
729        let text_options_changed = self.fonts.options() != &options;
730        let font_atlas_almost_full = self.fonts.atlas.fill_ratio() > 0.8;
731        let needs_recreate = text_options_changed || font_atlas_almost_full;
732
733        if needs_recreate {
734            let definitions = self.fonts.definitions.clone();
735
736            *self = Self {
737                fonts: FontsImpl::new(options, definitions),
738                galley_cache: Default::default(),
739            };
740        }
741
742        self.galley_cache.flush_cache();
743    }
744
745    /// Call at the end of each frame (before painting) to get the change to the font texture since last call.
746    pub fn font_image_delta(&mut self) -> Option<crate::ImageDelta> {
747        self.fonts.atlas.take_delta()
748    }
749
750    #[inline]
751    pub fn options(&self) -> &TextOptions {
752        self.texture_atlas().options()
753    }
754
755    #[inline]
756    pub fn definitions(&self) -> &FontDefinitions {
757        &self.fonts.definitions
758    }
759
760    /// The font atlas.
761    /// Pass this to [`crate::Tessellator`].
762    pub fn texture_atlas(&self) -> &TextureAtlas {
763        &self.fonts.atlas
764    }
765
766    /// The full font atlas image.
767    #[inline]
768    pub fn image(&self) -> crate::ColorImage {
769        self.fonts.atlas.image().clone()
770    }
771
772    /// Current size of the font image.
773    /// Pass this to [`crate::Tessellator`].
774    pub fn font_image_size(&self) -> [usize; 2] {
775        self.fonts.atlas.size()
776    }
777
778    /// Can we display this glyph?
779    pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
780        self.fonts.font(&font_id.family).has_glyph(c)
781    }
782
783    /// Can we display all the glyphs in this text?
784    pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
785        self.fonts.font(&font_id.family).has_glyphs(s)
786    }
787
788    pub fn num_galleys_in_cache(&self) -> usize {
789        self.galley_cache.num_galleys_in_cache()
790    }
791
792    /// How full is the font atlas?
793    ///
794    /// This increases as new fonts and/or glyphs are used,
795    /// but can also decrease in a call to [`Self::begin_pass`].
796    pub fn font_atlas_fill_ratio(&self) -> f32 {
797        self.fonts.atlas.fill_ratio()
798    }
799
800    /// Returns a [`FontsView`] with the given `pixels_per_point` that can be used to do text layout.
801    pub fn with_pixels_per_point(&mut self, pixels_per_point: f32) -> FontsView<'_> {
802        FontsView {
803            fonts: &mut self.fonts,
804            galley_cache: &mut self.galley_cache,
805            pixels_per_point,
806        }
807    }
808}
809
810// ----------------------------------------------------------------------------
811
812/// The context's collection of fonts, with this context's `pixels_per_point`. This is what you use to do text layout.
813pub struct FontsView<'a> {
814    pub fonts: &'a mut FontsImpl,
815    galley_cache: &'a mut GalleyCache,
816    pixels_per_point: f32,
817}
818
819impl FontsView<'_> {
820    #[inline]
821    pub fn options(&self) -> &TextOptions {
822        self.fonts.options()
823    }
824
825    #[inline]
826    pub fn definitions(&self) -> &FontDefinitions {
827        &self.fonts.definitions
828    }
829
830    /// The full font atlas image.
831    #[inline]
832    pub fn image(&self) -> crate::ColorImage {
833        self.fonts.atlas.image().clone()
834    }
835
836    /// Current size of the font image.
837    /// Pass this to [`crate::Tessellator`].
838    pub fn font_image_size(&self) -> [usize; 2] {
839        self.fonts.atlas.size()
840    }
841
842    /// Width of this character in points.
843    ///
844    /// If the font doesn't exist, this will return `0.0`.
845    pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
846        self.fonts
847            .font(&font_id.family)
848            .glyph_width(c, font_id.size)
849    }
850
851    /// Can we display this glyph?
852    pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
853        self.fonts.font(&font_id.family).has_glyph(c)
854    }
855
856    /// Can we display all the glyphs in this text?
857    pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
858        self.fonts.font(&font_id.family).has_glyphs(s)
859    }
860
861    /// Height of one row of text in points.
862    ///
863    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
864    #[inline]
865    pub fn row_height(&mut self, font_id: &FontId) -> f32 {
866        self.fonts
867            .font(&font_id.family)
868            .styled_metrics(
869                self.pixels_per_point,
870                font_id.size,
871                // TODO(valadaptive): use font variation coords when calculating row height
872                &VariationCoords::default(),
873            )
874            .row_height
875    }
876
877    /// List of all known font families.
878    pub fn families(&self) -> Vec<FontFamily> {
879        self.fonts.definitions.families.keys().cloned().collect()
880    }
881
882    /// Layout some text.
883    ///
884    /// This is the most advanced layout function.
885    /// See also [`Self::layout`], [`Self::layout_no_wrap`] and
886    /// [`Self::layout_delayed_color`].
887    ///
888    /// The implementation uses memoization so repeated calls are cheap.
889    #[inline]
890    pub fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
891        let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs.
892        self.galley_cache.layout(
893            self.fonts,
894            self.pixels_per_point,
895            job,
896            allow_split_paragraphs,
897        )
898    }
899
900    pub fn num_galleys_in_cache(&self) -> usize {
901        self.galley_cache.num_galleys_in_cache()
902    }
903
904    /// How full is the font atlas?
905    ///
906    /// This increases as new fonts and/or glyphs are used,
907    /// but can also decrease in a call to [`Fonts::begin_pass`].
908    pub fn font_atlas_fill_ratio(&self) -> f32 {
909        self.fonts.atlas.fill_ratio()
910    }
911
912    /// Will wrap text at the given width and line break at `\n`.
913    ///
914    /// The implementation uses memoization so repeated calls are cheap.
915    #[inline]
916    pub fn layout(
917        &mut self,
918        text: String,
919        font_id: FontId,
920        color: crate::Color32,
921        wrap_width: f32,
922    ) -> Arc<Galley> {
923        let job = LayoutJob::simple(text, font_id, color, wrap_width);
924        self.layout_job(job)
925    }
926
927    /// Will line break at `\n`.
928    ///
929    /// The implementation uses memoization so repeated calls are cheap.
930    #[inline]
931    pub fn layout_no_wrap(
932        &mut self,
933        text: String,
934        font_id: FontId,
935        color: crate::Color32,
936    ) -> Arc<Galley> {
937        let job = LayoutJob::simple(text, font_id, color, f32::INFINITY);
938        self.layout_job(job)
939    }
940
941    /// Like [`Self::layout`], made for when you want to pick a color for the text later.
942    ///
943    /// The implementation uses memoization so repeated calls are cheap.
944    #[inline]
945    pub fn layout_delayed_color(
946        &mut self,
947        text: String,
948        font_id: FontId,
949        wrap_width: f32,
950    ) -> Arc<Galley> {
951        self.layout(text, font_id, crate::Color32::PLACEHOLDER, wrap_width)
952    }
953}
954
955// ----------------------------------------------------------------------------
956
957/// The collection of fonts used by `epaint`.
958///
959/// Required in order to paint text.
960pub struct FontsImpl {
961    definitions: FontDefinitions,
962    atlas: TextureAtlas,
963    fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace>,
964    fonts_by_name: ahash::HashMap<String, FontFaceKey>,
965    family_cache: ahash::HashMap<FontFamily, CachedFamily>,
966
967    /// Recycled `harfrust` shaping buffer to avoid per-layout allocations.
968    shape_buffer: Option<harfrust::UnicodeBuffer>,
969}
970
971impl FontsImpl {
972    /// Create a new [`FontsImpl`] for text layout.
973    /// This call is expensive, so only create one [`FontsImpl`] and then reuse it.
974    pub fn new(options: TextOptions, definitions: FontDefinitions) -> Self {
975        let texture_width = options.max_texture_side.at_most(16 * 1024);
976        let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways.
977        let atlas = TextureAtlas::new([texture_width, initial_height], options);
978
979        let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace> = Default::default();
980        let mut fonts_by_name: ahash::HashMap<String, FontFaceKey> = Default::default();
981        for (name, font_data) in &definitions.font_data {
982            let blob = blob_from_font_data(font_data);
983            let font_face = FontFace::new(
984                options,
985                name.clone(),
986                blob,
987                font_data.index,
988                font_data.tweak.clone(),
989            )
990            .unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"));
991            let key = FontFaceKey::new();
992            fonts_by_id.insert(key, font_face);
993            fonts_by_name.insert(name.clone(), key);
994        }
995
996        Self {
997            definitions,
998            atlas,
999            fonts_by_id,
1000            fonts_by_name,
1001            family_cache: Default::default(),
1002            shape_buffer: Some(harfrust::UnicodeBuffer::new()),
1003        }
1004    }
1005
1006    pub fn options(&self) -> &TextOptions {
1007        self.atlas.options()
1008    }
1009
1010    /// Take the recycled shaping buffer (or create a new one if already taken).
1011    pub fn take_shape_buffer(&mut self) -> harfrust::UnicodeBuffer {
1012        self.shape_buffer.take().unwrap_or_default()
1013    }
1014
1015    /// Return a shaping buffer for reuse.
1016    pub fn return_shape_buffer(&mut self, buffer: harfrust::UnicodeBuffer) {
1017        self.shape_buffer = Some(buffer);
1018    }
1019
1020    /// Get the right font implementation from [`FontFamily`].
1021    pub fn font(&mut self, family: &FontFamily) -> Font<'_> {
1022        let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| {
1023            let fonts = &self.definitions.families.get(family);
1024            let fonts =
1025                fonts.unwrap_or_else(|| panic!("FontFamily::{family:?} is not bound to any fonts"));
1026
1027            let fonts: Vec<FontFaceKey> = fonts
1028                .iter()
1029                .map(|font_name| {
1030                    *self
1031                        .fonts_by_name
1032                        .get(font_name)
1033                        .unwrap_or_else(|| panic!("No font data found for {font_name:?}"))
1034                })
1035                .collect();
1036
1037            CachedFamily::new(fonts, &mut self.fonts_by_id)
1038        });
1039        Font {
1040            fonts_by_id: &mut self.fonts_by_id,
1041            cached_family,
1042            atlas: &mut self.atlas,
1043        }
1044    }
1045}
1046
1047// ----------------------------------------------------------------------------
1048
1049struct CachedGalley {
1050    /// When it was last used
1051    last_used: u32,
1052
1053    /// Hashes of all other entries this one depends on for quick re-layout.
1054    /// Their `last_used`s should be updated alongside this one to make sure they're
1055    /// not evicted.
1056    children: Option<Arc<[u64]>>,
1057
1058    galley: Arc<Galley>,
1059}
1060
1061#[derive(Default)]
1062struct GalleyCache {
1063    /// Frame counter used to do garbage collection on the cache
1064    generation: u32,
1065    cache: nohash_hasher::IntMap<u64, CachedGalley>,
1066}
1067
1068impl GalleyCache {
1069    fn layout_internal(
1070        &mut self,
1071        fonts: &mut FontsImpl,
1072        mut job: LayoutJob,
1073        pixels_per_point: f32,
1074        allow_split_paragraphs: bool,
1075    ) -> (u64, Arc<Galley>) {
1076        if job.wrap.max_width.is_finite() {
1077            // Protect against rounding errors in egui layout code.
1078
1079            // Say the user asks to wrap at width 200.0.
1080            // The text layout wraps, and reports that the final width was 196.0 points.
1081            // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say).
1082            // On the next frame, this is then set as the max width for the tooltip,
1083            // and we end up calling the text layout code again, this time with a wrap width of 196.0.
1084            // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced,
1085            // so that we actually set a wrap-width of 195.9997 instead.
1086            // Now the text that fit perfrectly at 196.0 needs to wrap one word earlier,
1087            // and so the text re-wraps and reports a new width of 185.0 points.
1088            // And then the cycle continues.
1089
1090            // So we limit max_width to integers.
1091
1092            // Related issues:
1093            // * https://github.com/emilk/egui/issues/4927
1094            // * https://github.com/emilk/egui/issues/4928
1095            // * https://github.com/emilk/egui/issues/5084
1096            // * https://github.com/emilk/egui/issues/5163
1097
1098            job.wrap.max_width = job.wrap.max_width.round();
1099        }
1100
1101        let hash = crate::util::hash((&job, OrderedFloat(pixels_per_point))); // TODO(emilk): even faster hasher?
1102
1103        let galley = match self.cache.entry(hash) {
1104            std::collections::hash_map::Entry::Occupied(entry) => {
1105                // The job was found in cache - no need to re-layout.
1106                let cached = entry.into_mut();
1107                cached.last_used = self.generation;
1108
1109                let galley = Arc::clone(&cached.galley);
1110                if let Some(children) = &cached.children {
1111                    // The point of `allow_split_paragraphs` is to split large jobs into paragraph,
1112                    // and then cache each paragraph individually.
1113                    // That way, if we edit a single paragraph, only that paragraph will be re-layouted.
1114                    // For that to work we need to keep all the child/paragraph
1115                    // galleys alive while the parent galley is alive:
1116                    for child_hash in Arc::clone(children).iter() {
1117                        if let Some(cached_child) = self.cache.get_mut(child_hash) {
1118                            cached_child.last_used = self.generation;
1119                        }
1120                    }
1121                }
1122
1123                galley
1124            }
1125            std::collections::hash_map::Entry::Vacant(entry) => {
1126                let job = Arc::new(job);
1127                if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) {
1128                    let (child_galleys, child_hashes) =
1129                        self.layout_each_paragraph_individually(fonts, &job, pixels_per_point);
1130                    debug_assert_eq!(
1131                        child_hashes.len(),
1132                        child_galleys.len(),
1133                        "Bug in `layout_each_paragraph_individually`"
1134                    );
1135                    let galley = Arc::new(Galley::concat(job, &child_galleys, pixels_per_point));
1136
1137                    self.cache.insert(
1138                        hash,
1139                        CachedGalley {
1140                            last_used: self.generation,
1141                            children: Some(child_hashes.into()),
1142                            galley: Arc::clone(&galley),
1143                        },
1144                    );
1145                    galley
1146                } else {
1147                    let galley = super::layout(fonts, pixels_per_point, job);
1148                    let galley = Arc::new(galley);
1149                    entry.insert(CachedGalley {
1150                        last_used: self.generation,
1151                        children: None,
1152                        galley: Arc::clone(&galley),
1153                    });
1154                    galley
1155                }
1156            }
1157        };
1158
1159        (hash, galley)
1160    }
1161
1162    fn layout(
1163        &mut self,
1164        fonts: &mut FontsImpl,
1165        pixels_per_point: f32,
1166        job: LayoutJob,
1167        allow_split_paragraphs: bool,
1168    ) -> Arc<Galley> {
1169        self.layout_internal(fonts, job, pixels_per_point, allow_split_paragraphs)
1170            .1
1171    }
1172
1173    /// Split on `\n` and lay out (and cache) each paragraph individually.
1174    fn layout_each_paragraph_individually(
1175        &mut self,
1176        fonts: &mut FontsImpl,
1177        job: &LayoutJob,
1178        pixels_per_point: f32,
1179    ) -> (Vec<Arc<Galley>>, Vec<u64>) {
1180        profiling::function_scope!();
1181
1182        let mut current_section = 0;
1183        let mut start = 0;
1184        let mut max_rows_remaining = job.wrap.max_rows;
1185        let mut child_galleys = Vec::new();
1186        let mut child_hashes = Vec::new();
1187
1188        while start < job.text.len() {
1189            let is_first_paragraph = start == 0;
1190            // `end` will not include the `\n` since we don't want to create an empty row in our
1191            // split galley
1192            let mut end = job.text[start..]
1193                .find('\n')
1194                .map_or(job.text.len(), |i| start + i);
1195            if end == job.text.len() - 1 && job.text.ends_with('\n') {
1196                end += 1; // If the text ends with a newline, we include it in the last paragraph.
1197            }
1198
1199            let mut paragraph_job = LayoutJob {
1200                text: job.text[start..end].to_owned(),
1201                wrap: crate::text::TextWrapping {
1202                    max_rows: max_rows_remaining,
1203                    ..job.wrap
1204                },
1205                sections: Vec::new(),
1206                break_on_newline: job.break_on_newline,
1207                halign: job.halign,
1208                justify: job.justify,
1209                first_row_min_height: if is_first_paragraph {
1210                    job.first_row_min_height
1211                } else {
1212                    0.0
1213                },
1214                round_output_to_gui: job.round_output_to_gui,
1215                keep_trailing_whitespace: job.keep_trailing_whitespace,
1216            };
1217
1218            // Add overlapping sections:
1219            for section in &job.sections[current_section..job.sections.len()] {
1220                let LayoutSection {
1221                    leading_space,
1222                    byte_range: section_range,
1223                    format,
1224                } = section;
1225
1226                // `start` and `end` are the byte range of the current paragraph.
1227                // How does the current section overlap with the paragraph range?
1228
1229                if section_range.end <= ByteIndex(start) {
1230                    // The section is behind us
1231                    current_section += 1;
1232                } else if ByteIndex(end) < section_range.start {
1233                    break; // Haven't reached this one yet.
1234                } else {
1235                    // Section range overlaps with paragraph range
1236                    debug_assert!(
1237                        section_range.start <= section_range.end,
1238                        "Bad byte_range: {section_range:?}"
1239                    );
1240                    let new_range = section_range.start.saturating_sub(start)
1241                        ..(section_range.end.min(ByteIndex(end))).saturating_sub(start);
1242                    debug_assert!(
1243                        new_range.start <= new_range.end,
1244                        "Bad new section range: {new_range:?}"
1245                    );
1246                    paragraph_job.sections.push(LayoutSection {
1247                        leading_space: if ByteIndex(start) <= section_range.start {
1248                            *leading_space
1249                        } else {
1250                            0.0
1251                        },
1252                        byte_range: new_range,
1253                        format: format.clone(),
1254                    });
1255                }
1256            }
1257
1258            // TODO(emilk): we could lay out each paragraph in parallel to get a nice speedup on multicore machines.
1259            let (hash, galley) =
1260                self.layout_internal(fonts, paragraph_job, pixels_per_point, false);
1261            child_hashes.push(hash);
1262
1263            // This will prevent us from invalidating cache entries unnecessarily:
1264            if max_rows_remaining != usize::MAX {
1265                max_rows_remaining -= galley.rows.len();
1266            }
1267
1268            let elided = galley.elided;
1269            child_galleys.push(galley);
1270            if elided {
1271                break;
1272            }
1273
1274            start = end + 1;
1275        }
1276
1277        (child_galleys, child_hashes)
1278    }
1279
1280    pub fn num_galleys_in_cache(&self) -> usize {
1281        self.cache.len()
1282    }
1283
1284    /// Must be called once per frame to clear the [`Galley`] cache.
1285    pub fn flush_cache(&mut self) {
1286        let current_generation = self.generation;
1287        self.cache.retain(|_key, cached| {
1288            cached.last_used == current_generation // only keep those that were used this frame
1289        });
1290        self.generation = self.generation.wrapping_add(1);
1291    }
1292}
1293
1294/// If true, lay out and cache each paragraph (sections separated by newlines) individually.
1295///
1296/// This makes it much faster to re-layout the full text when only a portion of it has changed since last frame, i.e. when editing somewhere in a file with thousands of lines/paragraphs.
1297fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool {
1298    // We currently don't support this elided text, i.e. when `max_rows` is set.
1299    // Most often, elided text is elided to one row,
1300    // and so will always be fast to lay out.
1301    job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n')
1302}
1303
1304#[cfg(feature = "default_fonts")]
1305#[cfg(test)]
1306mod tests {
1307    use core::f32;
1308
1309    use super::*;
1310    use crate::text::{TextWrapping, layout};
1311    use crate::{Stroke, text::TextFormat};
1312    use ecolor::Color32;
1313    use emath::Align;
1314
1315    fn jobs() -> Vec<LayoutJob> {
1316        vec![
1317            LayoutJob::simple(
1318                String::default(),
1319                FontId::new(14.0, FontFamily::Monospace),
1320                Color32::WHITE,
1321                f32::INFINITY,
1322            ),
1323            LayoutJob::simple(
1324                "ends with newlines\n\n".to_owned(),
1325                FontId::new(14.0, FontFamily::Monospace),
1326                Color32::WHITE,
1327                f32::INFINITY,
1328            ),
1329            LayoutJob::simple(
1330                "Simple test.".to_owned(),
1331                FontId::new(14.0, FontFamily::Monospace),
1332                Color32::WHITE,
1333                f32::INFINITY,
1334            ),
1335            {
1336                let mut job = LayoutJob::simple(
1337                    "hi".to_owned(),
1338                    FontId::default(),
1339                    Color32::WHITE,
1340                    f32::INFINITY,
1341                );
1342                job.append("\n", 0.0, TextFormat::default());
1343                job.append("\n", 0.0, TextFormat::default());
1344                job.append("world", 0.0, TextFormat::default());
1345                job.wrap.max_rows = 2;
1346                job
1347            },
1348            {
1349                let mut job = LayoutJob::simple(
1350                    "Test text with a lot of words\n and a newline.".to_owned(),
1351                    FontId::new(14.0, FontFamily::Monospace),
1352                    Color32::WHITE,
1353                    40.0,
1354                );
1355                job.first_row_min_height = 30.0;
1356                job
1357            },
1358            LayoutJob::simple(
1359                "This some text that may be long.\nDet kanske också finns lite ÅÄÖ här.".to_owned(),
1360                FontId::new(14.0, FontFamily::Proportional),
1361                Color32::WHITE,
1362                50.0,
1363            ),
1364            {
1365                let mut job = LayoutJob {
1366                    first_row_min_height: 20.0,
1367                    ..Default::default()
1368                };
1369                job.append(
1370                    "1st paragraph has underline and strikethrough, and has some non-ASCII characters:\n ÅÄÖ.",
1371                    0.0,
1372                    TextFormat {
1373                        font_id: FontId::new(15.0, FontFamily::Monospace),
1374                        underline: Stroke::new(1.0, Color32::RED),
1375                        strikethrough: Stroke::new(1.0, Color32::GREEN),
1376                        ..Default::default()
1377                    },
1378                );
1379                job.append(
1380                    "2nd paragraph has some leading space.\n",
1381                    16.0,
1382                    TextFormat {
1383                        font_id: FontId::new(14.0, FontFamily::Proportional),
1384                        ..Default::default()
1385                    },
1386                );
1387                job.append(
1388                    "3rd paragraph is kind of boring, but has italics.\nAnd a newline",
1389                    0.0,
1390                    TextFormat {
1391                        font_id: FontId::new(10.0, FontFamily::Proportional),
1392                        italics: true,
1393                        ..Default::default()
1394                    },
1395                );
1396
1397                job
1398            },
1399            {
1400                // Regression test for <https://github.com/emilk/egui/issues/7378>
1401                let mut job = LayoutJob::default();
1402                job.append("\n", 0.0, TextFormat::default());
1403                job.append("", 0.0, TextFormat::default());
1404                job
1405            },
1406        ]
1407    }
1408
1409    #[expect(clippy::print_stdout)]
1410    #[test]
1411    fn test_split_paragraphs() {
1412        for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
1413            let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1414
1415            for halign in [Align::Min, Align::Center, Align::Max] {
1416                for justify in [false, true] {
1417                    for mut job in jobs() {
1418                        job.halign = halign;
1419                        job.justify = justify;
1420
1421                        let whole = GalleyCache::default().layout(
1422                            &mut fonts,
1423                            pixels_per_point,
1424                            job.clone(),
1425                            false,
1426                        );
1427
1428                        let split = GalleyCache::default().layout(
1429                            &mut fonts,
1430                            pixels_per_point,
1431                            job.clone(),
1432                            true,
1433                        );
1434
1435                        for (i, row) in whole.rows.iter().enumerate() {
1436                            println!(
1437                                "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}",
1438                                row.row.section_index_at_start,
1439                                row.row.glyphs.first().map(|g| g.section_index)
1440                            );
1441                        }
1442                        for (i, row) in split.rows.iter().enumerate() {
1443                            println!(
1444                                "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}",
1445                                row.row.section_index_at_start,
1446                                row.row.glyphs.first().map(|g| g.section_index)
1447                            );
1448                        }
1449
1450                        // Don't compare for equaliity; but format with a specific precision and make sure we hit that.
1451                        // NOTE: we use a rather low precision, because as long as we're within a pixel I think it's good enough.
1452                        similar_asserts::assert_eq!(
1453                            format!("{:#.1?}", split),
1454                            format!("{:#.1?}", whole),
1455                            "pixels_per_point: {pixels_per_point:.2}, input text: '{}'",
1456                            job.text
1457                        );
1458                    }
1459                }
1460            }
1461        }
1462    }
1463
1464    #[test]
1465    fn test_intrinsic_size() {
1466        let pixels_per_point = [1.0, 1.3, 2.0, 0.867];
1467        let max_widths = [40.0, 80.0, 133.0, 200.0];
1468        let rounded_output_to_gui = [false, true];
1469
1470        for pixels_per_point in pixels_per_point {
1471            let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1472
1473            for &max_width in &max_widths {
1474                for round_output_to_gui in rounded_output_to_gui {
1475                    for mut job in jobs() {
1476                        job.wrap = TextWrapping::wrap_at_width(max_width);
1477
1478                        job.round_output_to_gui = round_output_to_gui;
1479
1480                        let galley_wrapped =
1481                            layout(&mut fonts, pixels_per_point, job.clone().into());
1482
1483                        job.wrap = TextWrapping::no_max_width();
1484
1485                        let text = job.text.clone();
1486                        let galley_unwrapped = layout(&mut fonts, pixels_per_point, job.into());
1487
1488                        let intrinsic_size = galley_wrapped.intrinsic_size();
1489                        let unwrapped_size = galley_unwrapped.size();
1490
1491                        let difference = (intrinsic_size - unwrapped_size).length().abs();
1492                        similar_asserts::assert_eq!(
1493                            format!("{intrinsic_size:.4?}"),
1494                            format!("{unwrapped_size:.4?}"),
1495                            "Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?}
1496                                Difference: {difference:.8?}
1497                                wrapped rows: {}, unwrapped rows: {}
1498                                pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}",
1499                            galley_wrapped.rows.len(),
1500                            galley_unwrapped.rows.len()
1501                            );
1502                        similar_asserts::assert_eq!(
1503                            format!("{intrinsic_size:.4?}"),
1504                            format!("{unwrapped_size:.4?}"),
1505                            "Unwrapped galley intrinsic size should exactly match its size. \
1506                                {:.8?} vs {:8?}",
1507                            galley_unwrapped.intrinsic_size(),
1508                            galley_unwrapped.size(),
1509                        );
1510                    }
1511                }
1512            }
1513        }
1514    }
1515
1516    #[test]
1517    fn test_fallback_glyph_width() {
1518        let mut fonts = Fonts::new(TextOptions::default(), FontDefinitions::empty());
1519        let mut view = fonts.with_pixels_per_point(1.0);
1520
1521        let width = view.glyph_width(&FontId::new(12.0, FontFamily::Proportional), ' ');
1522        assert_eq!(width, 0.0);
1523    }
1524}