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#[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    #[inline]
56    pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
57        self.wrap_mode = Some(wrap_mode);
58        self
59    }
60
61    #[inline]
63    pub fn wrap(mut self) -> Self {
64        self.wrap_mode = Some(TextWrapMode::Wrap);
65
66        self
67    }
68
69    #[inline]
71    pub fn truncate(mut self) -> Self {
72        self.wrap_mode = Some(TextWrapMode::Truncate);
73        self
74    }
75
76    #[inline]
79    pub fn extend(mut self) -> Self {
80        self.wrap_mode = Some(TextWrapMode::Extend);
81        self
82    }
83
84    #[inline]
86    pub fn halign(mut self, align: Align) -> Self {
87        self.halign = Some(align);
88        self
89    }
90
91    #[inline]
95    pub fn selectable(mut self, selectable: bool) -> Self {
96        self.selectable = Some(selectable);
97        self
98    }
99
100    #[inline]
115    pub fn sense(mut self, sense: Sense) -> Self {
116        self.sense = Some(sense);
117        self
118    }
119
120    #[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    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                Sense::focusable_noninteractive()
149            } else {
150                Sense::hover()
151            }
152        });
153
154        if selectable {
155            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; sense |= select_sense;
169        }
170
171        if let WidgetText::Galley(galley) = self.text {
172            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            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            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            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                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        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                let job = crate::text::LayoutJob {
286                    sections: galley.job.sections.clone(),
287                    text: galley.job.text.clone(),
288                    ..crate::text::LayoutJob::default()
289                };
290                response = response.on_hover_text(job);
292            }
293
294            let response_color = if interactive {
295                ui.style().interact(&response).text_color()
296            } else {
297                ui.style().visuals.text_color()
298            };
299
300            let underline = if response.has_focus() || response.highlighted() {
301                Stroke::new(1.0, response_color)
302            } else {
303                Stroke::NONE
304            };
305
306            let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels);
307            if selectable {
308                LabelSelectionState::label_text_selection(
309                    ui,
310                    &response,
311                    galley_pos,
312                    galley,
313                    response_color,
314                    underline,
315                );
316            } else {
317                ui.painter().add(
318                    epaint::TextShape::new(galley_pos, galley, response_color)
319                        .with_underline(underline),
320                );
321            }
322        }
323
324        response
325    }
326}