1use emath::GuiRounding as _;
2use epaint::text::{IntoTag, TextFormat, VariationCoords};
3use std::fmt::Formatter;
4use std::{borrow::Cow, sync::Arc};
5
6use crate::{
7 Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
8 text::{LayoutJob, TextWrapping},
9};
10
11#[derive(Clone, Debug, PartialEq)]
27pub struct RichText {
28 text: String,
29 size: Option<f32>,
30 extra_letter_spacing: f32,
31 line_height: Option<f32>,
32 family: Option<FontFamily>,
33 text_style: Option<TextStyle>,
34 background_color: Color32,
35 expand_bg: f32,
36 text_color: Option<Color32>,
37 coords: VariationCoords,
38 code: bool,
39 strong: bool,
40 weak: bool,
41 strikethrough: bool,
42 underline: bool,
43 italics: bool,
44 raised: bool,
45}
46
47impl Default for RichText {
48 fn default() -> Self {
49 Self {
50 text: Default::default(),
51 size: Default::default(),
52 extra_letter_spacing: Default::default(),
53 line_height: Default::default(),
54 family: Default::default(),
55 text_style: Default::default(),
56 background_color: Default::default(),
57 expand_bg: 1.0,
58 text_color: Default::default(),
59 coords: Default::default(),
60 code: Default::default(),
61 strong: Default::default(),
62 weak: Default::default(),
63 strikethrough: Default::default(),
64 underline: Default::default(),
65 italics: Default::default(),
66 raised: Default::default(),
67 }
68 }
69}
70
71impl From<&str> for RichText {
72 #[inline]
73 fn from(text: &str) -> Self {
74 Self::new(text)
75 }
76}
77
78impl From<&String> for RichText {
79 #[inline]
80 fn from(text: &String) -> Self {
81 Self::new(text)
82 }
83}
84
85impl From<&mut String> for RichText {
86 #[inline]
87 fn from(text: &mut String) -> Self {
88 Self::new(text.clone())
89 }
90}
91
92impl From<String> for RichText {
93 #[inline]
94 fn from(text: String) -> Self {
95 Self::new(text)
96 }
97}
98
99impl From<&Box<str>> for RichText {
100 #[inline]
101 fn from(text: &Box<str>) -> Self {
102 Self::new(text.clone())
103 }
104}
105
106impl From<&mut Box<str>> for RichText {
107 #[inline]
108 fn from(text: &mut Box<str>) -> Self {
109 Self::new(text.clone())
110 }
111}
112
113impl From<Box<str>> for RichText {
114 #[inline]
115 fn from(text: Box<str>) -> Self {
116 Self::new(text)
117 }
118}
119
120impl From<Cow<'_, str>> for RichText {
121 #[inline]
122 fn from(text: Cow<'_, str>) -> Self {
123 Self::new(text)
124 }
125}
126
127impl RichText {
128 #[inline]
129 pub fn new(text: impl Into<String>) -> Self {
130 Self {
131 text: text.into(),
132 ..Default::default()
133 }
134 }
135
136 #[inline]
137 pub fn is_empty(&self) -> bool {
138 self.text.is_empty()
139 }
140
141 #[inline]
142 pub fn text(&self) -> &str {
143 &self.text
144 }
145
146 #[inline]
149 pub fn size(mut self, size: f32) -> Self {
150 self.size = Some(size);
151 self
152 }
153
154 #[inline]
161 pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
162 self.extra_letter_spacing = extra_letter_spacing;
163 self
164 }
165
166 #[inline]
175 pub fn line_height(mut self, line_height: Option<f32>) -> Self {
176 self.line_height = line_height;
177 self
178 }
179
180 #[inline]
186 pub fn family(mut self, family: FontFamily) -> Self {
187 self.family = Some(family);
188 self
189 }
190
191 #[inline]
194 pub fn font(mut self, font_id: crate::FontId) -> Self {
195 let crate::FontId { size, family } = font_id;
196 self.size = Some(size);
197 self.family = Some(family);
198 self
199 }
200
201 #[inline]
203 pub fn variation(mut self, tag: impl IntoTag, coord: f32) -> Self {
204 self.coords.push(tag, coord);
205 self
206 }
207
208 #[inline]
210 pub fn variations<T: IntoTag>(
211 mut self,
212 variations: impl IntoIterator<Item = (T, f32)>,
213 ) -> Self {
214 self.coords = VariationCoords::new(variations);
215 self
216 }
217
218 #[inline]
220 pub fn text_style(mut self, text_style: TextStyle) -> Self {
221 self.text_style = Some(text_style);
222 self
223 }
224
225 #[inline]
227 pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
228 self.text_style.get_or_insert(text_style);
229 self
230 }
231
232 #[inline]
234 pub fn heading(self) -> Self {
235 self.text_style(TextStyle::Heading)
236 }
237
238 #[inline]
240 pub fn monospace(self) -> Self {
241 self.text_style(TextStyle::Monospace)
242 }
243
244 #[inline]
246 pub fn code(mut self) -> Self {
247 self.code = true;
248 self.text_style(TextStyle::Monospace)
249 }
250
251 #[inline]
253 pub fn strong(mut self) -> Self {
254 self.strong = true;
255 self
256 }
257
258 #[inline]
260 pub fn weak(mut self) -> Self {
261 self.weak = true;
262 self
263 }
264
265 #[inline]
269 pub fn underline(mut self) -> Self {
270 self.underline = true;
271 self
272 }
273
274 #[inline]
278 pub fn strikethrough(mut self) -> Self {
279 self.strikethrough = true;
280 self
281 }
282
283 #[inline]
285 pub fn italics(mut self) -> Self {
286 self.italics = true;
287 self
288 }
289
290 #[inline]
292 pub fn small(self) -> Self {
293 self.text_style(TextStyle::Small)
294 }
295
296 #[inline]
298 pub fn small_raised(self) -> Self {
299 self.text_style(TextStyle::Small).raised()
300 }
301
302 #[inline]
304 pub fn raised(mut self) -> Self {
305 self.raised = true;
306 self
307 }
308
309 #[inline]
311 pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
312 self.background_color = background_color.into();
313 self
314 }
315
316 #[inline]
321 pub fn color(mut self, color: impl Into<Color32>) -> Self {
322 self.text_color = Some(color.into());
323 self
324 }
325
326 pub fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
330 let mut font_id = self.text_style.as_ref().map_or_else(
331 || FontSelection::Default.resolve(style),
332 |text_style| text_style.resolve(style),
333 );
334
335 if let Some(size) = self.size {
336 font_id.size = size;
337 }
338 if let Some(family) = &self.family {
339 font_id.family = family.clone();
340 }
341 fonts.row_height(&font_id)
342 }
343
344 pub fn append_to(
374 self,
375 layout_job: &mut LayoutJob,
376 style: &Style,
377 fallback_font: FontSelection,
378 default_valign: Align,
379 ) {
380 let (text, format) = self.into_text_and_format(style, fallback_font, default_valign);
381
382 layout_job.append(&text, 0.0, format);
383 }
384
385 fn into_layout_job(
386 self,
387 style: &Style,
388 fallback_font: FontSelection,
389 default_valign: Align,
390 ) -> LayoutJob {
391 let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign);
392 LayoutJob::single_section(text, text_format)
393 }
394
395 fn into_text_and_format(
396 self,
397 style: &Style,
398 fallback_font: FontSelection,
399 default_valign: Align,
400 ) -> (String, crate::text::TextFormat) {
401 let text_color = self.get_text_color(&style.visuals);
402
403 let Self {
404 text,
405 size,
406 extra_letter_spacing,
407 line_height,
408 family,
409 text_style,
410 background_color,
411 expand_bg,
412 text_color: _, coords,
414 code,
415 strong: _, weak: _, strikethrough,
418 underline,
419 italics,
420 raised,
421 } = self;
422
423 let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
424 let text_color = text_color.unwrap_or(crate::Color32::PLACEHOLDER);
425
426 let font_id = {
427 let mut font_id = style.override_font_id.clone().unwrap_or_else(|| {
428 (text_style.as_ref().or(style.override_text_style.as_ref()))
429 .map(|text_style| text_style.resolve(style))
430 .unwrap_or_else(|| fallback_font.resolve(style))
431 });
432 if let Some(size) = size {
433 font_id.size = size;
434 }
435 if let Some(family) = family {
436 font_id.family = family;
437 }
438 font_id
439 };
440
441 let background_color = if code {
442 style.visuals.code_bg_color
443 } else {
444 background_color
445 };
446
447 let underline = if underline {
448 crate::Stroke::new(1.0, line_color)
449 } else {
450 crate::Stroke::NONE
451 };
452 let strikethrough = if strikethrough {
453 crate::Stroke::new(1.0, line_color)
454 } else {
455 crate::Stroke::NONE
456 };
457
458 let valign = if raised {
459 crate::Align::TOP
460 } else {
461 default_valign
462 };
463
464 (
465 text,
466 crate::text::TextFormat {
467 font_id,
468 extra_letter_spacing,
469 line_height,
470 color: text_color,
471 background: background_color,
472 coords,
473 italics,
474 underline,
475 strikethrough,
476 valign,
477 expand_bg,
478 },
479 )
480 }
481
482 fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
483 if let Some(text_color) = self.text_color {
484 Some(text_color)
485 } else if self.strong {
486 Some(visuals.strong_text_color())
487 } else if self.weak {
488 Some(visuals.weak_text_color())
489 } else {
490 visuals.override_text_color
491 }
492 }
493}
494
495#[derive(Clone)]
510pub enum WidgetText {
511 Text(String),
516
517 RichText(Arc<RichText>),
521
522 LayoutJob(Arc<LayoutJob>),
535
536 Galley(Arc<Galley>),
541}
542
543impl std::fmt::Debug for WidgetText {
544 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
545 let text = self.text();
546 match self {
547 Self::Text(_) => write!(f, "Text({text:?})"),
548 Self::RichText(_) => write!(f, "RichText({text:?})"),
549 Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"),
550 Self::Galley(_) => write!(f, "Galley({text:?})"),
551 }
552 }
553}
554
555impl Default for WidgetText {
556 fn default() -> Self {
557 Self::Text(String::new())
558 }
559}
560
561impl WidgetText {
562 #[inline]
563 pub fn is_empty(&self) -> bool {
564 match self {
565 Self::Text(text) => text.is_empty(),
566 Self::RichText(text) => text.is_empty(),
567 Self::LayoutJob(job) => job.is_empty(),
568 Self::Galley(galley) => galley.is_empty(),
569 }
570 }
571
572 #[inline]
573 pub fn text(&self) -> &str {
574 match self {
575 Self::Text(text) => text,
576 Self::RichText(text) => text.text(),
577 Self::LayoutJob(job) => &job.text,
578 Self::Galley(galley) => galley.text(),
579 }
580 }
581
582 #[must_use]
588 fn map_rich_text<F>(self, f: F) -> Self
589 where
590 F: FnOnce(RichText) -> RichText,
591 {
592 match self {
593 Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))),
594 Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))),
595 other => other,
596 }
597 }
598
599 #[inline]
603 pub fn text_style(self, text_style: TextStyle) -> Self {
604 self.map_rich_text(|text| text.text_style(text_style))
605 }
606
607 #[inline]
611 pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
612 self.map_rich_text(|text| text.fallback_text_style(text_style))
613 }
614
615 #[inline]
619 pub fn color(self, color: impl Into<Color32>) -> Self {
620 self.map_rich_text(|text| text.color(color))
621 }
622
623 #[inline]
625 pub fn heading(self) -> Self {
626 self.map_rich_text(|text| text.heading())
627 }
628
629 #[inline]
631 pub fn monospace(self) -> Self {
632 self.map_rich_text(|text| text.monospace())
633 }
634
635 #[inline]
637 pub fn code(self) -> Self {
638 self.map_rich_text(|text| text.code())
639 }
640
641 #[inline]
643 pub fn strong(self) -> Self {
644 self.map_rich_text(|text| text.strong())
645 }
646
647 #[inline]
649 pub fn weak(self) -> Self {
650 self.map_rich_text(|text| text.weak())
651 }
652
653 #[inline]
655 pub fn underline(self) -> Self {
656 self.map_rich_text(|text| text.underline())
657 }
658
659 #[inline]
661 pub fn strikethrough(self) -> Self {
662 self.map_rich_text(|text| text.strikethrough())
663 }
664
665 #[inline]
667 pub fn italics(self) -> Self {
668 self.map_rich_text(|text| text.italics())
669 }
670
671 #[inline]
673 pub fn small(self) -> Self {
674 self.map_rich_text(|text| text.small())
675 }
676
677 #[inline]
679 pub fn small_raised(self) -> Self {
680 self.map_rich_text(|text| text.small_raised())
681 }
682
683 #[inline]
685 pub fn raised(self) -> Self {
686 self.map_rich_text(|text| text.raised())
687 }
688
689 #[inline]
691 pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
692 self.map_rich_text(|text| text.background_color(background_color))
693 }
694
695 pub(crate) fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
697 match self {
698 Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
699 Self::RichText(text) => text.font_height(fonts, style),
700 Self::LayoutJob(job) => job.font_height(fonts),
701 Self::Galley(galley) => {
702 if let Some(placed_row) = galley.rows.first() {
703 placed_row.height().round_ui()
704 } else {
705 galley.size().y.round_ui()
706 }
707 }
708 }
709 }
710
711 pub fn into_layout_job(
712 self,
713 style: &Style,
714 fallback_font: FontSelection,
715 default_valign: Align,
716 ) -> Arc<LayoutJob> {
717 match self {
718 Self::Text(text) => Arc::new(LayoutJob::simple_format(
719 text,
720 TextFormat {
721 font_id: FontSelection::Default.resolve(style),
722 color: crate::Color32::PLACEHOLDER,
723 valign: default_valign,
724 ..Default::default()
725 },
726 )),
727 Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job(
728 style,
729 fallback_font,
730 default_valign,
731 )),
732 Self::LayoutJob(job) => job,
733 Self::Galley(galley) => Arc::clone(&galley.job),
734 }
735 }
736
737 pub fn into_galley(
741 self,
742 ui: &Ui,
743 wrap_mode: Option<TextWrapMode>,
744 available_width: f32,
745 fallback_font: impl Into<FontSelection>,
746 ) -> Arc<Galley> {
747 let valign = ui.text_valign();
748 let style = ui.style();
749
750 let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
751 let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width);
752
753 self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign)
754 }
755
756 pub fn into_galley_impl(
757 self,
758 ctx: &crate::Context,
759 style: &Style,
760 text_wrapping: TextWrapping,
761 fallback_font: FontSelection,
762 default_valign: Align,
763 ) -> Arc<Galley> {
764 match self {
765 Self::Text(text) => {
766 let color = style
767 .visuals
768 .override_text_color
769 .unwrap_or(crate::Color32::PLACEHOLDER);
770 let mut layout_job = LayoutJob::simple_format(
771 text,
772 TextFormat {
773 font_id: FontSelection::default()
775 .resolve_with_fallback(style, fallback_font),
776 color,
777 valign: default_valign,
778 ..Default::default()
779 },
780 );
781 layout_job.wrap = text_wrapping;
782 ctx.fonts_mut(|f| f.layout_job(layout_job))
783 }
784 Self::RichText(text) => {
785 let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
786 style,
787 fallback_font,
788 default_valign,
789 );
790 layout_job.wrap = text_wrapping;
791 ctx.fonts_mut(|f| f.layout_job(layout_job))
792 }
793 Self::LayoutJob(job) => {
794 let mut job = Arc::unwrap_or_clone(job);
795 job.wrap = text_wrapping;
796 ctx.fonts_mut(|f| f.layout_job(job))
797 }
798 Self::Galley(galley) => galley,
799 }
800 }
801}
802
803impl From<&str> for WidgetText {
804 #[inline]
805 fn from(text: &str) -> Self {
806 Self::Text(text.to_owned())
807 }
808}
809
810impl From<&String> for WidgetText {
811 #[inline]
812 fn from(text: &String) -> Self {
813 Self::Text(text.clone())
814 }
815}
816
817impl From<String> for WidgetText {
818 #[inline]
819 fn from(text: String) -> Self {
820 Self::Text(text)
821 }
822}
823
824impl From<&Box<str>> for WidgetText {
825 #[inline]
826 fn from(text: &Box<str>) -> Self {
827 Self::Text(text.to_string())
828 }
829}
830
831impl From<Box<str>> for WidgetText {
832 #[inline]
833 fn from(text: Box<str>) -> Self {
834 Self::Text(text.into())
835 }
836}
837
838impl From<Cow<'_, str>> for WidgetText {
839 #[inline]
840 fn from(text: Cow<'_, str>) -> Self {
841 Self::Text(text.into_owned())
842 }
843}
844
845impl From<RichText> for WidgetText {
846 #[inline]
847 fn from(rich_text: RichText) -> Self {
848 Self::RichText(Arc::new(rich_text))
849 }
850}
851
852impl From<Arc<RichText>> for WidgetText {
853 #[inline]
854 fn from(rich_text: Arc<RichText>) -> Self {
855 Self::RichText(rich_text)
856 }
857}
858
859impl From<LayoutJob> for WidgetText {
860 #[inline]
861 fn from(layout_job: LayoutJob) -> Self {
862 Self::LayoutJob(Arc::new(layout_job))
863 }
864}
865
866impl From<Arc<LayoutJob>> for WidgetText {
867 #[inline]
868 fn from(layout_job: Arc<LayoutJob>) -> Self {
869 Self::LayoutJob(layout_job)
870 }
871}
872
873impl From<Arc<Galley>> for WidgetText {
874 #[inline]
875 fn from(galley: Arc<Galley>) -> Self {
876 Self::Galley(galley)
877 }
878}
879
880#[cfg(test)]
881mod tests {
882 use crate::WidgetText;
883
884 #[test]
885 fn ensure_small_widget_text() {
886 assert_eq!(size_of::<WidgetText>(), size_of::<String>());
887 }
888}