1use std::{
2 borrow::Cow,
3 collections::BTreeMap,
4 sync::{
5 Arc,
6 atomic::{AtomicU64, Ordering},
7 },
8};
9
10use crate::{
11 TextureAtlas,
12 text::{
13 Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
14 font::{Font, FontFace, GlyphInfo},
15 },
16};
17use emath::{NumExt as _, OrderedFloat};
18
19#[cfg(feature = "default_fonts")]
20use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT};
21
22#[derive(Clone, Debug, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
27pub struct FontId {
28 pub size: f32,
30
31 pub family: FontFamily,
33 }
35
36impl Default for FontId {
37 #[inline]
38 fn default() -> Self {
39 Self {
40 size: 14.0,
41 family: FontFamily::Proportional,
42 }
43 }
44}
45
46impl FontId {
47 #[inline]
48 pub const fn new(size: f32, family: FontFamily) -> Self {
49 Self { size, family }
50 }
51
52 #[inline]
53 pub const fn proportional(size: f32) -> Self {
54 Self::new(size, FontFamily::Proportional)
55 }
56
57 #[inline]
58 pub const fn monospace(size: f32) -> Self {
59 Self::new(size, FontFamily::Monospace)
60 }
61}
62
63impl std::hash::Hash for FontId {
64 #[inline(always)]
65 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
66 let Self { size, family } = self;
67 emath::OrderedFloat(*size).hash(state);
68 family.hash(state);
69 }
70}
71
72#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
79#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
80pub enum FontFamily {
81 #[default]
85 Proportional,
86
87 Monospace,
91
92 Name(Arc<str>),
101}
102
103impl std::fmt::Display for FontFamily {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::Monospace => "Monospace".fmt(f),
107 Self::Proportional => "Proportional".fmt(f),
108 Self::Name(name) => (*name).fmt(f),
109 }
110 }
111}
112
113#[derive(Clone, Debug, PartialEq)]
117#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
118pub struct FontData {
119 pub font: Cow<'static, [u8]>,
121
122 pub index: u32,
125
126 pub tweak: FontTweak,
128}
129
130impl FontData {
131 pub fn from_static(font: &'static [u8]) -> Self {
132 Self {
133 font: Cow::Borrowed(font),
134 index: 0,
135 tweak: Default::default(),
136 }
137 }
138
139 pub fn from_owned(font: Vec<u8>) -> Self {
140 Self {
141 font: Cow::Owned(font),
142 index: 0,
143 tweak: Default::default(),
144 }
145 }
146
147 pub fn tweak(self, tweak: FontTweak) -> Self {
148 Self { tweak, ..self }
149 }
150}
151
152impl AsRef<[u8]> for FontData {
153 fn as_ref(&self) -> &[u8] {
154 self.font.as_ref()
155 }
156}
157
158#[derive(Clone, Debug, PartialEq)]
162#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
163pub struct FontTweak {
164 pub scale: f32,
169
170 pub y_offset_factor: f32,
180
181 pub y_offset: f32,
188
189 pub hinting_override: Option<bool>,
193
194 pub coords: VariationCoords,
196}
197
198impl Default for FontTweak {
199 fn default() -> Self {
200 Self {
201 scale: 1.0,
202 y_offset_factor: 0.0,
203 y_offset: 0.0,
204 hinting_override: None,
205 coords: VariationCoords::default(),
206 }
207 }
208}
209
210pub type Blob = Arc<dyn AsRef<[u8]> + Send + Sync>;
213
214fn blob_from_font_data(data: &FontData) -> Blob {
215 match data.clone().font {
216 Cow::Borrowed(bytes) => Arc::new(bytes) as Blob,
217 Cow::Owned(bytes) => Arc::new(bytes) as Blob,
218 }
219}
220
221#[derive(Clone, Debug, PartialEq)]
252#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
253#[cfg_attr(feature = "serde", serde(default))]
254pub struct FontDefinitions {
255 pub font_data: BTreeMap<String, Arc<FontData>>,
259
260 pub families: BTreeMap<FontFamily, Vec<String>>,
267}
268
269#[derive(Debug, Clone)]
270pub struct FontInsert {
271 pub name: String,
273
274 pub data: FontData,
276
277 pub families: Vec<InsertFontFamily>,
279}
280
281#[derive(Debug, Clone)]
282pub struct InsertFontFamily {
283 pub family: FontFamily,
285
286 pub priority: FontPriority,
288}
289
290#[derive(Debug, Clone)]
291pub enum FontPriority {
292 Highest,
296
297 Lowest,
301}
302
303impl FontInsert {
304 pub fn new(name: &str, data: FontData, families: Vec<InsertFontFamily>) -> Self {
305 Self {
306 name: name.to_owned(),
307 data,
308 families,
309 }
310 }
311}
312
313impl Default for FontDefinitions {
314 #[cfg(not(feature = "default_fonts"))]
317 fn default() -> Self {
318 Self::empty()
319 }
320
321 #[cfg(feature = "default_fonts")]
324 fn default() -> Self {
325 let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
326
327 let mut families = BTreeMap::new();
328
329 font_data.insert(
330 "Hack".to_owned(),
331 Arc::new(FontData::from_static(HACK_REGULAR)),
332 );
333
334 font_data.insert(
336 "NotoEmoji-Regular".to_owned(),
337 Arc::new(FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak {
338 scale: 0.81, ..Default::default()
340 })),
341 );
342
343 font_data.insert(
344 "Ubuntu-Light".to_owned(),
345 Arc::new(FontData::from_static(UBUNTU_LIGHT)),
346 );
347
348 font_data.insert(
350 "emoji-icon-font".to_owned(),
351 Arc::new(FontData::from_static(EMOJI_ICON).tweak(FontTweak {
352 scale: 0.90, ..Default::default()
354 })),
355 );
356
357 families.insert(
358 FontFamily::Monospace,
359 vec![
360 "Hack".to_owned(),
361 "Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned(),
363 "emoji-icon-font".to_owned(),
364 ],
365 );
366 families.insert(
367 FontFamily::Proportional,
368 vec![
369 "Ubuntu-Light".to_owned(),
370 "NotoEmoji-Regular".to_owned(),
371 "emoji-icon-font".to_owned(),
372 ],
373 );
374
375 Self {
376 font_data,
377 families,
378 }
379 }
380}
381
382impl FontDefinitions {
383 pub fn empty() -> Self {
385 let mut families = BTreeMap::new();
386 families.insert(FontFamily::Monospace, vec![]);
387 families.insert(FontFamily::Proportional, vec![]);
388
389 Self {
390 font_data: Default::default(),
391 families,
392 }
393 }
394
395 #[cfg(feature = "default_fonts")]
397 pub fn builtin_font_names() -> &'static [&'static str] {
398 &[
399 "Ubuntu-Light",
400 "NotoEmoji-Regular",
401 "emoji-icon-font",
402 "Hack",
403 ]
404 }
405
406 #[cfg(not(feature = "default_fonts"))]
408 pub fn builtin_font_names() -> &'static [&'static str] {
409 &[]
410 }
411}
412
413#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
415pub(crate) struct FontFaceKey(u64);
416
417impl FontFaceKey {
418 pub const INVALID: Self = Self(0);
419
420 fn new() -> Self {
421 static KEY_COUNTER: AtomicU64 = AtomicU64::new(1);
422 Self(crate::util::hash(
423 KEY_COUNTER.fetch_add(1, Ordering::Relaxed),
424 ))
425 }
426}
427
428impl nohash_hasher::IsEnabled for FontFaceKey {}
430
431#[derive(Debug)]
433pub(super) struct CachedFamily {
434 pub fonts: Vec<FontFaceKey>,
435
436 pub characters: Option<BTreeMap<char, Vec<String>>>,
438
439 pub replacement_glyph: (FontFaceKey, GlyphInfo),
440
441 pub glyph_info_cache: ahash::HashMap<char, (FontFaceKey, GlyphInfo)>,
442}
443
444impl CachedFamily {
445 fn new(
446 fonts: Vec<FontFaceKey>,
447 fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
448 ) -> Self {
449 if fonts.is_empty() {
450 return Self {
451 fonts,
452 characters: None,
453 replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
454 glyph_info_cache: Default::default(),
455 };
456 }
457
458 let mut slf = Self {
459 fonts,
460 characters: None,
461 replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
462 glyph_info_cache: Default::default(),
463 };
464
465 const PRIMARY_REPLACEMENT_CHAR: char = '◻'; const FALLBACK_REPLACEMENT_CHAR: char = '?'; let replacement_glyph = slf
469 .glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR, fonts_by_id)
470 .or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR, fonts_by_id))
471 .unwrap_or_else(|| {
472 log::warn!(
473 "Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
474 );
475 (FontFaceKey::INVALID, GlyphInfo::INVISIBLE)
476 });
477 slf.replacement_glyph = replacement_glyph;
478
479 slf
480 }
481
482 pub(crate) fn glyph_info_no_cache_or_fallback(
483 &mut self,
484 c: char,
485 fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
486 ) -> Option<(FontFaceKey, GlyphInfo)> {
487 for font_key in &self.fonts {
488 let font_face = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID");
489 if let Some(glyph_info) = font_face.glyph_info(c) {
490 self.glyph_info_cache.insert(c, (*font_key, glyph_info));
491 return Some((*font_key, glyph_info));
492 }
493 }
494 None
495 }
496}
497
498pub struct Fonts {
510 pub fonts: FontsImpl,
511 galley_cache: GalleyCache,
512}
513
514impl Fonts {
515 pub fn new(options: TextOptions, definitions: FontDefinitions) -> Self {
518 Self {
519 fonts: FontsImpl::new(options, definitions),
520 galley_cache: Default::default(),
521 }
522 }
523
524 pub fn begin_pass(&mut self, options: TextOptions) {
531 let text_options_changed = self.fonts.options() != &options;
532 let font_atlas_almost_full = self.fonts.atlas.fill_ratio() > 0.8;
533 let needs_recreate = text_options_changed || font_atlas_almost_full;
534
535 if needs_recreate {
536 let definitions = self.fonts.definitions.clone();
537
538 *self = Self {
539 fonts: FontsImpl::new(options, definitions),
540 galley_cache: Default::default(),
541 };
542 }
543
544 self.galley_cache.flush_cache();
545 }
546
547 pub fn font_image_delta(&mut self) -> Option<crate::ImageDelta> {
549 self.fonts.atlas.take_delta()
550 }
551
552 #[inline]
553 pub fn options(&self) -> &TextOptions {
554 self.texture_atlas().options()
555 }
556
557 #[inline]
558 pub fn definitions(&self) -> &FontDefinitions {
559 &self.fonts.definitions
560 }
561
562 pub fn texture_atlas(&self) -> &TextureAtlas {
565 &self.fonts.atlas
566 }
567
568 #[inline]
570 pub fn image(&self) -> crate::ColorImage {
571 self.fonts.atlas.image().clone()
572 }
573
574 pub fn font_image_size(&self) -> [usize; 2] {
577 self.fonts.atlas.size()
578 }
579
580 pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
582 self.fonts.font(&font_id.family).has_glyph(c)
583 }
584
585 pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
587 self.fonts.font(&font_id.family).has_glyphs(s)
588 }
589
590 pub fn num_galleys_in_cache(&self) -> usize {
591 self.galley_cache.num_galleys_in_cache()
592 }
593
594 pub fn font_atlas_fill_ratio(&self) -> f32 {
599 self.fonts.atlas.fill_ratio()
600 }
601
602 pub fn with_pixels_per_point(&mut self, pixels_per_point: f32) -> FontsView<'_> {
604 FontsView {
605 fonts: &mut self.fonts,
606 galley_cache: &mut self.galley_cache,
607 pixels_per_point,
608 }
609 }
610}
611
612pub struct FontsView<'a> {
616 pub fonts: &'a mut FontsImpl,
617 galley_cache: &'a mut GalleyCache,
618 pixels_per_point: f32,
619}
620
621impl FontsView<'_> {
622 #[inline]
623 pub fn options(&self) -> &TextOptions {
624 self.fonts.options()
625 }
626
627 #[inline]
628 pub fn definitions(&self) -> &FontDefinitions {
629 &self.fonts.definitions
630 }
631
632 #[inline]
634 pub fn image(&self) -> crate::ColorImage {
635 self.fonts.atlas.image().clone()
636 }
637
638 pub fn font_image_size(&self) -> [usize; 2] {
641 self.fonts.atlas.size()
642 }
643
644 pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
648 self.fonts
649 .font(&font_id.family)
650 .glyph_width(c, font_id.size)
651 }
652
653 pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
655 self.fonts.font(&font_id.family).has_glyph(c)
656 }
657
658 pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
660 self.fonts.font(&font_id.family).has_glyphs(s)
661 }
662
663 #[inline]
667 pub fn row_height(&mut self, font_id: &FontId) -> f32 {
668 self.fonts
669 .font(&font_id.family)
670 .styled_metrics(
671 self.pixels_per_point,
672 font_id.size,
673 &VariationCoords::default(),
675 )
676 .row_height
677 }
678
679 pub fn families(&self) -> Vec<FontFamily> {
681 self.fonts.definitions.families.keys().cloned().collect()
682 }
683
684 #[inline]
692 pub fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
693 let allow_split_paragraphs = true; self.galley_cache.layout(
695 self.fonts,
696 self.pixels_per_point,
697 job,
698 allow_split_paragraphs,
699 )
700 }
701
702 pub fn num_galleys_in_cache(&self) -> usize {
703 self.galley_cache.num_galleys_in_cache()
704 }
705
706 pub fn font_atlas_fill_ratio(&self) -> f32 {
711 self.fonts.atlas.fill_ratio()
712 }
713
714 #[inline]
718 pub fn layout(
719 &mut self,
720 text: String,
721 font_id: FontId,
722 color: crate::Color32,
723 wrap_width: f32,
724 ) -> Arc<Galley> {
725 let job = LayoutJob::simple(text, font_id, color, wrap_width);
726 self.layout_job(job)
727 }
728
729 #[inline]
733 pub fn layout_no_wrap(
734 &mut self,
735 text: String,
736 font_id: FontId,
737 color: crate::Color32,
738 ) -> Arc<Galley> {
739 let job = LayoutJob::simple(text, font_id, color, f32::INFINITY);
740 self.layout_job(job)
741 }
742
743 #[inline]
747 pub fn layout_delayed_color(
748 &mut self,
749 text: String,
750 font_id: FontId,
751 wrap_width: f32,
752 ) -> Arc<Galley> {
753 self.layout(text, font_id, crate::Color32::PLACEHOLDER, wrap_width)
754 }
755}
756
757pub struct FontsImpl {
763 definitions: FontDefinitions,
764 atlas: TextureAtlas,
765 fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace>,
766 fonts_by_name: ahash::HashMap<String, FontFaceKey>,
767 family_cache: ahash::HashMap<FontFamily, CachedFamily>,
768}
769
770impl FontsImpl {
771 pub fn new(options: TextOptions, definitions: FontDefinitions) -> Self {
774 let texture_width = options.max_texture_side.at_most(16 * 1024);
775 let initial_height = 32; let atlas = TextureAtlas::new([texture_width, initial_height], options);
777
778 let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace> = Default::default();
779 let mut fonts_by_name: ahash::HashMap<String, FontFaceKey> = Default::default();
780 for (name, font_data) in &definitions.font_data {
781 let blob = blob_from_font_data(font_data);
782 let font_face = FontFace::new(
783 options,
784 name.clone(),
785 blob,
786 font_data.index,
787 font_data.tweak.clone(),
788 )
789 .unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"));
790 let key = FontFaceKey::new();
791 fonts_by_id.insert(key, font_face);
792 fonts_by_name.insert(name.clone(), key);
793 }
794
795 Self {
796 definitions,
797 atlas,
798 fonts_by_id,
799 fonts_by_name,
800 family_cache: Default::default(),
801 }
802 }
803
804 pub fn options(&self) -> &TextOptions {
805 self.atlas.options()
806 }
807
808 pub fn font(&mut self, family: &FontFamily) -> Font<'_> {
810 let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| {
811 let fonts = &self.definitions.families.get(family);
812 let fonts =
813 fonts.unwrap_or_else(|| panic!("FontFamily::{family:?} is not bound to any fonts"));
814
815 let fonts: Vec<FontFaceKey> = fonts
816 .iter()
817 .map(|font_name| {
818 *self
819 .fonts_by_name
820 .get(font_name)
821 .unwrap_or_else(|| panic!("No font data found for {font_name:?}"))
822 })
823 .collect();
824
825 CachedFamily::new(fonts, &mut self.fonts_by_id)
826 });
827 Font {
828 fonts_by_id: &mut self.fonts_by_id,
829 cached_family,
830 atlas: &mut self.atlas,
831 }
832 }
833}
834
835struct CachedGalley {
838 last_used: u32,
840
841 children: Option<Arc<[u64]>>,
845
846 galley: Arc<Galley>,
847}
848
849#[derive(Default)]
850struct GalleyCache {
851 generation: u32,
853 cache: nohash_hasher::IntMap<u64, CachedGalley>,
854}
855
856impl GalleyCache {
857 fn layout_internal(
858 &mut self,
859 fonts: &mut FontsImpl,
860 mut job: LayoutJob,
861 pixels_per_point: f32,
862 allow_split_paragraphs: bool,
863 ) -> (u64, Arc<Galley>) {
864 if job.wrap.max_width.is_finite() {
865 job.wrap.max_width = job.wrap.max_width.round();
887 }
888
889 let hash = crate::util::hash((&job, OrderedFloat(pixels_per_point))); let galley = match self.cache.entry(hash) {
892 std::collections::hash_map::Entry::Occupied(entry) => {
893 let cached = entry.into_mut();
895 cached.last_used = self.generation;
896
897 let galley = Arc::clone(&cached.galley);
898 if let Some(children) = &cached.children {
899 for child_hash in Arc::clone(children).iter() {
905 if let Some(cached_child) = self.cache.get_mut(child_hash) {
906 cached_child.last_used = self.generation;
907 }
908 }
909 }
910
911 galley
912 }
913 std::collections::hash_map::Entry::Vacant(entry) => {
914 let job = Arc::new(job);
915 if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) {
916 let (child_galleys, child_hashes) =
917 self.layout_each_paragraph_individually(fonts, &job, pixels_per_point);
918 debug_assert_eq!(
919 child_hashes.len(),
920 child_galleys.len(),
921 "Bug in `layout_each_paragraph_individually`"
922 );
923 let galley = Arc::new(Galley::concat(job, &child_galleys, pixels_per_point));
924
925 self.cache.insert(
926 hash,
927 CachedGalley {
928 last_used: self.generation,
929 children: Some(child_hashes.into()),
930 galley: Arc::clone(&galley),
931 },
932 );
933 galley
934 } else {
935 let galley = super::layout(fonts, pixels_per_point, job);
936 let galley = Arc::new(galley);
937 entry.insert(CachedGalley {
938 last_used: self.generation,
939 children: None,
940 galley: Arc::clone(&galley),
941 });
942 galley
943 }
944 }
945 };
946
947 (hash, galley)
948 }
949
950 fn layout(
951 &mut self,
952 fonts: &mut FontsImpl,
953 pixels_per_point: f32,
954 job: LayoutJob,
955 allow_split_paragraphs: bool,
956 ) -> Arc<Galley> {
957 self.layout_internal(fonts, job, pixels_per_point, allow_split_paragraphs)
958 .1
959 }
960
961 fn layout_each_paragraph_individually(
963 &mut self,
964 fonts: &mut FontsImpl,
965 job: &LayoutJob,
966 pixels_per_point: f32,
967 ) -> (Vec<Arc<Galley>>, Vec<u64>) {
968 profiling::function_scope!();
969
970 let mut current_section = 0;
971 let mut start = 0;
972 let mut max_rows_remaining = job.wrap.max_rows;
973 let mut child_galleys = Vec::new();
974 let mut child_hashes = Vec::new();
975
976 while start < job.text.len() {
977 let is_first_paragraph = start == 0;
978 let mut end = job.text[start..]
981 .find('\n')
982 .map_or(job.text.len(), |i| start + i);
983 if end == job.text.len() - 1 && job.text.ends_with('\n') {
984 end += 1; }
986
987 let mut paragraph_job = LayoutJob {
988 text: job.text[start..end].to_owned(),
989 wrap: crate::text::TextWrapping {
990 max_rows: max_rows_remaining,
991 ..job.wrap
992 },
993 sections: Vec::new(),
994 break_on_newline: job.break_on_newline,
995 halign: job.halign,
996 justify: job.justify,
997 first_row_min_height: if is_first_paragraph {
998 job.first_row_min_height
999 } else {
1000 0.0
1001 },
1002 round_output_to_gui: job.round_output_to_gui,
1003 };
1004
1005 for section in &job.sections[current_section..job.sections.len()] {
1007 let LayoutSection {
1008 leading_space,
1009 byte_range: section_range,
1010 format,
1011 } = section;
1012
1013 if section_range.end <= start {
1017 current_section += 1;
1019 } else if end < section_range.start {
1020 break; } else {
1022 debug_assert!(
1024 section_range.start <= section_range.end,
1025 "Bad byte_range: {section_range:?}"
1026 );
1027 let new_range = section_range.start.saturating_sub(start)
1028 ..(section_range.end.at_most(end)).saturating_sub(start);
1029 debug_assert!(
1030 new_range.start <= new_range.end,
1031 "Bad new section range: {new_range:?}"
1032 );
1033 paragraph_job.sections.push(LayoutSection {
1034 leading_space: if start <= section_range.start {
1035 *leading_space
1036 } else {
1037 0.0
1038 },
1039 byte_range: new_range,
1040 format: format.clone(),
1041 });
1042 }
1043 }
1044
1045 let (hash, galley) =
1047 self.layout_internal(fonts, paragraph_job, pixels_per_point, false);
1048 child_hashes.push(hash);
1049
1050 if max_rows_remaining != usize::MAX {
1052 max_rows_remaining -= galley.rows.len();
1053 }
1054
1055 let elided = galley.elided;
1056 child_galleys.push(galley);
1057 if elided {
1058 break;
1059 }
1060
1061 start = end + 1;
1062 }
1063
1064 (child_galleys, child_hashes)
1065 }
1066
1067 pub fn num_galleys_in_cache(&self) -> usize {
1068 self.cache.len()
1069 }
1070
1071 pub fn flush_cache(&mut self) {
1073 let current_generation = self.generation;
1074 self.cache.retain(|_key, cached| {
1075 cached.last_used == current_generation });
1077 self.generation = self.generation.wrapping_add(1);
1078 }
1079}
1080
1081fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool {
1085 job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n')
1089}
1090
1091#[cfg(feature = "default_fonts")]
1092#[cfg(test)]
1093mod tests {
1094 use core::f32;
1095
1096 use super::*;
1097 use crate::text::{TextWrapping, layout};
1098 use crate::{Stroke, text::TextFormat};
1099 use ecolor::Color32;
1100 use emath::Align;
1101
1102 fn jobs() -> Vec<LayoutJob> {
1103 vec![
1104 LayoutJob::simple(
1105 String::default(),
1106 FontId::new(14.0, FontFamily::Monospace),
1107 Color32::WHITE,
1108 f32::INFINITY,
1109 ),
1110 LayoutJob::simple(
1111 "ends with newlines\n\n".to_owned(),
1112 FontId::new(14.0, FontFamily::Monospace),
1113 Color32::WHITE,
1114 f32::INFINITY,
1115 ),
1116 LayoutJob::simple(
1117 "Simple test.".to_owned(),
1118 FontId::new(14.0, FontFamily::Monospace),
1119 Color32::WHITE,
1120 f32::INFINITY,
1121 ),
1122 {
1123 let mut job = LayoutJob::simple(
1124 "hi".to_owned(),
1125 FontId::default(),
1126 Color32::WHITE,
1127 f32::INFINITY,
1128 );
1129 job.append("\n", 0.0, TextFormat::default());
1130 job.append("\n", 0.0, TextFormat::default());
1131 job.append("world", 0.0, TextFormat::default());
1132 job.wrap.max_rows = 2;
1133 job
1134 },
1135 {
1136 let mut job = LayoutJob::simple(
1137 "Test text with a lot of words\n and a newline.".to_owned(),
1138 FontId::new(14.0, FontFamily::Monospace),
1139 Color32::WHITE,
1140 40.0,
1141 );
1142 job.first_row_min_height = 30.0;
1143 job
1144 },
1145 LayoutJob::simple(
1146 "This some text that may be long.\nDet kanske också finns lite ÅÄÖ här.".to_owned(),
1147 FontId::new(14.0, FontFamily::Proportional),
1148 Color32::WHITE,
1149 50.0,
1150 ),
1151 {
1152 let mut job = LayoutJob {
1153 first_row_min_height: 20.0,
1154 ..Default::default()
1155 };
1156 job.append(
1157 "1st paragraph has underline and strikethrough, and has some non-ASCII characters:\n ÅÄÖ.",
1158 0.0,
1159 TextFormat {
1160 font_id: FontId::new(15.0, FontFamily::Monospace),
1161 underline: Stroke::new(1.0, Color32::RED),
1162 strikethrough: Stroke::new(1.0, Color32::GREEN),
1163 ..Default::default()
1164 },
1165 );
1166 job.append(
1167 "2nd paragraph has some leading space.\n",
1168 16.0,
1169 TextFormat {
1170 font_id: FontId::new(14.0, FontFamily::Proportional),
1171 ..Default::default()
1172 },
1173 );
1174 job.append(
1175 "3rd paragraph is kind of boring, but has italics.\nAnd a newline",
1176 0.0,
1177 TextFormat {
1178 font_id: FontId::new(10.0, FontFamily::Proportional),
1179 italics: true,
1180 ..Default::default()
1181 },
1182 );
1183
1184 job
1185 },
1186 {
1187 let mut job = LayoutJob::default();
1189 job.append("\n", 0.0, TextFormat::default());
1190 job.append("", 0.0, TextFormat::default());
1191 job
1192 },
1193 ]
1194 }
1195
1196 #[expect(clippy::print_stdout)]
1197 #[test]
1198 fn test_split_paragraphs() {
1199 for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
1200 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1201
1202 for halign in [Align::Min, Align::Center, Align::Max] {
1203 for justify in [false, true] {
1204 for mut job in jobs() {
1205 job.halign = halign;
1206 job.justify = justify;
1207
1208 let whole = GalleyCache::default().layout(
1209 &mut fonts,
1210 pixels_per_point,
1211 job.clone(),
1212 false,
1213 );
1214
1215 let split = GalleyCache::default().layout(
1216 &mut fonts,
1217 pixels_per_point,
1218 job.clone(),
1219 true,
1220 );
1221
1222 for (i, row) in whole.rows.iter().enumerate() {
1223 println!(
1224 "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}",
1225 row.row.section_index_at_start,
1226 row.row.glyphs.first().map(|g| g.section_index)
1227 );
1228 }
1229 for (i, row) in split.rows.iter().enumerate() {
1230 println!(
1231 "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}",
1232 row.row.section_index_at_start,
1233 row.row.glyphs.first().map(|g| g.section_index)
1234 );
1235 }
1236
1237 similar_asserts::assert_eq!(
1240 format!("{:#.1?}", split),
1241 format!("{:#.1?}", whole),
1242 "pixels_per_point: {pixels_per_point:.2}, input text: '{}'",
1243 job.text
1244 );
1245 }
1246 }
1247 }
1248 }
1249 }
1250
1251 #[test]
1252 fn test_intrinsic_size() {
1253 let pixels_per_point = [1.0, 1.3, 2.0, 0.867];
1254 let max_widths = [40.0, 80.0, 133.0, 200.0];
1255 let rounded_output_to_gui = [false, true];
1256
1257 for pixels_per_point in pixels_per_point {
1258 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1259
1260 for &max_width in &max_widths {
1261 for round_output_to_gui in rounded_output_to_gui {
1262 for mut job in jobs() {
1263 job.wrap = TextWrapping::wrap_at_width(max_width);
1264
1265 job.round_output_to_gui = round_output_to_gui;
1266
1267 let galley_wrapped =
1268 layout(&mut fonts, pixels_per_point, job.clone().into());
1269
1270 job.wrap = TextWrapping::no_max_width();
1271
1272 let text = job.text.clone();
1273 let galley_unwrapped = layout(&mut fonts, pixels_per_point, job.into());
1274
1275 let intrinsic_size = galley_wrapped.intrinsic_size();
1276 let unwrapped_size = galley_unwrapped.size();
1277
1278 let difference = (intrinsic_size - unwrapped_size).length().abs();
1279 similar_asserts::assert_eq!(
1280 format!("{intrinsic_size:.4?}"),
1281 format!("{unwrapped_size:.4?}"),
1282 "Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?}
1283 Difference: {difference:.8?}
1284 wrapped rows: {}, unwrapped rows: {}
1285 pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}",
1286 galley_wrapped.rows.len(),
1287 galley_unwrapped.rows.len()
1288 );
1289 similar_asserts::assert_eq!(
1290 format!("{intrinsic_size:.4?}"),
1291 format!("{unwrapped_size:.4?}"),
1292 "Unwrapped galley intrinsic size should exactly match its size. \
1293 {:.8?} vs {:8?}",
1294 galley_unwrapped.intrinsic_size(),
1295 galley_unwrapped.size(),
1296 );
1297 }
1298 }
1299 }
1300 }
1301 }
1302
1303 #[test]
1304 fn test_fallback_glyph_width() {
1305 let mut fonts = Fonts::new(TextOptions::default(), FontDefinitions::empty());
1306 let mut view = fonts.with_pixels_per_point(1.0);
1307
1308 let width = view.glyph_width(&FontId::new(12.0, FontFamily::Proportional), ' ');
1309 assert_eq!(width, 0.0);
1310 }
1311}