egui/widgets/
label.rs

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