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: &epaint::Fonts, 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 = text_style
408 .or_else(|| style.override_text_style.clone())
409 .map_or_else(
410 || fallback_font.resolve(style),
411 |text_style| text_style.resolve(style),
412 );
413 if let Some(fid) = style.override_font_id.clone() {
414 font_id = fid;
415 }
416 if let Some(size) = size {
417 font_id.size = size;
418 }
419 if let Some(family) = family {
420 font_id.family = family;
421 }
422 font_id
423 };
424
425 let mut background_color = background_color;
426 if code {
427 background_color = style.visuals.code_bg_color;
428 }
429 let underline = if underline {
430 crate::Stroke::new(1.0, line_color)
431 } else {
432 crate::Stroke::NONE
433 };
434 let strikethrough = if strikethrough {
435 crate::Stroke::new(1.0, line_color)
436 } else {
437 crate::Stroke::NONE
438 };
439
440 let valign = if raised {
441 crate::Align::TOP
442 } else {
443 default_valign
444 };
445
446 (
447 text,
448 crate::text::TextFormat {
449 font_id,
450 extra_letter_spacing,
451 line_height,
452 color: text_color,
453 background: background_color,
454 italics,
455 underline,
456 strikethrough,
457 valign,
458 expand_bg,
459 },
460 )
461 }
462
463 fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
464 if let Some(text_color) = self.text_color {
465 Some(text_color)
466 } else if self.strong {
467 Some(visuals.strong_text_color())
468 } else if self.weak {
469 Some(visuals.weak_text_color())
470 } else {
471 visuals.override_text_color
472 }
473 }
474}
475
476#[derive(Clone)]
491pub enum WidgetText {
492 Text(String),
497
498 RichText(Arc<RichText>),
502
503 LayoutJob(Arc<LayoutJob>),
516
517 Galley(Arc<Galley>),
522}
523
524impl std::fmt::Debug for WidgetText {
525 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
526 let text = self.text();
527 match self {
528 Self::Text(_) => write!(f, "Text({text:?})"),
529 Self::RichText(_) => write!(f, "RichText({text:?})"),
530 Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"),
531 Self::Galley(_) => write!(f, "Galley({text:?})"),
532 }
533 }
534}
535
536impl Default for WidgetText {
537 fn default() -> Self {
538 Self::Text(String::new())
539 }
540}
541
542impl WidgetText {
543 #[inline]
544 pub fn is_empty(&self) -> bool {
545 match self {
546 Self::Text(text) => text.is_empty(),
547 Self::RichText(text) => text.is_empty(),
548 Self::LayoutJob(job) => job.is_empty(),
549 Self::Galley(galley) => galley.is_empty(),
550 }
551 }
552
553 #[inline]
554 pub fn text(&self) -> &str {
555 match self {
556 Self::Text(text) => text,
557 Self::RichText(text) => text.text(),
558 Self::LayoutJob(job) => &job.text,
559 Self::Galley(galley) => galley.text(),
560 }
561 }
562
563 #[must_use]
569 fn map_rich_text<F>(self, f: F) -> Self
570 where
571 F: FnOnce(RichText) -> RichText,
572 {
573 match self {
574 Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))),
575 Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))),
576 other => other,
577 }
578 }
579
580 #[inline]
584 pub fn text_style(self, text_style: TextStyle) -> Self {
585 self.map_rich_text(|text| text.text_style(text_style))
586 }
587
588 #[inline]
592 pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
593 self.map_rich_text(|text| text.fallback_text_style(text_style))
594 }
595
596 #[inline]
600 pub fn color(self, color: impl Into<Color32>) -> Self {
601 self.map_rich_text(|text| text.color(color))
602 }
603
604 #[inline]
606 pub fn heading(self) -> Self {
607 self.map_rich_text(|text| text.heading())
608 }
609
610 #[inline]
612 pub fn monospace(self) -> Self {
613 self.map_rich_text(|text| text.monospace())
614 }
615
616 #[inline]
618 pub fn code(self) -> Self {
619 self.map_rich_text(|text| text.code())
620 }
621
622 #[inline]
624 pub fn strong(self) -> Self {
625 self.map_rich_text(|text| text.strong())
626 }
627
628 #[inline]
630 pub fn weak(self) -> Self {
631 self.map_rich_text(|text| text.weak())
632 }
633
634 #[inline]
636 pub fn underline(self) -> Self {
637 self.map_rich_text(|text| text.underline())
638 }
639
640 #[inline]
642 pub fn strikethrough(self) -> Self {
643 self.map_rich_text(|text| text.strikethrough())
644 }
645
646 #[inline]
648 pub fn italics(self) -> Self {
649 self.map_rich_text(|text| text.italics())
650 }
651
652 #[inline]
654 pub fn small(self) -> Self {
655 self.map_rich_text(|text| text.small())
656 }
657
658 #[inline]
660 pub fn small_raised(self) -> Self {
661 self.map_rich_text(|text| text.small_raised())
662 }
663
664 #[inline]
666 pub fn raised(self) -> Self {
667 self.map_rich_text(|text| text.raised())
668 }
669
670 #[inline]
672 pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
673 self.map_rich_text(|text| text.background_color(background_color))
674 }
675
676 pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
678 match self {
679 Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
680 Self::RichText(text) => text.font_height(fonts, style),
681 Self::LayoutJob(job) => job.font_height(fonts),
682 Self::Galley(galley) => {
683 if let Some(placed_row) = galley.rows.first() {
684 placed_row.height().round_ui()
685 } else {
686 galley.size().y.round_ui()
687 }
688 }
689 }
690 }
691
692 pub fn into_layout_job(
693 self,
694 style: &Style,
695 fallback_font: FontSelection,
696 default_valign: Align,
697 ) -> Arc<LayoutJob> {
698 match self {
699 Self::Text(text) => Arc::new(LayoutJob::simple_format(
700 text,
701 TextFormat {
702 font_id: FontSelection::Default.resolve(style),
703 color: crate::Color32::PLACEHOLDER,
704 valign: default_valign,
705 ..Default::default()
706 },
707 )),
708 Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job(
709 style,
710 fallback_font,
711 default_valign,
712 )),
713 Self::LayoutJob(job) => job,
714 Self::Galley(galley) => galley.job.clone(),
715 }
716 }
717
718 pub fn into_galley(
722 self,
723 ui: &Ui,
724 wrap_mode: Option<TextWrapMode>,
725 available_width: f32,
726 fallback_font: impl Into<FontSelection>,
727 ) -> Arc<Galley> {
728 let valign = ui.text_valign();
729 let style = ui.style();
730
731 let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
732 let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width);
733
734 self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign)
735 }
736
737 pub fn into_galley_impl(
738 self,
739 ctx: &crate::Context,
740 style: &Style,
741 text_wrapping: TextWrapping,
742 fallback_font: FontSelection,
743 default_valign: Align,
744 ) -> Arc<Galley> {
745 match self {
746 Self::Text(text) => {
747 let mut layout_job = LayoutJob::simple_format(
748 text,
749 TextFormat {
750 font_id: FontSelection::Default.resolve(style),
751 color: crate::Color32::PLACEHOLDER,
752 valign: default_valign,
753 ..Default::default()
754 },
755 );
756 layout_job.wrap = text_wrapping;
757 ctx.fonts(|f| f.layout_job(layout_job))
758 }
759 Self::RichText(text) => {
760 let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
761 style,
762 fallback_font,
763 default_valign,
764 );
765 layout_job.wrap = text_wrapping;
766 ctx.fonts(|f| f.layout_job(layout_job))
767 }
768 Self::LayoutJob(job) => {
769 let mut job = Arc::unwrap_or_clone(job);
770 job.wrap = text_wrapping;
771 ctx.fonts(|f| f.layout_job(job))
772 }
773 Self::Galley(galley) => galley,
774 }
775 }
776}
777
778impl From<&str> for WidgetText {
779 #[inline]
780 fn from(text: &str) -> Self {
781 Self::Text(text.to_owned())
782 }
783}
784
785impl From<&String> for WidgetText {
786 #[inline]
787 fn from(text: &String) -> Self {
788 Self::Text(text.clone())
789 }
790}
791
792impl From<String> for WidgetText {
793 #[inline]
794 fn from(text: String) -> Self {
795 Self::Text(text)
796 }
797}
798
799impl From<&Box<str>> for WidgetText {
800 #[inline]
801 fn from(text: &Box<str>) -> Self {
802 Self::Text(text.to_string())
803 }
804}
805
806impl From<Box<str>> for WidgetText {
807 #[inline]
808 fn from(text: Box<str>) -> Self {
809 Self::Text(text.into())
810 }
811}
812
813impl From<Cow<'_, str>> for WidgetText {
814 #[inline]
815 fn from(text: Cow<'_, str>) -> Self {
816 Self::Text(text.into_owned())
817 }
818}
819
820impl From<RichText> for WidgetText {
821 #[inline]
822 fn from(rich_text: RichText) -> Self {
823 Self::RichText(Arc::new(rich_text))
824 }
825}
826
827impl From<Arc<RichText>> for WidgetText {
828 #[inline]
829 fn from(rich_text: Arc<RichText>) -> Self {
830 Self::RichText(rich_text)
831 }
832}
833
834impl From<LayoutJob> for WidgetText {
835 #[inline]
836 fn from(layout_job: LayoutJob) -> Self {
837 Self::LayoutJob(Arc::new(layout_job))
838 }
839}
840
841impl From<Arc<LayoutJob>> for WidgetText {
842 #[inline]
843 fn from(layout_job: Arc<LayoutJob>) -> Self {
844 Self::LayoutJob(layout_job)
845 }
846}
847
848impl From<Arc<Galley>> for WidgetText {
849 #[inline]
850 fn from(galley: Arc<Galley>) -> Self {
851 Self::Galley(galley)
852 }
853}
854
855#[cfg(test)]
856mod tests {
857 use crate::WidgetText;
858
859 #[test]
860 fn ensure_small_widget_text() {
861 assert_eq!(size_of::<WidgetText>(), size_of::<String>());
862 }
863}