egui/widgets/
label.rs

1use std::sync::Arc;
2
3use crate::{
4    epaint, pos2, text_selection, vec2, Align, Direction, FontSelection, Galley, Pos2, Response,
5    Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
6};
7
8use self::text_selection::LabelSelectionState;
9
10/// Static text.
11///
12/// Usually it is more convenient to use [`Ui::label`].
13///
14/// ```
15/// # use egui::TextWrapMode;
16/// # egui::__run_test_ui(|ui| {
17/// ui.label("Equivalent");
18/// ui.add(egui::Label::new("Equivalent"));
19/// ui.add(egui::Label::new("With Options").truncate());
20/// ui.label(egui::RichText::new("With formatting").underline());
21/// # });
22/// ```
23///
24/// For full control of the text you can use [`crate::text::LayoutJob`]
25/// as argument to [`Self::new`].
26#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
27pub struct Label {
28    text: WidgetText,
29    wrap_mode: Option<TextWrapMode>,
30    sense: Option<Sense>,
31    selectable: Option<bool>,
32    halign: Option<Align>,
33}
34
35impl Label {
36    pub fn new(text: impl Into<WidgetText>) -> Self {
37        Self {
38            text: text.into(),
39            wrap_mode: None,
40            sense: None,
41            selectable: None,
42            halign: None,
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
121impl Label {
122    /// Do layout and position the galley in the ui, without painting it or adding widget info.
123    pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, Arc<Galley>, Response) {
124        let selectable = self
125            .selectable
126            .unwrap_or_else(|| ui.style().interaction.selectable_labels);
127
128        let mut sense = self.sense.unwrap_or_else(|| {
129            if ui.memory(|mem| mem.options.screen_reader) {
130                // We only want to focus labels if the screen reader is on.
131                Sense::focusable_noninteractive()
132            } else {
133                Sense::hover()
134            }
135        });
136
137        if selectable {
138            // On touch screens (e.g. mobile in `eframe` web), should
139            // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
140            // Since currently copying selected text in not supported on `eframe` web,
141            // we prioritize touch-scrolling:
142            let allow_drag_to_select = ui.input(|i| !i.has_touch_screen());
143
144            let mut select_sense = if allow_drag_to_select {
145                Sense::click_and_drag()
146            } else {
147                Sense::click()
148            };
149            select_sense -= Sense::FOCUSABLE; // Don't move focus to labels with TAB key.
150
151            sense = sense.union(select_sense);
152        }
153
154        if let WidgetText::Galley(galley) = self.text {
155            // If the user said "use this specific galley", then just use it:
156            let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
157            let pos = match galley.job.halign {
158                Align::LEFT => rect.left_top(),
159                Align::Center => rect.center_top(),
160                Align::RIGHT => rect.right_top(),
161            };
162            return (pos, galley, response);
163        }
164
165        let valign = ui.text_valign();
166        let mut layout_job = self
167            .text
168            .into_layout_job(ui.style(), FontSelection::Default, valign);
169
170        let available_width = ui.available_width();
171
172        let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
173        if wrap_mode == TextWrapMode::Wrap
174            && ui.layout().main_dir() == Direction::LeftToRight
175            && ui.layout().main_wrap()
176            && available_width.is_finite()
177        {
178            // On a wrapping horizontal layout we want text to start after the previous widget,
179            // then continue on the line below! This will take some extra work:
180
181            let cursor = ui.cursor();
182            let first_row_indentation = available_width - ui.available_size_before_wrap().x;
183            debug_assert!(first_row_indentation.is_finite());
184
185            layout_job.wrap.max_width = available_width;
186            layout_job.first_row_min_height = cursor.height();
187            layout_job.halign = Align::Min;
188            layout_job.justify = false;
189            if let Some(first_section) = layout_job.sections.first_mut() {
190                first_section.leading_space = first_row_indentation;
191            }
192            let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
193
194            let pos = pos2(ui.max_rect().left(), ui.cursor().top());
195            assert!(!galley.rows.is_empty(), "Galleys are never empty");
196            // collect a response from many rows:
197            let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y));
198            let mut response = ui.allocate_rect(rect, sense);
199            for row in galley.rows.iter().skip(1) {
200                let rect = row.rect.translate(vec2(pos.x, pos.y));
201                response |= ui.allocate_rect(rect, sense);
202            }
203            (pos, galley, response)
204        } else {
205            // Apply wrap_mode, but don't overwrite anything important
206            // the user may have set manually on the layout_job:
207            match wrap_mode {
208                TextWrapMode::Extend => {
209                    layout_job.wrap.max_width = f32::INFINITY;
210                }
211                TextWrapMode::Wrap => {
212                    layout_job.wrap.max_width = available_width;
213                }
214                TextWrapMode::Truncate => {
215                    layout_job.wrap.max_width = available_width;
216                    layout_job.wrap.max_rows = 1;
217                    layout_job.wrap.break_anywhere = true;
218                }
219            }
220
221            if ui.is_grid() {
222                // TODO(emilk): remove special Grid hacks like these
223                layout_job.halign = Align::LEFT;
224                layout_job.justify = false;
225            } else {
226                layout_job.halign = self.halign.unwrap_or(ui.layout().horizontal_placement());
227                layout_job.justify = ui.layout().horizontal_justify();
228            };
229
230            let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
231            let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
232            let galley_pos = match galley.job.halign {
233                Align::LEFT => rect.left_top(),
234                Align::Center => rect.center_top(),
235                Align::RIGHT => rect.right_top(),
236            };
237            (galley_pos, galley, response)
238        }
239    }
240}
241
242impl Widget for Label {
243    fn ui(self, ui: &mut Ui) -> Response {
244        // Interactive = the uses asked to sense interaction.
245        // We DON'T want to have the color respond just because the text is selectable;
246        // the cursor is enough to communicate that.
247        let interactive = self.sense.is_some_and(|sense| sense != Sense::hover());
248
249        let selectable = self.selectable;
250
251        let (galley_pos, galley, mut response) = self.layout_in_ui(ui);
252        response
253            .widget_info(|| WidgetInfo::labeled(WidgetType::Label, ui.is_enabled(), galley.text()));
254
255        if ui.is_rect_visible(response.rect) {
256            if galley.elided {
257                // Show the full (non-elided) text on hover:
258                response = response.on_hover_text(galley.text());
259            }
260
261            let response_color = if interactive {
262                ui.style().interact(&response).text_color()
263            } else {
264                ui.style().visuals.text_color()
265            };
266
267            let underline = if response.has_focus() || response.highlighted() {
268                Stroke::new(1.0, response_color)
269            } else {
270                Stroke::NONE
271            };
272
273            let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels);
274            if selectable {
275                LabelSelectionState::label_text_selection(
276                    ui,
277                    &response,
278                    galley_pos,
279                    galley,
280                    response_color,
281                    underline,
282                );
283            } else {
284                ui.painter().add(
285                    epaint::TextShape::new(galley_pos, galley, response_color)
286                        .with_underline(underline),
287                );
288            }
289        }
290
291        response
292    }
293}