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#[derive(Clone, Debug, PartialEq)]
20#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
21pub struct FontId {
22 pub size: f32,
24
25 pub family: FontFamily,
27 }
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#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
73#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
74pub enum FontFamily {
75 #[default]
79 Proportional,
80
81 Monospace,
85
86 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#[derive(Clone, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
112pub struct FontData {
113 pub font: Cow<'static, [u8]>,
115
116 pub index: u32,
119
120 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 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#[derive(Clone, Debug, PartialEq)]
180pub struct FontVariationAxis {
181 pub tag: Tag,
183
184 pub name: Option<String>,
186
187 pub range: Rangef,
189
190 pub default: f32,
192
193 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#[derive(Clone, Debug, PartialEq)]
207#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
208pub struct FontTweak {
209 pub scale: f32,
214
215 pub y_offset_factor: f32,
225
226 pub y_offset: f32,
233
234 pub hinting: Option<bool>,
238
239 pub hinting_target: HintingTarget,
243
244 pub subpixel_binning: Option<bool>,
248
249 pub coords: VariationCoords,
251
252 pub thin_space_width: f32,
259
260 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
297pub enum HintingTarget {
298 Mono,
304
305 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
340#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
341pub struct SmoothHinting {
342 pub light: bool,
349
350 pub symmetric_rendering: bool,
360
361 pub preserve_linear_metrics: bool,
373}
374
375impl Default for SmoothHinting {
376 fn default() -> Self {
377 Self {
380 light: false,
381 symmetric_rendering: true,
382 preserve_linear_metrics: true,
383 }
384 }
385}
386
387pub 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#[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 pub font_data: BTreeMap<String, Arc<FontData>>,
436
437 pub families: BTreeMap<FontFamily, Vec<String>>,
444}
445
446#[derive(Debug, Clone)]
447pub struct FontInsert {
448 pub name: String,
450
451 pub data: FontData,
453
454 pub families: Vec<InsertFontFamily>,
456}
457
458#[derive(Debug, Clone)]
459pub struct InsertFontFamily {
460 pub family: FontFamily,
462
463 pub priority: FontPriority,
465}
466
467#[derive(Debug, Clone)]
468pub enum FontPriority {
469 Highest,
473
474 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 #[cfg(not(feature = "default_fonts"))]
494 fn default() -> Self {
495 Self::empty()
496 }
497
498 #[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 font_data.insert(
513 "NotoEmoji-Regular".to_owned(),
514 Arc::new(FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak {
515 scale: 0.81, ..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 font_data.insert(
527 "emoji-icon-font".to_owned(),
528 Arc::new(FontData::from_static(EMOJI_ICON).tweak(FontTweak {
529 scale: 0.90, ..Default::default()
531 })),
532 );
533
534 families.insert(
535 FontFamily::Monospace,
536 vec![
537 "Hack".to_owned(),
538 "Ubuntu-Light".to_owned(), "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 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 #[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 #[cfg(not(feature = "default_fonts"))]
585 pub fn builtin_font_names() -> &'static [&'static str] {
586 &[]
587 }
588}
589
590#[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
605impl nohash_hasher::IsEnabled for FontFaceKey {}
607
608#[derive(Debug)]
610pub(super) struct CachedFamily {
611 pub fonts: Vec<FontFaceKey>,
612
613 pub characters: Option<BTreeMap<char, Vec<String>>>,
615
616 pub replacement_face_key: FontFaceKey,
618
619 pub replacement_char: char,
624
625 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 = '◻'; const FALLBACK_REPLACEMENT_CHAR: char = '?'; 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 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
696pub struct Fonts {
708 pub fonts: FontsImpl,
709 galley_cache: GalleyCache,
710}
711
712impl Fonts {
713 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 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 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 pub fn texture_atlas(&self) -> &TextureAtlas {
763 &self.fonts.atlas
764 }
765
766 #[inline]
768 pub fn image(&self) -> crate::ColorImage {
769 self.fonts.atlas.image().clone()
770 }
771
772 pub fn font_image_size(&self) -> [usize; 2] {
775 self.fonts.atlas.size()
776 }
777
778 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 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 pub fn font_atlas_fill_ratio(&self) -> f32 {
797 self.fonts.atlas.fill_ratio()
798 }
799
800 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
810pub 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 #[inline]
832 pub fn image(&self) -> crate::ColorImage {
833 self.fonts.atlas.image().clone()
834 }
835
836 pub fn font_image_size(&self) -> [usize; 2] {
839 self.fonts.atlas.size()
840 }
841
842 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 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 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 #[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 &VariationCoords::default(),
873 )
874 .row_height
875 }
876
877 pub fn families(&self) -> Vec<FontFamily> {
879 self.fonts.definitions.families.keys().cloned().collect()
880 }
881
882 #[inline]
890 pub fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
891 let allow_split_paragraphs = true; 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 pub fn font_atlas_fill_ratio(&self) -> f32 {
909 self.fonts.atlas.fill_ratio()
910 }
911
912 #[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 #[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 #[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
955pub 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 shape_buffer: Option<harfrust::UnicodeBuffer>,
969}
970
971impl FontsImpl {
972 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; 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 pub fn take_shape_buffer(&mut self) -> harfrust::UnicodeBuffer {
1012 self.shape_buffer.take().unwrap_or_default()
1013 }
1014
1015 pub fn return_shape_buffer(&mut self, buffer: harfrust::UnicodeBuffer) {
1017 self.shape_buffer = Some(buffer);
1018 }
1019
1020 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
1047struct CachedGalley {
1050 last_used: u32,
1052
1053 children: Option<Arc<[u64]>>,
1057
1058 galley: Arc<Galley>,
1059}
1060
1061#[derive(Default)]
1062struct GalleyCache {
1063 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 job.wrap.max_width = job.wrap.max_width.round();
1099 }
1100
1101 let hash = crate::util::hash((&job, OrderedFloat(pixels_per_point))); let galley = match self.cache.entry(hash) {
1104 std::collections::hash_map::Entry::Occupied(entry) => {
1105 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 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 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 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; }
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 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 if section_range.end <= ByteIndex(start) {
1230 current_section += 1;
1232 } else if ByteIndex(end) < section_range.start {
1233 break; } else {
1235 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 let (hash, galley) =
1260 self.layout_internal(fonts, paragraph_job, pixels_per_point, false);
1261 child_hashes.push(hash);
1262
1263 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 pub fn flush_cache(&mut self) {
1286 let current_generation = self.generation;
1287 self.cache.retain(|_key, cached| {
1288 cached.last_used == current_generation });
1290 self.generation = self.generation.wrapping_add(1);
1291 }
1292}
1293
1294fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool {
1298 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 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 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}