1use std::{
2 collections::BTreeMap,
3 sync::{
4 Arc,
5 atomic::{AtomicU64, Ordering},
6 },
7};
8
9use crate::{
10 AlphaFromCoverage, TextureAtlas,
11 text::{
12 Galley, LayoutJob, LayoutSection,
13 font::{Font, FontImpl, GlyphInfo},
14 },
15};
16use emath::{NumExt as _, OrderedFloat};
17
18#[cfg(feature = "default_fonts")]
19use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT};
20
21#[derive(Clone, Debug, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
26pub struct FontId {
27 pub size: f32,
29
30 pub family: FontFamily,
32 }
34
35impl Default for FontId {
36 #[inline]
37 fn default() -> Self {
38 Self {
39 size: 14.0,
40 family: FontFamily::Proportional,
41 }
42 }
43}
44
45impl FontId {
46 #[inline]
47 pub const fn new(size: f32, family: FontFamily) -> Self {
48 Self { size, family }
49 }
50
51 #[inline]
52 pub const fn proportional(size: f32) -> Self {
53 Self::new(size, FontFamily::Proportional)
54 }
55
56 #[inline]
57 pub const fn monospace(size: f32) -> Self {
58 Self::new(size, FontFamily::Monospace)
59 }
60}
61
62impl std::hash::Hash for FontId {
63 #[inline(always)]
64 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
65 let Self { size, family } = self;
66 emath::OrderedFloat(*size).hash(state);
67 family.hash(state);
68 }
69}
70
71#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
78#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
79pub enum FontFamily {
80 #[default]
84 Proportional,
85
86 Monospace,
90
91 Name(Arc<str>),
100}
101
102impl std::fmt::Display for FontFamily {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 Self::Monospace => "Monospace".fmt(f),
106 Self::Proportional => "Proportional".fmt(f),
107 Self::Name(name) => (*name).fmt(f),
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
116#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
117pub struct FontData {
118 pub font: std::borrow::Cow<'static, [u8]>,
120
121 pub index: u32,
124
125 pub tweak: FontTweak,
127}
128
129impl FontData {
130 pub fn from_static(font: &'static [u8]) -> Self {
131 Self {
132 font: std::borrow::Cow::Borrowed(font),
133 index: 0,
134 tweak: Default::default(),
135 }
136 }
137
138 pub fn from_owned(font: Vec<u8>) -> Self {
139 Self {
140 font: std::borrow::Cow::Owned(font),
141 index: 0,
142 tweak: Default::default(),
143 }
144 }
145
146 pub fn tweak(self, tweak: FontTweak) -> Self {
147 Self { tweak, ..self }
148 }
149}
150
151impl AsRef<[u8]> for FontData {
152 fn as_ref(&self) -> &[u8] {
153 self.font.as_ref()
154 }
155}
156
157#[derive(Copy, Clone, Debug, PartialEq)]
161#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
162pub struct FontTweak {
163 pub scale: f32,
168
169 pub y_offset_factor: f32,
179
180 pub y_offset: f32,
187}
188
189impl Default for FontTweak {
190 fn default() -> Self {
191 Self {
192 scale: 1.0,
193 y_offset_factor: 0.0,
194 y_offset: 0.0,
195 }
196 }
197}
198
199fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc {
202 match &data.font {
203 std::borrow::Cow::Borrowed(bytes) => {
204 ab_glyph::FontRef::try_from_slice_and_index(bytes, data.index)
205 .map(ab_glyph::FontArc::from)
206 }
207 std::borrow::Cow::Owned(bytes) => {
208 ab_glyph::FontVec::try_from_vec_and_index(bytes.clone(), data.index)
209 .map(ab_glyph::FontArc::from)
210 }
211 }
212 .unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"))
213}
214
215#[derive(Clone, Debug, PartialEq)]
246#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
247#[cfg_attr(feature = "serde", serde(default))]
248pub struct FontDefinitions {
249 pub font_data: BTreeMap<String, Arc<FontData>>,
253
254 pub families: BTreeMap<FontFamily, Vec<String>>,
261}
262
263#[derive(Debug, Clone)]
264pub struct FontInsert {
265 pub name: String,
267
268 pub data: FontData,
270
271 pub families: Vec<InsertFontFamily>,
273}
274
275#[derive(Debug, Clone)]
276pub struct InsertFontFamily {
277 pub family: FontFamily,
279
280 pub priority: FontPriority,
282}
283
284#[derive(Debug, Clone)]
285pub enum FontPriority {
286 Highest,
290
291 Lowest,
295}
296
297impl FontInsert {
298 pub fn new(name: &str, data: FontData, families: Vec<InsertFontFamily>) -> Self {
299 Self {
300 name: name.to_owned(),
301 data,
302 families,
303 }
304 }
305}
306
307impl Default for FontDefinitions {
308 #[cfg(not(feature = "default_fonts"))]
311 fn default() -> Self {
312 Self::empty()
313 }
314
315 #[cfg(feature = "default_fonts")]
318 fn default() -> Self {
319 let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
320
321 let mut families = BTreeMap::new();
322
323 font_data.insert(
324 "Hack".to_owned(),
325 Arc::new(FontData::from_static(HACK_REGULAR)),
326 );
327
328 font_data.insert(
330 "NotoEmoji-Regular".to_owned(),
331 Arc::new(FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak {
332 scale: 0.81, ..Default::default()
334 })),
335 );
336
337 font_data.insert(
338 "Ubuntu-Light".to_owned(),
339 Arc::new(FontData::from_static(UBUNTU_LIGHT)),
340 );
341
342 font_data.insert(
344 "emoji-icon-font".to_owned(),
345 Arc::new(FontData::from_static(EMOJI_ICON).tweak(FontTweak {
346 scale: 0.90, ..Default::default()
348 })),
349 );
350
351 families.insert(
352 FontFamily::Monospace,
353 vec![
354 "Hack".to_owned(),
355 "Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned(),
357 "emoji-icon-font".to_owned(),
358 ],
359 );
360 families.insert(
361 FontFamily::Proportional,
362 vec![
363 "Ubuntu-Light".to_owned(),
364 "NotoEmoji-Regular".to_owned(),
365 "emoji-icon-font".to_owned(),
366 ],
367 );
368
369 Self {
370 font_data,
371 families,
372 }
373 }
374}
375
376impl FontDefinitions {
377 pub fn empty() -> Self {
379 let mut families = BTreeMap::new();
380 families.insert(FontFamily::Monospace, vec![]);
381 families.insert(FontFamily::Proportional, vec![]);
382
383 Self {
384 font_data: Default::default(),
385 families,
386 }
387 }
388
389 #[cfg(feature = "default_fonts")]
391 pub fn builtin_font_names() -> &'static [&'static str] {
392 &[
393 "Ubuntu-Light",
394 "NotoEmoji-Regular",
395 "emoji-icon-font",
396 "Hack",
397 ]
398 }
399
400 #[cfg(not(feature = "default_fonts"))]
402 pub fn builtin_font_names() -> &'static [&'static str] {
403 &[]
404 }
405}
406
407#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
409pub(crate) struct FontFaceKey(u64);
410
411impl FontFaceKey {
412 pub const INVALID: Self = Self(0);
413
414 fn new() -> Self {
415 static KEY_COUNTER: AtomicU64 = AtomicU64::new(1);
416 Self(crate::util::hash(
417 KEY_COUNTER.fetch_add(1, Ordering::Relaxed),
418 ))
419 }
420}
421
422impl nohash_hasher::IsEnabled for FontFaceKey {}
424
425#[derive(Debug)]
427pub(super) struct CachedFamily {
428 pub fonts: Vec<FontFaceKey>,
429
430 pub characters: Option<BTreeMap<char, Vec<String>>>,
432
433 pub replacement_glyph: (FontFaceKey, GlyphInfo),
434
435 pub glyph_info_cache: ahash::HashMap<char, (FontFaceKey, GlyphInfo)>,
436}
437
438impl CachedFamily {
439 fn new(
440 fonts: Vec<FontFaceKey>,
441 fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
442 ) -> Self {
443 if fonts.is_empty() {
444 return Self {
445 fonts,
446 characters: None,
447 replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
448 glyph_info_cache: Default::default(),
449 };
450 }
451
452 let mut slf = Self {
453 fonts,
454 characters: None,
455 replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
456 glyph_info_cache: Default::default(),
457 };
458
459 const PRIMARY_REPLACEMENT_CHAR: char = '◻'; const FALLBACK_REPLACEMENT_CHAR: char = '?'; let replacement_glyph = slf
463 .glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR, fonts_by_id)
464 .or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR, fonts_by_id))
465 .unwrap_or_else(|| {
466 log::warn!(
467 "Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
468 );
469 (FontFaceKey::INVALID, GlyphInfo::INVISIBLE)
470 });
471 slf.replacement_glyph = replacement_glyph;
472
473 slf
474 }
475
476 pub(crate) fn glyph_info_no_cache_or_fallback(
477 &mut self,
478 c: char,
479 fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
480 ) -> Option<(FontFaceKey, GlyphInfo)> {
481 for font_key in &self.fonts {
482 let font_impl = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID");
483 if let Some(glyph_info) = font_impl.glyph_info(c) {
484 self.glyph_info_cache.insert(c, (*font_key, glyph_info));
485 return Some((*font_key, glyph_info));
486 }
487 }
488 None
489 }
490}
491
492pub struct Fonts {
504 pub fonts: FontsImpl,
505 galley_cache: GalleyCache,
506}
507
508impl Fonts {
509 pub fn new(
514 max_texture_side: usize,
515 text_alpha_from_coverage: AlphaFromCoverage,
516 definitions: FontDefinitions,
517 ) -> Self {
518 Self {
519 fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
520 galley_cache: Default::default(),
521 }
522 }
523
524 pub fn begin_pass(
532 &mut self,
533 max_texture_side: usize,
534 text_alpha_from_coverage: AlphaFromCoverage,
535 ) {
536 let max_texture_side_changed = self.fonts.max_texture_side != max_texture_side;
537 let text_alpha_from_coverage_changed =
538 self.fonts.atlas.text_alpha_from_coverage != text_alpha_from_coverage;
539 let font_atlas_almost_full = self.fonts.atlas.fill_ratio() > 0.8;
540 let needs_recreate =
541 max_texture_side_changed || text_alpha_from_coverage_changed || font_atlas_almost_full;
542
543 if needs_recreate {
544 let definitions = self.fonts.definitions.clone();
545
546 *self = Self {
547 fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
548 galley_cache: Default::default(),
549 };
550 }
551
552 self.galley_cache.flush_cache();
553 }
554
555 pub fn font_image_delta(&mut self) -> Option<crate::ImageDelta> {
557 self.fonts.atlas.take_delta()
558 }
559
560 #[inline]
561 pub fn max_texture_side(&self) -> usize {
562 self.fonts.max_texture_side
563 }
564
565 #[inline]
566 pub fn definitions(&self) -> &FontDefinitions {
567 &self.fonts.definitions
568 }
569
570 pub fn texture_atlas(&self) -> &TextureAtlas {
573 &self.fonts.atlas
574 }
575
576 #[inline]
578 pub fn image(&self) -> crate::ColorImage {
579 self.fonts.atlas.image().clone()
580 }
581
582 pub fn font_image_size(&self) -> [usize; 2] {
585 self.fonts.atlas.size()
586 }
587
588 pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
590 self.fonts.font(&font_id.family).has_glyph(c)
591 }
592
593 pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
595 self.fonts.font(&font_id.family).has_glyphs(s)
596 }
597
598 pub fn num_galleys_in_cache(&self) -> usize {
599 self.galley_cache.num_galleys_in_cache()
600 }
601
602 pub fn font_atlas_fill_ratio(&self) -> f32 {
607 self.fonts.atlas.fill_ratio()
608 }
609
610 pub fn with_pixels_per_point(&mut self, pixels_per_point: f32) -> FontsView<'_> {
612 FontsView {
613 fonts: &mut self.fonts,
614 galley_cache: &mut self.galley_cache,
615 pixels_per_point,
616 }
617 }
618}
619
620pub struct FontsView<'a> {
624 pub fonts: &'a mut FontsImpl,
625 galley_cache: &'a mut GalleyCache,
626 pixels_per_point: f32,
627}
628
629impl FontsView<'_> {
630 #[inline]
631 pub fn max_texture_side(&self) -> usize {
632 self.fonts.max_texture_side
633 }
634
635 #[inline]
636 pub fn definitions(&self) -> &FontDefinitions {
637 &self.fonts.definitions
638 }
639
640 #[inline]
642 pub fn image(&self) -> crate::ColorImage {
643 self.fonts.atlas.image().clone()
644 }
645
646 pub fn font_image_size(&self) -> [usize; 2] {
649 self.fonts.atlas.size()
650 }
651
652 pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
656 self.fonts
657 .font(&font_id.family)
658 .glyph_width(c, font_id.size)
659 }
660
661 pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
663 self.fonts.font(&font_id.family).has_glyph(c)
664 }
665
666 pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
668 self.fonts.font(&font_id.family).has_glyphs(s)
669 }
670
671 pub fn row_height(&mut self, font_id: &FontId) -> f32 {
675 self.fonts
676 .font(&font_id.family)
677 .scaled_metrics(self.pixels_per_point, font_id.size)
678 .row_height
679 }
680
681 pub fn families(&self) -> Vec<FontFamily> {
683 self.fonts.definitions.families.keys().cloned().collect()
684 }
685
686 #[inline]
694 pub fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
695 let allow_split_paragraphs = true; self.galley_cache.layout(
697 self.fonts,
698 self.pixels_per_point,
699 job,
700 allow_split_paragraphs,
701 )
702 }
703
704 pub fn num_galleys_in_cache(&self) -> usize {
705 self.galley_cache.num_galleys_in_cache()
706 }
707
708 pub fn font_atlas_fill_ratio(&self) -> f32 {
713 self.fonts.atlas.fill_ratio()
714 }
715
716 pub fn layout(
720 &mut self,
721 text: String,
722 font_id: FontId,
723 color: crate::Color32,
724 wrap_width: f32,
725 ) -> Arc<Galley> {
726 let job = LayoutJob::simple(text, font_id, color, wrap_width);
727 self.layout_job(job)
728 }
729
730 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 pub fn layout_delayed_color(
747 &mut self,
748 text: String,
749 font_id: FontId,
750 wrap_width: f32,
751 ) -> Arc<Galley> {
752 self.layout(text, font_id, crate::Color32::PLACEHOLDER, wrap_width)
753 }
754}
755
756pub struct FontsImpl {
762 max_texture_side: usize,
763 definitions: FontDefinitions,
764 atlas: TextureAtlas,
765 fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontImpl>,
766 fonts_by_name: ahash::HashMap<String, FontFaceKey>,
767 family_cache: ahash::HashMap<FontFamily, CachedFamily>,
768}
769
770impl FontsImpl {
771 pub fn new(
774 max_texture_side: usize,
775 text_alpha_from_coverage: AlphaFromCoverage,
776 definitions: FontDefinitions,
777 ) -> Self {
778 let texture_width = max_texture_side.at_most(16 * 1024);
779 let initial_height = 32; let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage);
781
782 let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontImpl> = Default::default();
783 let mut font_impls: ahash::HashMap<String, FontFaceKey> = Default::default();
784 for (name, font_data) in &definitions.font_data {
785 let tweak = font_data.tweak;
786 let ab_glyph = ab_glyph_font_from_font_data(name, font_data);
787 let font_impl = FontImpl::new(name.clone(), ab_glyph, tweak);
788 let key = FontFaceKey::new();
789 fonts_by_id.insert(key, font_impl);
790 font_impls.insert(name.clone(), key);
791 }
792
793 Self {
794 max_texture_side,
795 definitions,
796 atlas,
797 fonts_by_id,
798 fonts_by_name: font_impls,
799 family_cache: Default::default(),
800 }
801 }
802
803 pub fn font(&mut self, family: &FontFamily) -> Font<'_> {
805 let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| {
806 let fonts = &self.definitions.families.get(family);
807 let fonts =
808 fonts.unwrap_or_else(|| panic!("FontFamily::{family:?} is not bound to any fonts"));
809
810 let fonts: Vec<FontFaceKey> = fonts
811 .iter()
812 .map(|font_name| {
813 *self
814 .fonts_by_name
815 .get(font_name)
816 .unwrap_or_else(|| panic!("No font data found for {font_name:?}"))
817 })
818 .collect();
819
820 CachedFamily::new(fonts, &mut self.fonts_by_id)
821 });
822 Font {
823 fonts_by_id: &mut self.fonts_by_id,
824 cached_family,
825 atlas: &mut self.atlas,
826 }
827 }
828}
829
830struct CachedGalley {
833 last_used: u32,
835
836 children: Option<Arc<[u64]>>,
840
841 galley: Arc<Galley>,
842}
843
844#[derive(Default)]
845struct GalleyCache {
846 generation: u32,
848 cache: nohash_hasher::IntMap<u64, CachedGalley>,
849}
850
851impl GalleyCache {
852 fn layout_internal(
853 &mut self,
854 fonts: &mut FontsImpl,
855 mut job: LayoutJob,
856 pixels_per_point: f32,
857 allow_split_paragraphs: bool,
858 ) -> (u64, Arc<Galley>) {
859 if job.wrap.max_width.is_finite() {
860 job.wrap.max_width = job.wrap.max_width.round();
882 }
883
884 let hash = crate::util::hash((&job, OrderedFloat(pixels_per_point))); let galley = match self.cache.entry(hash) {
887 std::collections::hash_map::Entry::Occupied(entry) => {
888 let cached = entry.into_mut();
890 cached.last_used = self.generation;
891
892 let galley = cached.galley.clone();
893 if let Some(children) = &cached.children {
894 for child_hash in children.clone().iter() {
900 if let Some(cached_child) = self.cache.get_mut(child_hash) {
901 cached_child.last_used = self.generation;
902 }
903 }
904 }
905
906 galley
907 }
908 std::collections::hash_map::Entry::Vacant(entry) => {
909 let job = Arc::new(job);
910 if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) {
911 let (child_galleys, child_hashes) =
912 self.layout_each_paragraph_individually(fonts, &job, pixels_per_point);
913 debug_assert_eq!(
914 child_hashes.len(),
915 child_galleys.len(),
916 "Bug in `layout_each_paragraph_individually`"
917 );
918 let galley = Arc::new(Galley::concat(job, &child_galleys, pixels_per_point));
919
920 self.cache.insert(
921 hash,
922 CachedGalley {
923 last_used: self.generation,
924 children: Some(child_hashes.into()),
925 galley: galley.clone(),
926 },
927 );
928 galley
929 } else {
930 let galley = super::layout(fonts, pixels_per_point, job);
931 let galley = Arc::new(galley);
932 entry.insert(CachedGalley {
933 last_used: self.generation,
934 children: None,
935 galley: galley.clone(),
936 });
937 galley
938 }
939 }
940 };
941
942 (hash, galley)
943 }
944
945 fn layout(
946 &mut self,
947 fonts: &mut FontsImpl,
948 pixels_per_point: f32,
949 job: LayoutJob,
950 allow_split_paragraphs: bool,
951 ) -> Arc<Galley> {
952 self.layout_internal(fonts, job, pixels_per_point, allow_split_paragraphs)
953 .1
954 }
955
956 fn layout_each_paragraph_individually(
958 &mut self,
959 fonts: &mut FontsImpl,
960 job: &LayoutJob,
961 pixels_per_point: f32,
962 ) -> (Vec<Arc<Galley>>, Vec<u64>) {
963 profiling::function_scope!();
964
965 let mut current_section = 0;
966 let mut start = 0;
967 let mut max_rows_remaining = job.wrap.max_rows;
968 let mut child_galleys = Vec::new();
969 let mut child_hashes = Vec::new();
970
971 while start < job.text.len() {
972 let is_first_paragraph = start == 0;
973 let mut end = job.text[start..]
976 .find('\n')
977 .map_or(job.text.len(), |i| start + i);
978 if end == job.text.len() - 1 && job.text.ends_with('\n') {
979 end += 1; }
981
982 let mut paragraph_job = LayoutJob {
983 text: job.text[start..end].to_owned(),
984 wrap: crate::text::TextWrapping {
985 max_rows: max_rows_remaining,
986 ..job.wrap
987 },
988 sections: Vec::new(),
989 break_on_newline: job.break_on_newline,
990 halign: job.halign,
991 justify: job.justify,
992 first_row_min_height: if is_first_paragraph {
993 job.first_row_min_height
994 } else {
995 0.0
996 },
997 round_output_to_gui: job.round_output_to_gui,
998 };
999
1000 for section in &job.sections[current_section..job.sections.len()] {
1002 let LayoutSection {
1003 leading_space,
1004 byte_range: section_range,
1005 format,
1006 } = section;
1007
1008 if section_range.end <= start {
1012 current_section += 1;
1014 } else if end < section_range.start {
1015 break; } else {
1017 debug_assert!(
1019 section_range.start <= section_range.end,
1020 "Bad byte_range: {section_range:?}"
1021 );
1022 let new_range = section_range.start.saturating_sub(start)
1023 ..(section_range.end.at_most(end)).saturating_sub(start);
1024 debug_assert!(
1025 new_range.start <= new_range.end,
1026 "Bad new section range: {new_range:?}"
1027 );
1028 paragraph_job.sections.push(LayoutSection {
1029 leading_space: if start <= section_range.start {
1030 *leading_space
1031 } else {
1032 0.0
1033 },
1034 byte_range: new_range,
1035 format: format.clone(),
1036 });
1037 }
1038 }
1039
1040 let (hash, galley) =
1042 self.layout_internal(fonts, paragraph_job, pixels_per_point, false);
1043 child_hashes.push(hash);
1044
1045 if max_rows_remaining != usize::MAX {
1047 max_rows_remaining -= galley.rows.len();
1048 }
1049
1050 let elided = galley.elided;
1051 child_galleys.push(galley);
1052 if elided {
1053 break;
1054 }
1055
1056 start = end + 1;
1057 }
1058
1059 (child_galleys, child_hashes)
1060 }
1061
1062 pub fn num_galleys_in_cache(&self) -> usize {
1063 self.cache.len()
1064 }
1065
1066 pub fn flush_cache(&mut self) {
1068 let current_generation = self.generation;
1069 self.cache.retain(|_key, cached| {
1070 cached.last_used == current_generation });
1072 self.generation = self.generation.wrapping_add(1);
1073 }
1074}
1075
1076fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool {
1080 job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n')
1084}
1085
1086#[cfg(feature = "default_fonts")]
1087#[cfg(test)]
1088mod tests {
1089 use core::f32;
1090
1091 use super::*;
1092 use crate::text::{TextWrapping, layout};
1093 use crate::{Stroke, text::TextFormat};
1094 use ecolor::Color32;
1095 use emath::Align;
1096
1097 fn jobs() -> Vec<LayoutJob> {
1098 vec![
1099 LayoutJob::simple(
1100 String::default(),
1101 FontId::new(14.0, FontFamily::Monospace),
1102 Color32::WHITE,
1103 f32::INFINITY,
1104 ),
1105 LayoutJob::simple(
1106 "ends with newlines\n\n".to_owned(),
1107 FontId::new(14.0, FontFamily::Monospace),
1108 Color32::WHITE,
1109 f32::INFINITY,
1110 ),
1111 LayoutJob::simple(
1112 "Simple test.".to_owned(),
1113 FontId::new(14.0, FontFamily::Monospace),
1114 Color32::WHITE,
1115 f32::INFINITY,
1116 ),
1117 {
1118 let mut job = LayoutJob::simple(
1119 "hi".to_owned(),
1120 FontId::default(),
1121 Color32::WHITE,
1122 f32::INFINITY,
1123 );
1124 job.append("\n", 0.0, TextFormat::default());
1125 job.append("\n", 0.0, TextFormat::default());
1126 job.append("world", 0.0, TextFormat::default());
1127 job.wrap.max_rows = 2;
1128 job
1129 },
1130 {
1131 let mut job = LayoutJob::simple(
1132 "Test text with a lot of words\n and a newline.".to_owned(),
1133 FontId::new(14.0, FontFamily::Monospace),
1134 Color32::WHITE,
1135 40.0,
1136 );
1137 job.first_row_min_height = 30.0;
1138 job
1139 },
1140 LayoutJob::simple(
1141 "This some text that may be long.\nDet kanske också finns lite ÅÄÖ här.".to_owned(),
1142 FontId::new(14.0, FontFamily::Proportional),
1143 Color32::WHITE,
1144 50.0,
1145 ),
1146 {
1147 let mut job = LayoutJob {
1148 first_row_min_height: 20.0,
1149 ..Default::default()
1150 };
1151 job.append(
1152 "1st paragraph has underline and strikethrough, and has some non-ASCII characters:\n ÅÄÖ.",
1153 0.0,
1154 TextFormat {
1155 font_id: FontId::new(15.0, FontFamily::Monospace),
1156 underline: Stroke::new(1.0, Color32::RED),
1157 strikethrough: Stroke::new(1.0, Color32::GREEN),
1158 ..Default::default()
1159 },
1160 );
1161 job.append(
1162 "2nd paragraph has some leading space.\n",
1163 16.0,
1164 TextFormat {
1165 font_id: FontId::new(14.0, FontFamily::Proportional),
1166 ..Default::default()
1167 },
1168 );
1169 job.append(
1170 "3rd paragraph is kind of boring, but has italics.\nAnd a newline",
1171 0.0,
1172 TextFormat {
1173 font_id: FontId::new(10.0, FontFamily::Proportional),
1174 italics: true,
1175 ..Default::default()
1176 },
1177 );
1178
1179 job
1180 },
1181 {
1182 let mut job = LayoutJob::default();
1184 job.append("\n", 0.0, TextFormat::default());
1185 job.append("", 0.0, TextFormat::default());
1186 job
1187 },
1188 ]
1189 }
1190
1191 #[expect(clippy::print_stdout)]
1192 #[test]
1193 fn test_split_paragraphs() {
1194 for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
1195 let max_texture_side = 4096;
1196 let mut fonts = FontsImpl::new(
1197 max_texture_side,
1198 AlphaFromCoverage::default(),
1199 FontDefinitions::default(),
1200 );
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(
1259 1024,
1260 AlphaFromCoverage::default(),
1261 FontDefinitions::default(),
1262 );
1263
1264 for &max_width in &max_widths {
1265 for round_output_to_gui in rounded_output_to_gui {
1266 for mut job in jobs() {
1267 job.wrap = TextWrapping::wrap_at_width(max_width);
1268
1269 job.round_output_to_gui = round_output_to_gui;
1270
1271 let galley_wrapped =
1272 layout(&mut fonts, pixels_per_point, job.clone().into());
1273
1274 job.wrap = TextWrapping::no_max_width();
1275
1276 let text = job.text.clone();
1277 let galley_unwrapped = layout(&mut fonts, pixels_per_point, job.into());
1278
1279 let intrinsic_size = galley_wrapped.intrinsic_size();
1280 let unwrapped_size = galley_unwrapped.size();
1281
1282 let difference = (intrinsic_size - unwrapped_size).length().abs();
1283 similar_asserts::assert_eq!(
1284 format!("{intrinsic_size:.4?}"),
1285 format!("{unwrapped_size:.4?}"),
1286 "Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?}
1287 Difference: {difference:.8?}
1288 wrapped rows: {}, unwrapped rows: {}
1289 pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}",
1290 galley_wrapped.rows.len(),
1291 galley_unwrapped.rows.len()
1292 );
1293 similar_asserts::assert_eq!(
1294 format!("{intrinsic_size:.4?}"),
1295 format!("{unwrapped_size:.4?}"),
1296 "Unwrapped galley intrinsic size should exactly match its size. \
1297 {:.8?} vs {:8?}",
1298 galley_unwrapped.intrinsic_size(),
1299 galley_unwrapped.size(),
1300 );
1301 }
1302 }
1303 }
1304 }
1305 }
1306
1307 #[test]
1308 fn test_fallback_glyph_width() {
1309 let mut fonts = Fonts::new(1024, AlphaFromCoverage::default(), FontDefinitions::empty());
1310 let mut view = fonts.with_pixels_per_point(1.0);
1311
1312 let width = view.glyph_width(&FontId::new(12.0, FontFamily::Proportional), ' ');
1313 assert_eq!(width, 0.0);
1314 }
1315}