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 color = style
748                    .visuals
749                    .override_text_color
750                    .unwrap_or(crate::Color32::PLACEHOLDER);
751                let mut layout_job = LayoutJob::simple_format(
752                    text,
753                    TextFormat {
754                        font_id: FontSelection::default()
756                            .resolve_with_fallback(style, fallback_font),
757                        color,
758                        valign: default_valign,
759                        ..Default::default()
760                    },
761                );
762                layout_job.wrap = text_wrapping;
763                ctx.fonts(|f| f.layout_job(layout_job))
764            }
765            Self::RichText(text) => {
766                let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
767                    style,
768                    fallback_font,
769                    default_valign,
770                );
771                layout_job.wrap = text_wrapping;
772                ctx.fonts(|f| f.layout_job(layout_job))
773            }
774            Self::LayoutJob(job) => {
775                let mut job = Arc::unwrap_or_clone(job);
776                job.wrap = text_wrapping;
777                ctx.fonts(|f| f.layout_job(job))
778            }
779            Self::Galley(galley) => galley,
780        }
781    }
782}
783
784impl From<&str> for WidgetText {
785    #[inline]
786    fn from(text: &str) -> Self {
787        Self::Text(text.to_owned())
788    }
789}
790
791impl From<&String> for WidgetText {
792    #[inline]
793    fn from(text: &String) -> Self {
794        Self::Text(text.clone())
795    }
796}
797
798impl From<String> for WidgetText {
799    #[inline]
800    fn from(text: String) -> Self {
801        Self::Text(text)
802    }
803}
804
805impl From<&Box<str>> for WidgetText {
806    #[inline]
807    fn from(text: &Box<str>) -> Self {
808        Self::Text(text.to_string())
809    }
810}
811
812impl From<Box<str>> for WidgetText {
813    #[inline]
814    fn from(text: Box<str>) -> Self {
815        Self::Text(text.into())
816    }
817}
818
819impl From<Cow<'_, str>> for WidgetText {
820    #[inline]
821    fn from(text: Cow<'_, str>) -> Self {
822        Self::Text(text.into_owned())
823    }
824}
825
826impl From<RichText> for WidgetText {
827    #[inline]
828    fn from(rich_text: RichText) -> Self {
829        Self::RichText(Arc::new(rich_text))
830    }
831}
832
833impl From<Arc<RichText>> for WidgetText {
834    #[inline]
835    fn from(rich_text: Arc<RichText>) -> Self {
836        Self::RichText(rich_text)
837    }
838}
839
840impl From<LayoutJob> for WidgetText {
841    #[inline]
842    fn from(layout_job: LayoutJob) -> Self {
843        Self::LayoutJob(Arc::new(layout_job))
844    }
845}
846
847impl From<Arc<LayoutJob>> for WidgetText {
848    #[inline]
849    fn from(layout_job: Arc<LayoutJob>) -> Self {
850        Self::LayoutJob(layout_job)
851    }
852}
853
854impl From<Arc<Galley>> for WidgetText {
855    #[inline]
856    fn from(galley: Arc<Galley>) -> Self {
857        Self::Galley(galley)
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use crate::WidgetText;
864
865    #[test]
866    fn ensure_small_widget_text() {
867        assert_eq!(size_of::<WidgetText>(), size_of::<String>());
868    }
869}