egui/widgets/
label.rs

1use std::sync::Arc;
2
3use crate::{
4    Align, Direction, FontSelection, Galley, Pos2, Response, Sense, Stroke, TextWrapMode, Ui,
5    Widget, WidgetInfo, WidgetText, WidgetType, epaint, pos2, text_selection::LabelSelectionState,
6};
7
8/// Static text.
9///
10/// Usually it is more convenient to use [`Ui::label`].
11///
12/// ```
13/// # use egui::TextWrapMode;
14/// # egui::__run_test_ui(|ui| {
15/// ui.label("Equivalent");
16/// ui.add(egui::Label::new("Equivalent"));
17/// ui.add(egui::Label::new("With Options").truncate());
18/// ui.label(egui::RichText::new("With formatting").underline());
19/// # });
20/// ```
21///
22/// For full control of the text you can use [`crate::text::LayoutJob`]
23/// as argument to [`Self::new`].
24#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
25pub struct Label {
26    text: WidgetText,
27    wrap_mode: Option<TextWrapMode>,
28    sense: Option<Sense>,
29    selectable: Option<bool>,
30    halign: Option<Align>,
31    show_tooltip_when_elided: bool,
32}
33
34impl Label {
35    pub fn new(text: impl Into<WidgetText>) -> Self {
36        Self {
37            text: text.into(),
38            wrap_mode: None,
39            sense: None,
40            selectable: None,
41            halign: None,
42            show_tooltip_when_elided: true,
43        }
44    }
45
46    pub fn text(&self) -> &str {
47        self.text.text()
48    }
49
50    /// Set the wrap mode for the text.
51    ///
52    /// By default, [`crate::Ui::wrap_mode`] will be used, which can be overridden with [`crate::Style::wrap_mode`].
53    ///
54    /// Note that any `\n` in the text will always produce a new line.
55    #[inline]
56    pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
57        self.wrap_mode = Some(wrap_mode);
58        self
59    }
60
61    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`].
62    #[inline]
63    pub fn wrap(mut self) -> Self {
64        self.wrap_mode = Some(TextWrapMode::Wrap);
65
66        self
67    }
68
69    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`].
70    #[inline]
71    pub fn truncate(mut self) -> Self {
72        self.wrap_mode = Some(TextWrapMode::Truncate);
73        self
74    }
75
76    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Extend`],
77    /// disabling wrapping and truncating, and instead expanding the parent [`Ui`].
78    #[inline]
79    pub fn extend(mut self) -> Self {
80        self.wrap_mode = Some(TextWrapMode::Extend);
81        self
82    }
83
84    /// Sets the horizontal alignment of the Label to the given `Align` value.
85    #[inline]
86    pub fn halign(mut self, align: Align) -> Self {
87        self.halign = Some(align);
88        self
89    }
90
91    /// Can the user select the text with the mouse?
92    ///
93    /// Overrides [`crate::style::Interaction::selectable_labels`].
94    #[inline]
95    pub fn selectable(mut self, selectable: bool) -> Self {
96        self.selectable = Some(selectable);
97        self
98    }
99
100    /// Make the label respond to clicks and/or drags.
101    ///
102    /// By default, a label is inert and does not respond to click or drags.
103    /// By calling this you can turn the label into a button of sorts.
104    /// This will also give the label the hover-effect of a button, but without the frame.
105    ///
106    /// ```
107    /// # use egui::{Label, Sense};
108    /// # egui::__run_test_ui(|ui| {
109    /// if ui.add(Label::new("click me").sense(Sense::click())).clicked() {
110    ///     /* … */
111    /// }
112    /// # });
113    /// ```
114    #[inline]
115    pub fn sense(mut self, sense: Sense) -> Self {
116        self.sense = Some(sense);
117        self
118    }
119
120    /// Show the full text when hovered, if the text was elided.
121    ///
122    /// By default, this is true.
123    ///
124    /// ```
125    /// # use egui::{Label, Sense};
126    /// # egui::__run_test_ui(|ui| {
127    /// ui.add(Label::new("some text").show_tooltip_when_elided(false))
128    ///     .on_hover_text("completely different text");
129    /// # });
130    /// ```
131    #[inline]
132    pub fn show_tooltip_when_elided(mut self, show: bool) -> Self {
133        self.show_tooltip_when_elided = show;
134        self
135    }
136}
137
138impl Label {
139    /// Do layout and position the galley in the ui, without painting it or adding widget info.
140    pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, Arc<Galley>, Response) {
141        let selectable = self
142            .selectable
143            .unwrap_or_else(|| ui.style().interaction.selectable_labels);
144
145        let mut sense = self.sense.unwrap_or_else(|| {
146            if ui.memory(|mem| mem.options.screen_reader) {
147                // We only want to focus labels if the screen reader is on.
148                Sense::focusable_noninteractive()
149            } else {
150                Sense::hover()
151            }
152        });
153
154        if selectable {
155            // On touch screens (e.g. mobile in `eframe` web), should
156            // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
157            // Since currently copying selected text in not supported on `eframe` web,
158            // we prioritize touch-scrolling:
159            let allow_drag_to_select = ui.input(|i| !i.has_touch_screen());
160
161            let mut select_sense = if allow_drag_to_select {
162                Sense::click_and_drag()
163            } else {
164                Sense::click()
165            };
166            select_sense -= Sense::FOCUSABLE; // Don't move focus to labels with TAB key.
167
168            sense |= select_sense;
169        }
170
171        if let WidgetText::Galley(galley) = self.text {
172            // If the user said "use this specific galley", then just use it:
173            let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
174            let pos = match galley.job.halign {
175                Align::LEFT => rect.left_top(),
176                Align::Center => rect.center_top(),
177                Align::RIGHT => rect.right_top(),
178            };
179            return (pos, galley, response);
180        }
181
182        let valign = ui.text_valign();
183        let mut layout_job = Arc::unwrap_or_clone(self.text.into_layout_job(
184            ui.style(),
185            FontSelection::Default,
186            valign,
187        ));
188
189        let available_width = ui.available_width();
190
191        let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
192        if wrap_mode == TextWrapMode::Wrap
193            && ui.layout().main_dir() == Direction::LeftToRight
194            && ui.layout().main_wrap()
195            && available_width.is_finite()
196        {
197            // On a wrapping horizontal layout we want text to start after the previous widget,
198            // then continue on the line below! This will take some extra work:
199
200            let cursor = ui.cursor();
201            let first_row_indentation = available_width - ui.available_size_before_wrap().x;
202            debug_assert!(
203                first_row_indentation.is_finite(),
204                "first row indentation is not finite: {first_row_indentation}"
205            );
206
207            layout_job.wrap.max_width = available_width;
208            layout_job.first_row_min_height = cursor.height();
209            layout_job.halign = Align::Min;
210            layout_job.justify = false;
211            if let Some(first_section) = layout_job.sections.first_mut() {
212                first_section.leading_space = first_row_indentation;
213            }
214            let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
215
216            let pos = pos2(ui.max_rect().left(), ui.cursor().top());
217            assert!(!galley.rows.is_empty(), "Galleys are never empty");
218            // collect a response from many rows:
219            let rect = galley.rows[0]
220                .rect_without_leading_space()
221                .translate(pos.to_vec2());
222            let mut response = ui.allocate_rect(rect, sense);
223            response.intrinsic_size = Some(galley.intrinsic_size());
224            for placed_row in galley.rows.iter().skip(1) {
225                let rect = placed_row.rect().translate(pos.to_vec2());
226                response |= ui.allocate_rect(rect, sense);
227            }
228            (pos, galley, response)
229        } else {
230            // Apply wrap_mode, but don't overwrite anything important
231            // the user may have set manually on the layout_job:
232            match wrap_mode {
233                TextWrapMode::Extend => {
234                    layout_job.wrap.max_width = f32::INFINITY;
235                }
236                TextWrapMode::Wrap => {
237                    layout_job.wrap.max_width = available_width;
238                }
239                TextWrapMode::Truncate => {
240                    layout_job.wrap.max_width = available_width;
241                    layout_job.wrap.max_rows = 1;
242                    layout_job.wrap.break_anywhere = true;
243                }
244            }
245
246            if ui.is_grid() {
247                // TODO(emilk): remove special Grid hacks like these
248                layout_job.halign = Align::LEFT;
249                layout_job.justify = false;
250            } else {
251                layout_job.halign = self.halign.unwrap_or(ui.layout().horizontal_placement());
252                layout_job.justify = ui.layout().horizontal_justify();
253            };
254
255            let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
256            let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
257            response.intrinsic_size = Some(galley.intrinsic_size());
258            let galley_pos = match galley.job.halign {
259                Align::LEFT => rect.left_top(),
260                Align::Center => rect.center_top(),
261                Align::RIGHT => rect.right_top(),
262            };
263            (galley_pos, galley, response)
264        }
265    }
266}
267
268impl Widget for Label {
269    fn ui(self, ui: &mut Ui) -> Response {
270        // Interactive = the uses asked to sense interaction.
271        // We DON'T want to have the color respond just because the text is selectable;
272        // the cursor is enough to communicate that.
273        let interactive = self.sense.is_some_and(|sense| sense != Sense::hover());
274
275        let selectable = self.selectable;
276        let show_tooltip_when_elided = self.show_tooltip_when_elided;
277
278        let (galley_pos, galley, mut response) = self.layout_in_ui(ui);
279        response
280            .widget_info(|| WidgetInfo::labeled(WidgetType::Label, ui.is_enabled(), galley.text()));
281
282        if ui.is_rect_visible(response.rect) {
283            if show_tooltip_when_elided && galley.elided {
284                // Show the full (non-elided) text on hover:
285                response = response.on_hover_text(galley.text());
286            }
287
288            let response_color = if interactive {
289                ui.style().interact(&response).text_color()
290            } else {
291                ui.style().visuals.text_color()
292            };
293
294            let underline = if response.has_focus() || response.highlighted() {
295                Stroke::new(1.0, response_color)
296            } else {
297                Stroke::NONE
298            };
299
300            let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels);
301            if selectable {
302                LabelSelectionState::label_text_selection(
303                    ui,
304                    &response,
305                    galley_pos,
306                    galley,
307                    response_color,
308                    underline,
309                );
310            } else {
311                ui.painter().add(
312                    epaint::TextShape::new(galley_pos, galley, response_color)
313                        .with_underline(underline),
314                );
315            }
316        }
317
318        response
319    }
320}