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