1use std::{collections::BTreeMap, sync::Arc};
2
3use crate::{
4 AlphaFromCoverage, TextureAtlas,
5 mutex::{Mutex, MutexGuard},
6 text::{
7 Galley, LayoutJob, LayoutSection,
8 font::{Font, FontImpl},
9 },
10};
11use emath::{NumExt as _, OrderedFloat};
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 std::hash::Hash for FontId {
58 #[inline(always)]
59 fn hash<H: std::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 std::fmt::Display for FontFamily {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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: std::borrow::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: std::borrow::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: std::borrow::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
146impl AsRef<[u8]> for FontData {
147 fn as_ref(&self) -> &[u8] {
148 self.font.as_ref()
149 }
150}
151
152#[derive(Copy, Clone, Debug, PartialEq)]
156#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
157pub struct FontTweak {
158 pub scale: f32,
163
164 pub y_offset_factor: f32,
174
175 pub y_offset: f32,
182
183 pub baseline_offset_factor: f32,
189}
190
191impl Default for FontTweak {
192 fn default() -> Self {
193 Self {
194 scale: 1.0,
195 y_offset_factor: 0.0,
196 y_offset: 0.0,
197 baseline_offset_factor: 0.0,
198 }
199 }
200}
201
202fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc {
205 match &data.font {
206 std::borrow::Cow::Borrowed(bytes) => {
207 ab_glyph::FontRef::try_from_slice_and_index(bytes, data.index)
208 .map(ab_glyph::FontArc::from)
209 }
210 std::borrow::Cow::Owned(bytes) => {
211 ab_glyph::FontVec::try_from_vec_and_index(bytes.clone(), data.index)
212 .map(ab_glyph::FontArc::from)
213 }
214 }
215 .unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"))
216}
217
218#[derive(Clone, Debug, PartialEq)]
249#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
250#[cfg_attr(feature = "serde", serde(default))]
251pub struct FontDefinitions {
252 pub font_data: BTreeMap<String, Arc<FontData>>,
256
257 pub families: BTreeMap<FontFamily, Vec<String>>,
264}
265
266#[derive(Debug, Clone)]
267pub struct FontInsert {
268 pub name: String,
270
271 pub data: FontData,
273
274 pub families: Vec<InsertFontFamily>,
276}
277
278#[derive(Debug, Clone)]
279pub struct InsertFontFamily {
280 pub family: FontFamily,
282
283 pub priority: FontPriority,
285}
286
287#[derive(Debug, Clone)]
288pub enum FontPriority {
289 Highest,
293
294 Lowest,
298}
299
300impl FontInsert {
301 pub fn new(name: &str, data: FontData, families: Vec<InsertFontFamily>) -> Self {
302 Self {
303 name: name.to_owned(),
304 data,
305 families,
306 }
307 }
308}
309
310impl Default for FontDefinitions {
311 #[cfg(not(feature = "default_fonts"))]
314 fn default() -> Self {
315 Self::empty()
316 }
317
318 #[cfg(feature = "default_fonts")]
321 fn default() -> Self {
322 let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
323
324 let mut families = BTreeMap::new();
325
326 font_data.insert(
327 "Hack".to_owned(),
328 Arc::new(FontData::from_static(HACK_REGULAR)),
329 );
330
331 font_data.insert(
333 "NotoEmoji-Regular".to_owned(),
334 Arc::new(FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak {
335 scale: 0.81, ..Default::default()
337 })),
338 );
339
340 font_data.insert(
341 "Ubuntu-Light".to_owned(),
342 Arc::new(FontData::from_static(UBUNTU_LIGHT)),
343 );
344
345 font_data.insert(
347 "emoji-icon-font".to_owned(),
348 Arc::new(FontData::from_static(EMOJI_ICON).tweak(FontTweak {
349 scale: 0.90, ..Default::default()
351 })),
352 );
353
354 families.insert(
355 FontFamily::Monospace,
356 vec![
357 "Hack".to_owned(),
358 "Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned(),
360 "emoji-icon-font".to_owned(),
361 ],
362 );
363 families.insert(
364 FontFamily::Proportional,
365 vec![
366 "Ubuntu-Light".to_owned(),
367 "NotoEmoji-Regular".to_owned(),
368 "emoji-icon-font".to_owned(),
369 ],
370 );
371
372 Self {
373 font_data,
374 families,
375 }
376 }
377}
378
379impl FontDefinitions {
380 pub fn empty() -> Self {
382 let mut families = BTreeMap::new();
383 families.insert(FontFamily::Monospace, vec![]);
384 families.insert(FontFamily::Proportional, vec![]);
385
386 Self {
387 font_data: Default::default(),
388 families,
389 }
390 }
391
392 #[cfg(feature = "default_fonts")]
394 pub fn builtin_font_names() -> &'static [&'static str] {
395 &[
396 "Ubuntu-Light",
397 "NotoEmoji-Regular",
398 "emoji-icon-font",
399 "Hack",
400 ]
401 }
402
403 #[cfg(not(feature = "default_fonts"))]
405 pub fn builtin_font_names() -> &'static [&'static str] {
406 &[]
407 }
408}
409
410#[derive(Clone)]
422pub struct Fonts(Arc<Mutex<FontsAndCache>>);
423
424impl Fonts {
425 pub fn new(
431 pixels_per_point: f32,
432 max_texture_side: usize,
433 text_alpha_from_coverage: AlphaFromCoverage,
434 definitions: FontDefinitions,
435 ) -> Self {
436 let fonts_and_cache = FontsAndCache {
437 fonts: FontsImpl::new(
438 pixels_per_point,
439 max_texture_side,
440 text_alpha_from_coverage,
441 definitions,
442 ),
443 galley_cache: Default::default(),
444 };
445 Self(Arc::new(Mutex::new(fonts_and_cache)))
446 }
447
448 pub fn begin_pass(
456 &self,
457 pixels_per_point: f32,
458 max_texture_side: usize,
459 text_alpha_from_coverage: AlphaFromCoverage,
460 ) {
461 let mut fonts_and_cache = self.0.lock();
462
463 let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point;
464 let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side;
465 let text_alpha_from_coverage_changed =
466 fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage;
467 let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8;
468 let needs_recreate = pixels_per_point_changed
469 || max_texture_side_changed
470 || text_alpha_from_coverage_changed
471 || font_atlas_almost_full;
472
473 if needs_recreate {
474 let definitions = fonts_and_cache.fonts.definitions.clone();
475
476 *fonts_and_cache = FontsAndCache {
477 fonts: FontsImpl::new(
478 pixels_per_point,
479 max_texture_side,
480 text_alpha_from_coverage,
481 definitions,
482 ),
483 galley_cache: Default::default(),
484 };
485 }
486
487 fonts_and_cache.galley_cache.flush_cache();
488 }
489
490 pub fn font_image_delta(&self) -> Option<crate::ImageDelta> {
492 self.lock().fonts.atlas.lock().take_delta()
493 }
494
495 #[doc(hidden)]
497 #[inline]
498 pub fn lock(&self) -> MutexGuard<'_, FontsAndCache> {
499 self.0.lock()
500 }
501
502 #[inline]
503 pub fn pixels_per_point(&self) -> f32 {
504 self.lock().fonts.pixels_per_point
505 }
506
507 #[inline]
508 pub fn max_texture_side(&self) -> usize {
509 self.lock().fonts.max_texture_side
510 }
511
512 pub fn texture_atlas(&self) -> Arc<Mutex<TextureAtlas>> {
515 self.lock().fonts.atlas.clone()
516 }
517
518 #[inline]
520 pub fn image(&self) -> crate::ColorImage {
521 self.lock().fonts.atlas.lock().image().clone()
522 }
523
524 pub fn font_image_size(&self) -> [usize; 2] {
527 self.lock().fonts.atlas.lock().size()
528 }
529
530 #[inline]
532 pub fn glyph_width(&self, font_id: &FontId, c: char) -> f32 {
533 self.lock().fonts.glyph_width(font_id, c)
534 }
535
536 #[inline]
538 pub fn has_glyph(&self, font_id: &FontId, c: char) -> bool {
539 self.lock().fonts.has_glyph(font_id, c)
540 }
541
542 pub fn has_glyphs(&self, font_id: &FontId, s: &str) -> bool {
544 self.lock().fonts.has_glyphs(font_id, s)
545 }
546
547 #[inline]
551 pub fn row_height(&self, font_id: &FontId) -> f32 {
552 self.lock().fonts.row_height(font_id)
553 }
554
555 pub fn families(&self) -> Vec<FontFamily> {
557 self.lock()
558 .fonts
559 .definitions
560 .families
561 .keys()
562 .cloned()
563 .collect()
564 }
565
566 #[inline]
574 pub fn layout_job(&self, job: LayoutJob) -> Arc<Galley> {
575 self.lock().layout_job(job)
576 }
577
578 pub fn num_galleys_in_cache(&self) -> usize {
579 self.lock().galley_cache.num_galleys_in_cache()
580 }
581
582 pub fn font_atlas_fill_ratio(&self) -> f32 {
587 self.lock().fonts.atlas.lock().fill_ratio()
588 }
589
590 pub fn layout(
594 &self,
595 text: String,
596 font_id: FontId,
597 color: crate::Color32,
598 wrap_width: f32,
599 ) -> Arc<Galley> {
600 let job = LayoutJob::simple(text, font_id, color, wrap_width);
601 self.layout_job(job)
602 }
603
604 pub fn layout_no_wrap(
608 &self,
609 text: String,
610 font_id: FontId,
611 color: crate::Color32,
612 ) -> Arc<Galley> {
613 let job = LayoutJob::simple(text, font_id, color, f32::INFINITY);
614 self.layout_job(job)
615 }
616
617 pub fn layout_delayed_color(
621 &self,
622 text: String,
623 font_id: FontId,
624 wrap_width: f32,
625 ) -> Arc<Galley> {
626 self.layout(text, font_id, crate::Color32::PLACEHOLDER, wrap_width)
627 }
628}
629
630pub struct FontsAndCache {
633 pub fonts: FontsImpl,
634 galley_cache: GalleyCache,
635}
636
637impl FontsAndCache {
638 fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
639 let allow_split_paragraphs = true; self.galley_cache
641 .layout(&mut self.fonts, job, allow_split_paragraphs)
642 }
643}
644
645pub struct FontsImpl {
651 pixels_per_point: f32,
652 max_texture_side: usize,
653 definitions: FontDefinitions,
654 atlas: Arc<Mutex<TextureAtlas>>,
655 font_impl_cache: FontImplCache,
656 sized_family: ahash::HashMap<(OrderedFloat<f32>, FontFamily), Font>,
657}
658
659impl FontsImpl {
660 pub fn new(
663 pixels_per_point: f32,
664 max_texture_side: usize,
665 text_alpha_from_coverage: AlphaFromCoverage,
666 definitions: FontDefinitions,
667 ) -> Self {
668 assert!(
669 0.0 < pixels_per_point && pixels_per_point < 100.0,
670 "pixels_per_point out of range: {pixels_per_point}"
671 );
672
673 let texture_width = max_texture_side.at_most(16 * 1024);
674 let initial_height = 32; let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage);
676
677 let atlas = Arc::new(Mutex::new(atlas));
678
679 let font_impl_cache =
680 FontImplCache::new(atlas.clone(), pixels_per_point, &definitions.font_data);
681
682 Self {
683 pixels_per_point,
684 max_texture_side,
685 definitions,
686 atlas,
687 font_impl_cache,
688 sized_family: Default::default(),
689 }
690 }
691
692 #[inline(always)]
693 pub fn pixels_per_point(&self) -> f32 {
694 self.pixels_per_point
695 }
696
697 #[inline]
698 pub fn definitions(&self) -> &FontDefinitions {
699 &self.definitions
700 }
701
702 pub fn font(&mut self, font_id: &FontId) -> &mut Font {
704 let FontId { size, family } = font_id;
705 let mut size = *size;
706 size = size.at_least(0.1).at_most(2048.0);
707
708 self.sized_family
709 .entry((OrderedFloat(size), family.clone()))
710 .or_insert_with(|| {
711 let fonts = &self.definitions.families.get(family);
712 let fonts = fonts
713 .unwrap_or_else(|| panic!("FontFamily::{family:?} is not bound to any fonts"));
714
715 let fonts: Vec<Arc<FontImpl>> = fonts
716 .iter()
717 .map(|font_name| self.font_impl_cache.font_impl(size, font_name))
718 .collect();
719
720 Font::new(fonts)
721 })
722 }
723
724 fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
726 self.font(font_id).glyph_width(c)
727 }
728
729 pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
731 self.font(font_id).has_glyph(c)
732 }
733
734 pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
736 self.font(font_id).has_glyphs(s)
737 }
738
739 fn row_height(&mut self, font_id: &FontId) -> f32 {
743 self.font(font_id).row_height()
744 }
745}
746
747struct CachedGalley {
750 last_used: u32,
752
753 children: Option<Arc<[u64]>>,
757
758 galley: Arc<Galley>,
759}
760
761#[derive(Default)]
762struct GalleyCache {
763 generation: u32,
765 cache: nohash_hasher::IntMap<u64, CachedGalley>,
766}
767
768impl GalleyCache {
769 fn layout_internal(
770 &mut self,
771 fonts: &mut FontsImpl,
772 mut job: LayoutJob,
773 allow_split_paragraphs: bool,
774 ) -> (u64, Arc<Galley>) {
775 if job.wrap.max_width.is_finite() {
776 job.wrap.max_width = job.wrap.max_width.round();
798 }
799
800 let hash = crate::util::hash(&job); let galley = match self.cache.entry(hash) {
803 std::collections::hash_map::Entry::Occupied(entry) => {
804 let cached = entry.into_mut();
806 cached.last_used = self.generation;
807
808 let galley = cached.galley.clone();
809 if let Some(children) = &cached.children {
810 for child_hash in children.clone().iter() {
816 if let Some(cached_child) = self.cache.get_mut(child_hash) {
817 cached_child.last_used = self.generation;
818 }
819 }
820 }
821
822 galley
823 }
824 std::collections::hash_map::Entry::Vacant(entry) => {
825 let job = Arc::new(job);
826 if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) {
827 let (child_galleys, child_hashes) =
828 self.layout_each_paragraph_individually(fonts, &job);
829 debug_assert_eq!(
830 child_hashes.len(),
831 child_galleys.len(),
832 "Bug in `layout_each_paragraph_individuallly`"
833 );
834 let galley =
835 Arc::new(Galley::concat(job, &child_galleys, fonts.pixels_per_point));
836
837 self.cache.insert(
838 hash,
839 CachedGalley {
840 last_used: self.generation,
841 children: Some(child_hashes.into()),
842 galley: galley.clone(),
843 },
844 );
845 galley
846 } else {
847 let galley = super::layout(fonts, job);
848 let galley = Arc::new(galley);
849 entry.insert(CachedGalley {
850 last_used: self.generation,
851 children: None,
852 galley: galley.clone(),
853 });
854 galley
855 }
856 }
857 };
858
859 (hash, galley)
860 }
861
862 fn layout(
863 &mut self,
864 fonts: &mut FontsImpl,
865 job: LayoutJob,
866 allow_split_paragraphs: bool,
867 ) -> Arc<Galley> {
868 self.layout_internal(fonts, job, allow_split_paragraphs).1
869 }
870
871 fn layout_each_paragraph_individually(
873 &mut self,
874 fonts: &mut FontsImpl,
875 job: &LayoutJob,
876 ) -> (Vec<Arc<Galley>>, Vec<u64>) {
877 profiling::function_scope!();
878
879 let mut current_section = 0;
880 let mut start = 0;
881 let mut max_rows_remaining = job.wrap.max_rows;
882 let mut child_galleys = Vec::new();
883 let mut child_hashes = Vec::new();
884
885 while start < job.text.len() {
886 let is_first_paragraph = start == 0;
887 let mut end = job.text[start..]
890 .find('\n')
891 .map_or(job.text.len(), |i| start + i);
892 if end == job.text.len() - 1 && job.text.ends_with('\n') {
893 end += 1; }
895
896 let mut paragraph_job = LayoutJob {
897 text: job.text[start..end].to_owned(),
898 wrap: crate::text::TextWrapping {
899 max_rows: max_rows_remaining,
900 ..job.wrap
901 },
902 sections: Vec::new(),
903 break_on_newline: job.break_on_newline,
904 halign: job.halign,
905 justify: job.justify,
906 first_row_min_height: if is_first_paragraph {
907 job.first_row_min_height
908 } else {
909 0.0
910 },
911 round_output_to_gui: job.round_output_to_gui,
912 };
913
914 for section in &job.sections[current_section..job.sections.len()] {
916 let LayoutSection {
917 leading_space,
918 byte_range: section_range,
919 format,
920 } = section;
921
922 if section_range.end <= start {
926 current_section += 1;
928 } else if end < section_range.start {
929 break; } else {
931 debug_assert!(
933 section_range.start < section_range.end,
934 "Bad byte_range: {section_range:?}"
935 );
936 let new_range = section_range.start.saturating_sub(start)
937 ..(section_range.end.at_most(end)).saturating_sub(start);
938 debug_assert!(
939 new_range.start <= new_range.end,
940 "Bad new section range: {new_range:?}"
941 );
942 paragraph_job.sections.push(LayoutSection {
943 leading_space: if start <= section_range.start {
944 *leading_space
945 } else {
946 0.0
947 },
948 byte_range: new_range,
949 format: format.clone(),
950 });
951 }
952 }
953
954 let (hash, galley) = self.layout_internal(fonts, paragraph_job, false);
956 child_hashes.push(hash);
957
958 if max_rows_remaining != usize::MAX {
960 max_rows_remaining -= galley.rows.len();
961 }
962
963 let elided = galley.elided;
964 child_galleys.push(galley);
965 if elided {
966 break;
967 }
968
969 start = end + 1;
970 }
971
972 (child_galleys, child_hashes)
973 }
974
975 pub fn num_galleys_in_cache(&self) -> usize {
976 self.cache.len()
977 }
978
979 pub fn flush_cache(&mut self) {
981 let current_generation = self.generation;
982 self.cache.retain(|_key, cached| {
983 cached.last_used == current_generation });
985 self.generation = self.generation.wrapping_add(1);
986 }
987}
988
989fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool {
993 job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n')
997}
998
999struct FontImplCache {
1002 atlas: Arc<Mutex<TextureAtlas>>,
1003 pixels_per_point: f32,
1004 ab_glyph_fonts: BTreeMap<String, (FontTweak, ab_glyph::FontArc)>,
1005
1006 cache: ahash::HashMap<(u32, String), Arc<FontImpl>>,
1008}
1009
1010impl FontImplCache {
1011 pub fn new(
1012 atlas: Arc<Mutex<TextureAtlas>>,
1013 pixels_per_point: f32,
1014 font_data: &BTreeMap<String, Arc<FontData>>,
1015 ) -> Self {
1016 let ab_glyph_fonts = font_data
1017 .iter()
1018 .map(|(name, font_data)| {
1019 let tweak = font_data.tweak;
1020 let ab_glyph = ab_glyph_font_from_font_data(name, font_data);
1021 (name.clone(), (tweak, ab_glyph))
1022 })
1023 .collect();
1024
1025 Self {
1026 atlas,
1027 pixels_per_point,
1028 ab_glyph_fonts,
1029 cache: Default::default(),
1030 }
1031 }
1032
1033 pub fn font_impl(&mut self, scale_in_points: f32, font_name: &str) -> Arc<FontImpl> {
1034 use ab_glyph::Font as _;
1035
1036 let (tweak, ab_glyph_font) = self
1037 .ab_glyph_fonts
1038 .get(font_name)
1039 .unwrap_or_else(|| panic!("No font data found for {font_name:?}"))
1040 .clone();
1041
1042 let scale_in_pixels = self.pixels_per_point * scale_in_points;
1043
1044 let units_per_em = ab_glyph_font.units_per_em().unwrap_or_else(|| {
1046 panic!("The font unit size of {font_name:?} exceeds the expected range (16..=16384)")
1047 });
1048 let font_scaling = ab_glyph_font.height_unscaled() / units_per_em;
1049 let scale_in_pixels = scale_in_pixels * font_scaling;
1050
1051 self.cache
1052 .entry((
1053 (scale_in_pixels * tweak.scale).round() as u32,
1054 font_name.to_owned(),
1055 ))
1056 .or_insert_with(|| {
1057 Arc::new(FontImpl::new(
1058 self.atlas.clone(),
1059 self.pixels_per_point,
1060 font_name.to_owned(),
1061 ab_glyph_font,
1062 scale_in_pixels,
1063 tweak,
1064 ))
1065 })
1066 .clone()
1067 }
1068}
1069
1070#[cfg(feature = "default_fonts")]
1071#[cfg(test)]
1072mod tests {
1073 use core::f32;
1074
1075 use super::*;
1076 use crate::text::{TextWrapping, layout};
1077 use crate::{Stroke, text::TextFormat};
1078 use ecolor::Color32;
1079 use emath::Align;
1080
1081 fn jobs() -> Vec<LayoutJob> {
1082 vec![
1083 LayoutJob::simple(
1084 String::default(),
1085 FontId::new(14.0, FontFamily::Monospace),
1086 Color32::WHITE,
1087 f32::INFINITY,
1088 ),
1089 LayoutJob::simple(
1090 "ends with newlines\n\n".to_owned(),
1091 FontId::new(14.0, FontFamily::Monospace),
1092 Color32::WHITE,
1093 f32::INFINITY,
1094 ),
1095 LayoutJob::simple(
1096 "Simple test.".to_owned(),
1097 FontId::new(14.0, FontFamily::Monospace),
1098 Color32::WHITE,
1099 f32::INFINITY,
1100 ),
1101 {
1102 let mut job = LayoutJob::simple(
1103 "hi".to_owned(),
1104 FontId::default(),
1105 Color32::WHITE,
1106 f32::INFINITY,
1107 );
1108 job.append("\n", 0.0, TextFormat::default());
1109 job.append("\n", 0.0, TextFormat::default());
1110 job.append("world", 0.0, TextFormat::default());
1111 job.wrap.max_rows = 2;
1112 job
1113 },
1114 {
1115 let mut job = LayoutJob::simple(
1116 "Test text with a lot of words\n and a newline.".to_owned(),
1117 FontId::new(14.0, FontFamily::Monospace),
1118 Color32::WHITE,
1119 40.0,
1120 );
1121 job.first_row_min_height = 30.0;
1122 job
1123 },
1124 LayoutJob::simple(
1125 "This some text that may be long.\nDet kanske också finns lite ÅÄÖ här.".to_owned(),
1126 FontId::new(14.0, FontFamily::Proportional),
1127 Color32::WHITE,
1128 50.0,
1129 ),
1130 {
1131 let mut job = LayoutJob {
1132 first_row_min_height: 20.0,
1133 ..Default::default()
1134 };
1135 job.append(
1136 "1st paragraph has underline and strikethrough, and has some non-ASCII characters:\n ÅÄÖ.",
1137 0.0,
1138 TextFormat {
1139 font_id: FontId::new(15.0, FontFamily::Monospace),
1140 underline: Stroke::new(1.0, Color32::RED),
1141 strikethrough: Stroke::new(1.0, Color32::GREEN),
1142 ..Default::default()
1143 },
1144 );
1145 job.append(
1146 "2nd paragraph has some leading space.\n",
1147 16.0,
1148 TextFormat {
1149 font_id: FontId::new(14.0, FontFamily::Proportional),
1150 ..Default::default()
1151 },
1152 );
1153 job.append(
1154 "3rd paragraph is kind of boring, but has italics.\nAnd a newline",
1155 0.0,
1156 TextFormat {
1157 font_id: FontId::new(10.0, FontFamily::Proportional),
1158 italics: true,
1159 ..Default::default()
1160 },
1161 );
1162
1163 job
1164 },
1165 ]
1166 }
1167
1168 #[test]
1169 fn test_split_paragraphs() {
1170 for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
1171 let max_texture_side = 4096;
1172 let mut fonts = FontsImpl::new(
1173 pixels_per_point,
1174 max_texture_side,
1175 AlphaFromCoverage::default(),
1176 FontDefinitions::default(),
1177 );
1178
1179 for halign in [Align::Min, Align::Center, Align::Max] {
1180 for justify in [false, true] {
1181 for mut job in jobs() {
1182 job.halign = halign;
1183 job.justify = justify;
1184
1185 let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false);
1186
1187 let split = GalleyCache::default().layout(&mut fonts, job.clone(), true);
1188
1189 for (i, row) in whole.rows.iter().enumerate() {
1190 println!(
1191 "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}",
1192 row.row.section_index_at_start,
1193 row.row.glyphs.first().map(|g| g.section_index)
1194 );
1195 }
1196 for (i, row) in split.rows.iter().enumerate() {
1197 println!(
1198 "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}",
1199 row.row.section_index_at_start,
1200 row.row.glyphs.first().map(|g| g.section_index)
1201 );
1202 }
1203
1204 similar_asserts::assert_eq!(
1207 format!("{:#.1?}", split),
1208 format!("{:#.1?}", whole),
1209 "pixels_per_point: {pixels_per_point:.2}, input text: '{}'",
1210 job.text
1211 );
1212 }
1213 }
1214 }
1215 }
1216 }
1217
1218 #[test]
1219 fn test_intrinsic_size() {
1220 let pixels_per_point = [1.0, 1.3, 2.0, 0.867];
1221 let max_widths = [40.0, 80.0, 133.0, 200.0];
1222 let rounded_output_to_gui = [false, true];
1223
1224 for pixels_per_point in pixels_per_point {
1225 let mut fonts = FontsImpl::new(
1226 pixels_per_point,
1227 1024,
1228 AlphaFromCoverage::default(),
1229 FontDefinitions::default(),
1230 );
1231
1232 for &max_width in &max_widths {
1233 for round_output_to_gui in rounded_output_to_gui {
1234 for mut job in jobs() {
1235 job.wrap = TextWrapping::wrap_at_width(max_width);
1236
1237 job.round_output_to_gui = round_output_to_gui;
1238
1239 let galley_wrapped = layout(&mut fonts, job.clone().into());
1240
1241 job.wrap = TextWrapping::no_max_width();
1242
1243 let text = job.text.clone();
1244 let galley_unwrapped = layout(&mut fonts, job.into());
1245
1246 let intrinsic_size = galley_wrapped.intrinsic_size();
1247 let unwrapped_size = galley_unwrapped.size();
1248
1249 let difference = (intrinsic_size - unwrapped_size).length().abs();
1250 similar_asserts::assert_eq!(
1251 format!("{intrinsic_size:.4?}"),
1252 format!("{unwrapped_size:.4?}"),
1253 "Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?}
1254 Difference: {difference:.8?}
1255 wrapped rows: {}, unwrapped rows: {}
1256 pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}",
1257 galley_wrapped.rows.len(),
1258 galley_unwrapped.rows.len()
1259 );
1260 similar_asserts::assert_eq!(
1261 format!("{intrinsic_size:.4?}"),
1262 format!("{unwrapped_size:.4?}"),
1263 "Unwrapped galley intrinsic size should exactly match its size. \
1264 {:.8?} vs {:8?}",
1265 galley_unwrapped.intrinsic_size(),
1266 galley_unwrapped.size(),
1267 );
1268 }
1269 }
1270 }
1271 }
1272 }
1273}