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