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#[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 #[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
121impl Label {
122 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 Sense::focusable_noninteractive()
132 } else {
133 Sense::hover()
134 }
135 });
136
137 if selectable {
138 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; sense = sense.union(select_sense);
152 }
153
154 if let WidgetText::Galley(galley) = self.text {
155 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 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 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 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 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 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 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}