1use emath::GuiRounding as _;
2use epaint::text::TextFormat;
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 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 code: Default::default(),
59 strong: Default::default(),
60 weak: Default::default(),
61 strikethrough: Default::default(),
62 underline: Default::default(),
63 italics: Default::default(),
64 raised: Default::default(),
65 }
66 }
67}
68
69impl From<&str> for RichText {
70 #[inline]
71 fn from(text: &str) -> Self {
72 Self::new(text)
73 }
74}
75
76impl From<&String> for RichText {
77 #[inline]
78 fn from(text: &String) -> Self {
79 Self::new(text)
80 }
81}
82
83impl From<&mut String> for RichText {
84 #[inline]
85 fn from(text: &mut String) -> Self {
86 Self::new(text.clone())
87 }
88}
89
90impl From<String> for RichText {
91 #[inline]
92 fn from(text: String) -> Self {
93 Self::new(text)
94 }
95}
96
97impl From<&Box<str>> for RichText {
98 #[inline]
99 fn from(text: &Box<str>) -> Self {
100 Self::new(text.clone())
101 }
102}
103
104impl From<&mut Box<str>> for RichText {
105 #[inline]
106 fn from(text: &mut Box<str>) -> Self {
107 Self::new(text.clone())
108 }
109}
110
111impl From<Box<str>> for RichText {
112 #[inline]
113 fn from(text: Box<str>) -> Self {
114 Self::new(text)
115 }
116}
117
118impl From<Cow<'_, str>> for RichText {
119 #[inline]
120 fn from(text: Cow<'_, str>) -> Self {
121 Self::new(text)
122 }
123}
124
125impl RichText {
126 #[inline]
127 pub fn new(text: impl Into<String>) -> Self {
128 Self {
129 text: text.into(),
130 ..Default::default()
131 }
132 }
133
134 #[inline]
135 pub fn is_empty(&self) -> bool {
136 self.text.is_empty()
137 }
138
139 #[inline]
140 pub fn text(&self) -> &str {
141 &self.text
142 }
143
144 #[inline]
147 pub fn size(mut self, size: f32) -> Self {
148 self.size = Some(size);
149 self
150 }
151
152 #[inline]
159 pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
160 self.extra_letter_spacing = extra_letter_spacing;
161 self
162 }
163
164 #[inline]
173 pub fn line_height(mut self, line_height: Option<f32>) -> Self {
174 self.line_height = line_height;
175 self
176 }
177
178 #[inline]
184 pub fn family(mut self, family: FontFamily) -> Self {
185 self.family = Some(family);
186 self
187 }
188
189 #[inline]
192 pub fn font(mut self, font_id: crate::FontId) -> Self {
193 let crate::FontId { size, family } = font_id;
194 self.size = Some(size);
195 self.family = Some(family);
196 self
197 }
198
199 #[inline]
201 pub fn text_style(mut self, text_style: TextStyle) -> Self {
202 self.text_style = Some(text_style);
203 self
204 }
205
206 #[inline]
208 pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
209 self.text_style.get_or_insert(text_style);
210 self
211 }
212
213 #[inline]
215 pub fn heading(self) -> Self {
216 self.text_style(TextStyle::Heading)
217 }
218
219 #[inline]
221 pub fn monospace(self) -> Self {
222 self.text_style(TextStyle::Monospace)
223 }
224
225 #[inline]
227 pub fn code(mut self) -> Self {
228 self.code = true;
229 self.text_style(TextStyle::Monospace)
230 }
231
232 #[inline]
234 pub fn strong(mut self) -> Self {
235 self.strong = true;
236 self
237 }
238
239 #[inline]
241 pub fn weak(mut self) -> Self {
242 self.weak = true;
243 self
244 }
245
246 #[inline]
250 pub fn underline(mut self) -> Self {
251 self.underline = true;
252 self
253 }
254
255 #[inline]
259 pub fn strikethrough(mut self) -> Self {
260 self.strikethrough = true;
261 self
262 }
263
264 #[inline]
266 pub fn italics(mut self) -> Self {
267 self.italics = true;
268 self
269 }
270
271 #[inline]
273 pub fn small(self) -> Self {
274 self.text_style(TextStyle::Small)
275 }
276
277 #[inline]
279 pub fn small_raised(self) -> Self {
280 self.text_style(TextStyle::Small).raised()
281 }
282
283 #[inline]
285 pub fn raised(mut self) -> Self {
286 self.raised = true;
287 self
288 }
289
290 #[inline]
292 pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
293 self.background_color = background_color.into();
294 self
295 }
296
297 #[inline]
302 pub fn color(mut self, color: impl Into<Color32>) -> Self {
303 self.text_color = Some(color.into());
304 self
305 }
306
307 pub fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
311 let mut font_id = self.text_style.as_ref().map_or_else(
312 || FontSelection::Default.resolve(style),
313 |text_style| text_style.resolve(style),
314 );
315
316 if let Some(size) = self.size {
317 font_id.size = size;
318 }
319 if let Some(family) = &self.family {
320 font_id.family = family.clone();
321 }
322 fonts.row_height(&font_id)
323 }
324
325 pub fn append_to(
355 self,
356 layout_job: &mut LayoutJob,
357 style: &Style,
358 fallback_font: FontSelection,
359 default_valign: Align,
360 ) {
361 let (text, format) = self.into_text_and_format(style, fallback_font, default_valign);
362
363 layout_job.append(&text, 0.0, format);
364 }
365
366 fn into_layout_job(
367 self,
368 style: &Style,
369 fallback_font: FontSelection,
370 default_valign: Align,
371 ) -> LayoutJob {
372 let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign);
373 LayoutJob::single_section(text, text_format)
374 }
375
376 fn into_text_and_format(
377 self,
378 style: &Style,
379 fallback_font: FontSelection,
380 default_valign: Align,
381 ) -> (String, crate::text::TextFormat) {
382 let text_color = self.get_text_color(&style.visuals);
383
384 let Self {
385 text,
386 size,
387 extra_letter_spacing,
388 line_height,
389 family,
390 text_style,
391 background_color,
392 expand_bg,
393 text_color: _, code,
395 strong: _, weak: _, strikethrough,
398 underline,
399 italics,
400 raised,
401 } = self;
402
403 let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
404 let text_color = text_color.unwrap_or(crate::Color32::PLACEHOLDER);
405
406 let font_id = {
407 let mut font_id = style.override_font_id.clone().unwrap_or_else(|| {
408 (text_style.as_ref().or(style.override_text_style.as_ref()))
409 .map(|text_style| text_style.resolve(style))
410 .unwrap_or_else(|| fallback_font.resolve(style))
411 });
412 if let Some(size) = size {
413 font_id.size = size;
414 }
415 if let Some(family) = family {
416 font_id.family = family;
417 }
418 font_id
419 };
420
421 let background_color = if code {
422 style.visuals.code_bg_color
423 } else {
424 background_color
425 };
426
427 let underline = if underline {
428 crate::Stroke::new(1.0, line_color)
429 } else {
430 crate::Stroke::NONE
431 };
432 let strikethrough = if strikethrough {
433 crate::Stroke::new(1.0, line_color)
434 } else {
435 crate::Stroke::NONE
436 };
437
438 let valign = if raised {
439 crate::Align::TOP
440 } else {
441 default_valign
442 };
443
444 (
445 text,
446 crate::text::TextFormat {
447 font_id,
448 extra_letter_spacing,
449 line_height,
450 color: text_color,
451 background: background_color,
452 italics,
453 underline,
454 strikethrough,
455 valign,
456 expand_bg,
457 },
458 )
459 }
460
461 fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
462 if let Some(text_color) = self.text_color {
463 Some(text_color)
464 } else if self.strong {
465 Some(visuals.strong_text_color())
466 } else if self.weak {
467 Some(visuals.weak_text_color())
468 } else {
469 visuals.override_text_color
470 }
471 }
472}
473
474#[derive(Clone)]
489pub enum WidgetText {
490 Text(String),
495
496 RichText(Arc<RichText>),
500
501 LayoutJob(Arc<LayoutJob>),
514
515 Galley(Arc<Galley>),
520}
521
522impl std::fmt::Debug for WidgetText {
523 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
524 let text = self.text();
525 match self {
526 Self::Text(_) => write!(f, "Text({text:?})"),
527 Self::RichText(_) => write!(f, "RichText({text:?})"),
528 Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"),
529 Self::Galley(_) => write!(f, "Galley({text:?})"),
530 }
531 }
532}
533
534impl Default for WidgetText {
535 fn default() -> Self {
536 Self::Text(String::new())
537 }
538}
539
540impl WidgetText {
541 #[inline]
542 pub fn is_empty(&self) -> bool {
543 match self {
544 Self::Text(text) => text.is_empty(),
545 Self::RichText(text) => text.is_empty(),
546 Self::LayoutJob(job) => job.is_empty(),
547 Self::Galley(galley) => galley.is_empty(),
548 }
549 }
550
551 #[inline]
552 pub fn text(&self) -> &str {
553 match self {
554 Self::Text(text) => text,
555 Self::RichText(text) => text.text(),
556 Self::LayoutJob(job) => &job.text,
557 Self::Galley(galley) => galley.text(),
558 }
559 }
560
561 #[must_use]
567 fn map_rich_text<F>(self, f: F) -> Self
568 where
569 F: FnOnce(RichText) -> RichText,
570 {
571 match self {
572 Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))),
573 Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))),
574 other => other,
575 }
576 }
577
578 #[inline]
582 pub fn text_style(self, text_style: TextStyle) -> Self {
583 self.map_rich_text(|text| text.text_style(text_style))
584 }
585
586 #[inline]
590 pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
591 self.map_rich_text(|text| text.fallback_text_style(text_style))
592 }
593
594 #[inline]
598 pub fn color(self, color: impl Into<Color32>) -> Self {
599 self.map_rich_text(|text| text.color(color))
600 }
601
602 #[inline]
604 pub fn heading(self) -> Self {
605 self.map_rich_text(|text| text.heading())
606 }
607
608 #[inline]
610 pub fn monospace(self) -> Self {
611 self.map_rich_text(|text| text.monospace())
612 }
613
614 #[inline]
616 pub fn code(self) -> Self {
617 self.map_rich_text(|text| text.code())
618 }
619
620 #[inline]
622 pub fn strong(self) -> Self {
623 self.map_rich_text(|text| text.strong())
624 }
625
626 #[inline]
628 pub fn weak(self) -> Self {
629 self.map_rich_text(|text| text.weak())
630 }
631
632 #[inline]
634 pub fn underline(self) -> Self {
635 self.map_rich_text(|text| text.underline())
636 }
637
638 #[inline]
640 pub fn strikethrough(self) -> Self {
641 self.map_rich_text(|text| text.strikethrough())
642 }
643
644 #[inline]
646 pub fn italics(self) -> Self {
647 self.map_rich_text(|text| text.italics())
648 }
649
650 #[inline]
652 pub fn small(self) -> Self {
653 self.map_rich_text(|text| text.small())
654 }
655
656 #[inline]
658 pub fn small_raised(self) -> Self {
659 self.map_rich_text(|text| text.small_raised())
660 }
661
662 #[inline]
664 pub fn raised(self) -> Self {
665 self.map_rich_text(|text| text.raised())
666 }
667
668 #[inline]
670 pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
671 self.map_rich_text(|text| text.background_color(background_color))
672 }
673
674 pub(crate) fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
676 match self {
677 Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
678 Self::RichText(text) => text.font_height(fonts, style),
679 Self::LayoutJob(job) => job.font_height(fonts),
680 Self::Galley(galley) => {
681 if let Some(placed_row) = galley.rows.first() {
682 placed_row.height().round_ui()
683 } else {
684 galley.size().y.round_ui()
685 }
686 }
687 }
688 }
689
690 pub fn into_layout_job(
691 self,
692 style: &Style,
693 fallback_font: FontSelection,
694 default_valign: Align,
695 ) -> Arc<LayoutJob> {
696 match self {
697 Self::Text(text) => Arc::new(LayoutJob::simple_format(
698 text,
699 TextFormat {
700 font_id: FontSelection::Default.resolve(style),
701 color: crate::Color32::PLACEHOLDER,
702 valign: default_valign,
703 ..Default::default()
704 },
705 )),
706 Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job(
707 style,
708 fallback_font,
709 default_valign,
710 )),
711 Self::LayoutJob(job) => job,
712 Self::Galley(galley) => galley.job.clone(),
713 }
714 }
715
716 pub fn into_galley(
720 self,
721 ui: &Ui,
722 wrap_mode: Option<TextWrapMode>,
723 available_width: f32,
724 fallback_font: impl Into<FontSelection>,
725 ) -> Arc<Galley> {
726 let valign = ui.text_valign();
727 let style = ui.style();
728
729 let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
730 let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width);
731
732 self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign)
733 }
734
735 pub fn into_galley_impl(
736 self,
737 ctx: &crate::Context,
738 style: &Style,
739 text_wrapping: TextWrapping,
740 fallback_font: FontSelection,
741 default_valign: Align,
742 ) -> Arc<Galley> {
743 match self {
744 Self::Text(text) => {
745 let color = style
746 .visuals
747 .override_text_color
748 .unwrap_or(crate::Color32::PLACEHOLDER);
749 let mut layout_job = LayoutJob::simple_format(
750 text,
751 TextFormat {
752 font_id: FontSelection::default()
754 .resolve_with_fallback(style, fallback_font),
755 color,
756 valign: default_valign,
757 ..Default::default()
758 },
759 );
760 layout_job.wrap = text_wrapping;
761 ctx.fonts_mut(|f| f.layout_job(layout_job))
762 }
763 Self::RichText(text) => {
764 let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
765 style,
766 fallback_font,
767 default_valign,
768 );
769 layout_job.wrap = text_wrapping;
770 ctx.fonts_mut(|f| f.layout_job(layout_job))
771 }
772 Self::LayoutJob(job) => {
773 let mut job = Arc::unwrap_or_clone(job);
774 job.wrap = text_wrapping;
775 ctx.fonts_mut(|f| f.layout_job(job))
776 }
777 Self::Galley(galley) => galley,
778 }
779 }
780}
781
782impl From<&str> for WidgetText {
783 #[inline]
784 fn from(text: &str) -> Self {
785 Self::Text(text.to_owned())
786 }
787}
788
789impl From<&String> for WidgetText {
790 #[inline]
791 fn from(text: &String) -> Self {
792 Self::Text(text.clone())
793 }
794}
795
796impl From<String> for WidgetText {
797 #[inline]
798 fn from(text: String) -> Self {
799 Self::Text(text)
800 }
801}
802
803impl From<&Box<str>> for WidgetText {
804 #[inline]
805 fn from(text: &Box<str>) -> Self {
806 Self::Text(text.to_string())
807 }
808}
809
810impl From<Box<str>> for WidgetText {
811 #[inline]
812 fn from(text: Box<str>) -> Self {
813 Self::Text(text.into())
814 }
815}
816
817impl From<Cow<'_, str>> for WidgetText {
818 #[inline]
819 fn from(text: Cow<'_, str>) -> Self {
820 Self::Text(text.into_owned())
821 }
822}
823
824impl From<RichText> for WidgetText {
825 #[inline]
826 fn from(rich_text: RichText) -> Self {
827 Self::RichText(Arc::new(rich_text))
828 }
829}
830
831impl From<Arc<RichText>> for WidgetText {
832 #[inline]
833 fn from(rich_text: Arc<RichText>) -> Self {
834 Self::RichText(rich_text)
835 }
836}
837
838impl From<LayoutJob> for WidgetText {
839 #[inline]
840 fn from(layout_job: LayoutJob) -> Self {
841 Self::LayoutJob(Arc::new(layout_job))
842 }
843}
844
845impl From<Arc<LayoutJob>> for WidgetText {
846 #[inline]
847 fn from(layout_job: Arc<LayoutJob>) -> Self {
848 Self::LayoutJob(layout_job)
849 }
850}
851
852impl From<Arc<Galley>> for WidgetText {
853 #[inline]
854 fn from(galley: Arc<Galley>) -> Self {
855 Self::Galley(galley)
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use crate::WidgetText;
862
863 #[test]
864 fn ensure_small_widget_text() {
865 assert_eq!(size_of::<WidgetText>(), size_of::<String>());
866 }
867}