egui/
widget_text.rs

1use std::{borrow::Cow, sync::Arc};
2
3use emath::GuiRounding as _;
4
5use crate::{
6    text::{LayoutJob, TextWrapping},
7    Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
8};
9
10/// Text and optional style choices for it.
11///
12/// The style choices (font, color) are applied to the entire text.
13/// For more detailed control, use [`crate::text::LayoutJob`] instead.
14///
15/// A [`RichText`] can be used in most widgets and helper functions, e.g. [`Ui::label`] and [`Ui::button`].
16///
17/// ### Example
18/// ```
19/// use egui::{RichText, Color32};
20///
21/// RichText::new("Plain");
22/// RichText::new("colored").color(Color32::RED);
23/// RichText::new("Large and underlined").size(20.0).underline();
24/// ```
25#[derive(Debug, Clone, Default, 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    text_color: Option<Color32>,
35    code: bool,
36    strong: bool,
37    weak: bool,
38    strikethrough: bool,
39    underline: bool,
40    italics: bool,
41    raised: bool,
42}
43
44impl From<&str> for RichText {
45    #[inline]
46    fn from(text: &str) -> Self {
47        Self::new(text)
48    }
49}
50
51impl From<&String> for RichText {
52    #[inline]
53    fn from(text: &String) -> Self {
54        Self::new(text)
55    }
56}
57
58impl From<&mut String> for RichText {
59    #[inline]
60    fn from(text: &mut String) -> Self {
61        Self::new(text.clone())
62    }
63}
64
65impl From<String> for RichText {
66    #[inline]
67    fn from(text: String) -> Self {
68        Self::new(text)
69    }
70}
71
72impl From<&Box<str>> for RichText {
73    #[inline]
74    fn from(text: &Box<str>) -> Self {
75        Self::new(text.clone())
76    }
77}
78
79impl From<&mut Box<str>> for RichText {
80    #[inline]
81    fn from(text: &mut Box<str>) -> Self {
82        Self::new(text.clone())
83    }
84}
85
86impl From<Box<str>> for RichText {
87    #[inline]
88    fn from(text: Box<str>) -> Self {
89        Self::new(text)
90    }
91}
92
93impl From<Cow<'_, str>> for RichText {
94    #[inline]
95    fn from(text: Cow<'_, str>) -> Self {
96        Self::new(text)
97    }
98}
99
100impl RichText {
101    #[inline]
102    pub fn new(text: impl Into<String>) -> Self {
103        Self {
104            text: text.into(),
105            ..Default::default()
106        }
107    }
108
109    #[inline]
110    pub fn is_empty(&self) -> bool {
111        self.text.is_empty()
112    }
113
114    #[inline]
115    pub fn text(&self) -> &str {
116        &self.text
117    }
118
119    /// Select the font size (in points).
120    /// This overrides the value from [`Self::text_style`].
121    #[inline]
122    pub fn size(mut self, size: f32) -> Self {
123        self.size = Some(size);
124        self
125    }
126
127    /// Extra spacing between letters, in points.
128    ///
129    /// Default: 0.0.
130    ///
131    /// For even text it is recommended you round this to an even number of _pixels_,
132    /// e.g. using [`crate::Painter::round_to_pixel`].
133    #[inline]
134    pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
135        self.extra_letter_spacing = extra_letter_spacing;
136        self
137    }
138
139    /// Explicit line height of the text in points.
140    ///
141    /// This is the distance between the bottom row of two subsequent lines of text.
142    ///
143    /// If `None` (the default), the line height is determined by the font.
144    ///
145    /// For even text it is recommended you round this to an even number of _pixels_,
146    /// e.g. using [`crate::Painter::round_to_pixel`].
147    #[inline]
148    pub fn line_height(mut self, line_height: Option<f32>) -> Self {
149        self.line_height = line_height;
150        self
151    }
152
153    /// Select the font family.
154    ///
155    /// This overrides the value from [`Self::text_style`].
156    ///
157    /// Only the families available in [`crate::FontDefinitions::families`] may be used.
158    #[inline]
159    pub fn family(mut self, family: FontFamily) -> Self {
160        self.family = Some(family);
161        self
162    }
163
164    /// Select the font and size.
165    /// This overrides the value from [`Self::text_style`].
166    #[inline]
167    pub fn font(mut self, font_id: crate::FontId) -> Self {
168        let crate::FontId { size, family } = font_id;
169        self.size = Some(size);
170        self.family = Some(family);
171        self
172    }
173
174    /// Override the [`TextStyle`].
175    #[inline]
176    pub fn text_style(mut self, text_style: TextStyle) -> Self {
177        self.text_style = Some(text_style);
178        self
179    }
180
181    /// Set the [`TextStyle`] unless it has already been set
182    #[inline]
183    pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
184        self.text_style.get_or_insert(text_style);
185        self
186    }
187
188    /// Use [`TextStyle::Heading`].
189    #[inline]
190    pub fn heading(self) -> Self {
191        self.text_style(TextStyle::Heading)
192    }
193
194    /// Use [`TextStyle::Monospace`].
195    #[inline]
196    pub fn monospace(self) -> Self {
197        self.text_style(TextStyle::Monospace)
198    }
199
200    /// Monospace label with different background color.
201    #[inline]
202    pub fn code(mut self) -> Self {
203        self.code = true;
204        self.text_style(TextStyle::Monospace)
205    }
206
207    /// Extra strong text (stronger color).
208    #[inline]
209    pub fn strong(mut self) -> Self {
210        self.strong = true;
211        self
212    }
213
214    /// Extra weak text (fainter color).
215    #[inline]
216    pub fn weak(mut self) -> Self {
217        self.weak = true;
218        self
219    }
220
221    /// Draw a line under the text.
222    ///
223    /// If you want to control the line color, use [`LayoutJob`] instead.
224    #[inline]
225    pub fn underline(mut self) -> Self {
226        self.underline = true;
227        self
228    }
229
230    /// Draw a line through the text, crossing it out.
231    ///
232    /// If you want to control the strikethrough line color, use [`LayoutJob`] instead.
233    #[inline]
234    pub fn strikethrough(mut self) -> Self {
235        self.strikethrough = true;
236        self
237    }
238
239    /// Tilt the characters to the right.
240    #[inline]
241    pub fn italics(mut self) -> Self {
242        self.italics = true;
243        self
244    }
245
246    /// Smaller text.
247    #[inline]
248    pub fn small(self) -> Self {
249        self.text_style(TextStyle::Small)
250    }
251
252    /// For e.g. exponents.
253    #[inline]
254    pub fn small_raised(self) -> Self {
255        self.text_style(TextStyle::Small).raised()
256    }
257
258    /// Align text to top. Only applicable together with [`Self::small()`].
259    #[inline]
260    pub fn raised(mut self) -> Self {
261        self.raised = true;
262        self
263    }
264
265    /// Fill-color behind the text.
266    #[inline]
267    pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
268        self.background_color = background_color.into();
269        self
270    }
271
272    /// Override text color.
273    ///
274    /// If not set, [`Color32::PLACEHOLDER`] will be used,
275    /// which will be replaced with a color chosen by the widget that paints the text.
276    #[inline]
277    pub fn color(mut self, color: impl Into<Color32>) -> Self {
278        self.text_color = Some(color.into());
279        self
280    }
281
282    /// Read the font height of the selected text style.
283    ///
284    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
285    pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
286        let mut font_id = self.text_style.as_ref().map_or_else(
287            || FontSelection::Default.resolve(style),
288            |text_style| text_style.resolve(style),
289        );
290
291        if let Some(size) = self.size {
292            font_id.size = size;
293        }
294        if let Some(family) = &self.family {
295            font_id.family = family.clone();
296        }
297        fonts.row_height(&font_id)
298    }
299
300    /// Append to an existing [`LayoutJob`]
301    ///
302    /// Note that the color of the [`RichText`] must be set, or may default to an undesirable color.
303    ///
304    /// ### Example
305    /// ```
306    /// use egui::{Style, RichText, text::LayoutJob, Color32, FontSelection, Align};
307    ///
308    /// let style = Style::default();
309    /// let mut layout_job = LayoutJob::default();
310    /// RichText::new("Normal")
311    ///     .color(style.visuals.text_color())
312    ///     .append_to(
313    ///         &mut layout_job,
314    ///         &style,
315    ///         FontSelection::Default,
316    ///         Align::Center,
317    ///     );
318    /// RichText::new("Large and underlined")
319    ///     .color(style.visuals.text_color())
320    ///     .size(20.0)
321    ///     .underline()
322    ///     .append_to(
323    ///         &mut layout_job,
324    ///         &style,
325    ///         FontSelection::Default,
326    ///         Align::Center,
327    ///     );
328    /// ```
329    pub fn append_to(
330        self,
331        layout_job: &mut LayoutJob,
332        style: &Style,
333        fallback_font: FontSelection,
334        default_valign: Align,
335    ) {
336        let (text, format) = self.into_text_and_format(style, fallback_font, default_valign);
337
338        layout_job.append(&text, 0.0, format);
339    }
340
341    fn into_layout_job(
342        self,
343        style: &Style,
344        fallback_font: FontSelection,
345        default_valign: Align,
346    ) -> LayoutJob {
347        let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign);
348        LayoutJob::single_section(text, text_format)
349    }
350
351    fn into_text_and_format(
352        self,
353        style: &Style,
354        fallback_font: FontSelection,
355        default_valign: Align,
356    ) -> (String, crate::text::TextFormat) {
357        let text_color = self.get_text_color(&style.visuals);
358
359        let Self {
360            text,
361            size,
362            extra_letter_spacing,
363            line_height,
364            family,
365            text_style,
366            background_color,
367            text_color: _, // already used by `get_text_color`
368            code,
369            strong: _, // already used by `get_text_color`
370            weak: _,   // already used by `get_text_color`
371            strikethrough,
372            underline,
373            italics,
374            raised,
375        } = self;
376
377        let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
378        let text_color = text_color.unwrap_or(crate::Color32::PLACEHOLDER);
379
380        let font_id = {
381            let mut font_id = text_style
382                .or_else(|| style.override_text_style.clone())
383                .map_or_else(
384                    || fallback_font.resolve(style),
385                    |text_style| text_style.resolve(style),
386                );
387            if let Some(fid) = style.override_font_id.clone() {
388                font_id = fid;
389            }
390            if let Some(size) = size {
391                font_id.size = size;
392            }
393            if let Some(family) = family {
394                font_id.family = family;
395            }
396            font_id
397        };
398
399        let mut background_color = background_color;
400        if code {
401            background_color = style.visuals.code_bg_color;
402        }
403        let underline = if underline {
404            crate::Stroke::new(1.0, line_color)
405        } else {
406            crate::Stroke::NONE
407        };
408        let strikethrough = if strikethrough {
409            crate::Stroke::new(1.0, line_color)
410        } else {
411            crate::Stroke::NONE
412        };
413
414        let valign = if raised {
415            crate::Align::TOP
416        } else {
417            default_valign
418        };
419
420        (
421            text,
422            crate::text::TextFormat {
423                font_id,
424                extra_letter_spacing,
425                line_height,
426                color: text_color,
427                background: background_color,
428                italics,
429                underline,
430                strikethrough,
431                valign,
432            },
433        )
434    }
435
436    fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
437        if let Some(text_color) = self.text_color {
438            Some(text_color)
439        } else if self.strong {
440            Some(visuals.strong_text_color())
441        } else if self.weak {
442            Some(visuals.weak_text_color())
443        } else {
444            visuals.override_text_color
445        }
446    }
447}
448
449// ----------------------------------------------------------------------------
450
451/// This is how you specify text for a widget.
452///
453/// A lot of widgets use `impl Into<WidgetText>` as an argument,
454/// allowing you to pass in [`String`], [`RichText`], [`LayoutJob`], and more.
455///
456/// Often a [`WidgetText`] is just a simple [`String`],
457/// but it can be a [`RichText`] (text with color, style, etc),
458/// a [`LayoutJob`] (for when you want full control of how the text looks)
459/// or text that has already been laid out in a [`Galley`].
460///
461/// You can color the text however you want, or use [`Color32::PLACEHOLDER`]
462/// which will be replaced with a color chosen by the widget that paints the text.
463#[derive(Clone)]
464pub enum WidgetText {
465    RichText(RichText),
466
467    /// Use this [`LayoutJob`] when laying out the text.
468    ///
469    /// Only [`LayoutJob::text`] and [`LayoutJob::sections`] are guaranteed to be respected.
470    ///
471    /// [`TextWrapping::max_width`](epaint::text::TextWrapping::max_width), [`LayoutJob::halign`], [`LayoutJob::justify`]
472    /// and [`LayoutJob::first_row_min_height`] will likely be determined by the [`crate::Layout`]
473    /// of the [`Ui`] the widget is placed in.
474    /// If you want all parts of the [`LayoutJob`] respected, then convert it to a
475    /// [`Galley`] and use [`Self::Galley`] instead.
476    ///
477    /// You can color the text however you want, or use [`Color32::PLACEHOLDER`]
478    /// which will be replaced with a color chosen by the widget that paints the text.
479    LayoutJob(LayoutJob),
480
481    /// Use exactly this galley when painting the text.
482    ///
483    /// You can color the text however you want, or use [`Color32::PLACEHOLDER`]
484    /// which will be replaced with a color chosen by the widget that paints the text.
485    Galley(Arc<Galley>),
486}
487
488impl Default for WidgetText {
489    fn default() -> Self {
490        Self::RichText(RichText::default())
491    }
492}
493
494impl WidgetText {
495    #[inline]
496    pub fn is_empty(&self) -> bool {
497        match self {
498            Self::RichText(text) => text.is_empty(),
499            Self::LayoutJob(job) => job.is_empty(),
500            Self::Galley(galley) => galley.is_empty(),
501        }
502    }
503
504    #[inline]
505    pub fn text(&self) -> &str {
506        match self {
507            Self::RichText(text) => text.text(),
508            Self::LayoutJob(job) => &job.text,
509            Self::Galley(galley) => galley.text(),
510        }
511    }
512
513    /// Override the [`TextStyle`] if, and only if, this is a [`RichText`].
514    ///
515    /// Prefer using [`RichText`] directly!
516    #[inline]
517    pub fn text_style(self, text_style: TextStyle) -> Self {
518        match self {
519            Self::RichText(text) => Self::RichText(text.text_style(text_style)),
520            Self::LayoutJob(_) | Self::Galley(_) => self,
521        }
522    }
523
524    /// Set the [`TextStyle`] unless it has already been set
525    ///
526    /// Prefer using [`RichText`] directly!
527    #[inline]
528    pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
529        match self {
530            Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
531            Self::LayoutJob(_) | Self::Galley(_) => self,
532        }
533    }
534
535    /// Override text color if, and only if, this is a [`RichText`].
536    ///
537    /// Prefer using [`RichText`] directly!
538    #[inline]
539    pub fn color(self, color: impl Into<Color32>) -> Self {
540        match self {
541            Self::RichText(text) => Self::RichText(text.color(color)),
542            Self::LayoutJob(_) | Self::Galley(_) => self,
543        }
544    }
545
546    /// Prefer using [`RichText`] directly!
547    pub fn heading(self) -> Self {
548        match self {
549            Self::RichText(text) => Self::RichText(text.heading()),
550            Self::LayoutJob(_) | Self::Galley(_) => self,
551        }
552    }
553
554    /// Prefer using [`RichText`] directly!
555    pub fn monospace(self) -> Self {
556        match self {
557            Self::RichText(text) => Self::RichText(text.monospace()),
558            Self::LayoutJob(_) | Self::Galley(_) => self,
559        }
560    }
561
562    /// Prefer using [`RichText`] directly!
563    pub fn code(self) -> Self {
564        match self {
565            Self::RichText(text) => Self::RichText(text.code()),
566            Self::LayoutJob(_) | Self::Galley(_) => self,
567        }
568    }
569
570    /// Prefer using [`RichText`] directly!
571    pub fn strong(self) -> Self {
572        match self {
573            Self::RichText(text) => Self::RichText(text.strong()),
574            Self::LayoutJob(_) | Self::Galley(_) => self,
575        }
576    }
577
578    /// Prefer using [`RichText`] directly!
579    pub fn weak(self) -> Self {
580        match self {
581            Self::RichText(text) => Self::RichText(text.weak()),
582            Self::LayoutJob(_) | Self::Galley(_) => self,
583        }
584    }
585
586    /// Prefer using [`RichText`] directly!
587    pub fn underline(self) -> Self {
588        match self {
589            Self::RichText(text) => Self::RichText(text.underline()),
590            Self::LayoutJob(_) | Self::Galley(_) => self,
591        }
592    }
593
594    /// Prefer using [`RichText`] directly!
595    pub fn strikethrough(self) -> Self {
596        match self {
597            Self::RichText(text) => Self::RichText(text.strikethrough()),
598            Self::LayoutJob(_) | Self::Galley(_) => self,
599        }
600    }
601
602    /// Prefer using [`RichText`] directly!
603    pub fn italics(self) -> Self {
604        match self {
605            Self::RichText(text) => Self::RichText(text.italics()),
606            Self::LayoutJob(_) | Self::Galley(_) => self,
607        }
608    }
609
610    /// Prefer using [`RichText`] directly!
611    pub fn small(self) -> Self {
612        match self {
613            Self::RichText(text) => Self::RichText(text.small()),
614            Self::LayoutJob(_) | Self::Galley(_) => self,
615        }
616    }
617
618    /// Prefer using [`RichText`] directly!
619    pub fn small_raised(self) -> Self {
620        match self {
621            Self::RichText(text) => Self::RichText(text.small_raised()),
622            Self::LayoutJob(_) | Self::Galley(_) => self,
623        }
624    }
625
626    /// Prefer using [`RichText`] directly!
627    pub fn raised(self) -> Self {
628        match self {
629            Self::RichText(text) => Self::RichText(text.raised()),
630            Self::LayoutJob(_) | Self::Galley(_) => self,
631        }
632    }
633
634    /// Prefer using [`RichText`] directly!
635    pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
636        match self {
637            Self::RichText(text) => Self::RichText(text.background_color(background_color)),
638            Self::LayoutJob(_) | Self::Galley(_) => self,
639        }
640    }
641
642    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
643    pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
644        match self {
645            Self::RichText(text) => text.font_height(fonts, style),
646            Self::LayoutJob(job) => job.font_height(fonts),
647            Self::Galley(galley) => {
648                if let Some(row) = galley.rows.first() {
649                    row.height().round_ui()
650                } else {
651                    galley.size().y.round_ui()
652                }
653            }
654        }
655    }
656
657    pub fn into_layout_job(
658        self,
659        style: &Style,
660        fallback_font: FontSelection,
661        default_valign: Align,
662    ) -> LayoutJob {
663        match self {
664            Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign),
665            Self::LayoutJob(job) => job,
666            Self::Galley(galley) => (*galley.job).clone(),
667        }
668    }
669
670    /// Layout with wrap mode based on the containing [`Ui`].
671    ///
672    /// `wrap_mode`: override for [`Ui::wrap_mode`]
673    pub fn into_galley(
674        self,
675        ui: &Ui,
676        wrap_mode: Option<TextWrapMode>,
677        available_width: f32,
678        fallback_font: impl Into<FontSelection>,
679    ) -> Arc<Galley> {
680        let valign = ui.text_valign();
681        let style = ui.style();
682
683        let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
684        let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width);
685
686        self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign)
687    }
688
689    pub fn into_galley_impl(
690        self,
691        ctx: &crate::Context,
692        style: &Style,
693        text_wrapping: TextWrapping,
694        fallback_font: FontSelection,
695        default_valign: Align,
696    ) -> Arc<Galley> {
697        match self {
698            Self::RichText(text) => {
699                let mut layout_job = text.into_layout_job(style, fallback_font, default_valign);
700                layout_job.wrap = text_wrapping;
701                ctx.fonts(|f| f.layout_job(layout_job))
702            }
703            Self::LayoutJob(mut job) => {
704                job.wrap = text_wrapping;
705                ctx.fonts(|f| f.layout_job(job))
706            }
707            Self::Galley(galley) => galley,
708        }
709    }
710}
711
712impl From<&str> for WidgetText {
713    #[inline]
714    fn from(text: &str) -> Self {
715        Self::RichText(RichText::new(text))
716    }
717}
718
719impl From<&String> for WidgetText {
720    #[inline]
721    fn from(text: &String) -> Self {
722        Self::RichText(RichText::new(text))
723    }
724}
725
726impl From<String> for WidgetText {
727    #[inline]
728    fn from(text: String) -> Self {
729        Self::RichText(RichText::new(text))
730    }
731}
732
733impl From<&Box<str>> for WidgetText {
734    #[inline]
735    fn from(text: &Box<str>) -> Self {
736        Self::RichText(RichText::new(text.clone()))
737    }
738}
739
740impl From<Box<str>> for WidgetText {
741    #[inline]
742    fn from(text: Box<str>) -> Self {
743        Self::RichText(RichText::new(text))
744    }
745}
746
747impl From<Cow<'_, str>> for WidgetText {
748    #[inline]
749    fn from(text: Cow<'_, str>) -> Self {
750        Self::RichText(RichText::new(text))
751    }
752}
753
754impl From<RichText> for WidgetText {
755    #[inline]
756    fn from(rich_text: RichText) -> Self {
757        Self::RichText(rich_text)
758    }
759}
760
761impl From<LayoutJob> for WidgetText {
762    #[inline]
763    fn from(layout_job: LayoutJob) -> Self {
764        Self::LayoutJob(layout_job)
765    }
766}
767
768impl From<Arc<Galley>> for WidgetText {
769    #[inline]
770    fn from(galley: Arc<Galley>) -> Self {
771        Self::Galley(galley)
772    }
773}