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 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}