egui/
widget_text.rs

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