1use crate::theme::Theme;
2use crate::visuals::LabelVisuals;
3use egui::{
4 Align, Color32, FontSelection, Image, Pos2, Rect, Response, Sense, Stroke, StrokeKind,
5 TextWrapMode, Ui, Vec2, Widget, WidgetText,
6 epaint::{RectShape, TextShape},
7 style::WidgetVisuals,
8 text::LayoutJob,
9};
10use std::sync::Arc;
11
12#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
26#[derive(Clone)]
27pub struct Label {
28 text: WidgetText,
29 visuals: Option<LabelVisuals>,
30 pub(crate) image: Option<Image<'static>>,
31 spacing: f32,
32 expansion: Option<f32>,
33 pub(crate) sense: Option<Sense>,
34 wrap_mode: Option<TextWrapMode>,
35 selectable: Option<bool>,
36 text_first: bool,
37 selected: bool,
38 interactive: bool,
39 fill_width: bool,
40}
41impl Label {
42 pub fn new(text: impl Into<WidgetText>, image: Option<Image<'static>>) -> Self {
45 Self {
46 text: text.into(),
47 visuals: None,
48 image,
49 spacing: 6.0,
50 expansion: None,
51 sense: None,
52 wrap_mode: None,
53 selectable: None,
54 text_first: true,
55 selected: false,
56 interactive: false,
57 fill_width: false,
58 }
59 }
60
61 pub fn visuals(mut self, visuals: LabelVisuals) -> Self {
63 self.visuals = Some(visuals);
64 self
65 }
66
67 pub fn spacing(mut self, spacing: f32) -> Self {
69 self.spacing = spacing;
70 self
71 }
72
73 pub fn expand(mut self, expansion: Option<f32>) -> Self {
75 self.expansion = expansion;
76 self
77 }
78
79 pub fn sense(mut self, sense: Sense) -> Self {
82 self.sense = Some(sense);
83 self.selectable = Some(false);
84 self
85 }
86
87 pub fn interactive(mut self, interactive: bool) -> Self {
91 self.interactive = interactive;
92 self
93 }
94
95 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
97 self.wrap_mode = Some(wrap_mode);
98 self
99 }
100
101 pub fn wrap(mut self) -> Self {
103 self.wrap_mode = Some(TextWrapMode::Wrap);
104 self
105 }
106
107 pub fn selectable(mut self, selectable: bool) -> Self {
109 self.selectable = Some(selectable);
110 self
111 }
112
113 pub fn image_on_left(mut self) -> Self {
115 self.text_first = false;
116 self
117 }
118
119 pub fn selected(mut self, selected: bool) -> Self {
121 self.selected = selected;
122 self
123 }
124
125 pub fn fill_width(mut self, fill: bool) -> Self {
127 self.fill_width = fill;
128 self
129 }
130
131 fn image_display_size(image: &Image<'_>, ui: &Ui) -> Vec2 {
136 image.calc_size(ui.available_size(), image.size())
137 }
138
139 pub fn galley_and_size(
143 &self,
144 ui: &Ui,
145 available_width_for_text: f32,
146 ) -> (Arc<egui::Galley>, Vec2) {
147 let layout_job = self.prepare_layout_job(ui, available_width_for_text);
148 let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
149 let text_size = galley.size();
150
151 let image_size = self
152 .image
153 .as_ref()
154 .map(|image| Self::image_display_size(image, ui))
155 .unwrap_or(Vec2::ZERO);
156
157 let total_width = text_size.x
158 + if self.image.is_some() {
159 self.spacing + image_size.x
160 } else {
161 0.0
162 };
163
164 let total_height = text_size.y.max(image_size.y);
165 (galley, Vec2::new(total_width, total_height))
166 }
167
168 fn prepare_layout_job(&self, ui: &Ui, wrap_width: f32) -> LayoutJob {
169 let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
170 let layout_job = self.text.clone().into_layout_job(
171 ui.style(),
172 FontSelection::Default,
173 ui.text_valign(),
174 );
175
176 let mut layout_job: LayoutJob = (*layout_job).clone();
178 match wrap_mode {
179 TextWrapMode::Extend => {
180 layout_job.wrap.max_width = f32::INFINITY;
181 }
182 TextWrapMode::Wrap => {
183 layout_job.wrap.max_width = wrap_width;
184 }
185 TextWrapMode::Truncate => {
186 layout_job.wrap.max_width = wrap_width;
187 layout_job.wrap.max_rows = 1;
188 layout_job.wrap.break_anywhere = true;
189 }
190 }
191
192 layout_job.halign = Align::LEFT;
193 layout_job
194 }
195 pub(crate) fn paint_content_within_rect(
196 &self,
197 ui: &mut Ui,
198 rect: Rect,
199 button_visuals: &WidgetVisuals,
200 ) {
201 let available_width_for_text = if let Some(image) = &self.image {
203 (rect.width() - Self::image_display_size(image, ui).x - self.spacing).max(0.0)
204 } else {
205 rect.width()
206 };
207
208 let (galley, _) = self.galley_and_size(ui, available_width_for_text);
210 if ui.is_rect_visible(rect) {
211 let (text_pos, image_rect_opt) = layout_content_within_rect(
212 ui,
213 rect,
214 &galley,
215 &self.image,
216 self.spacing,
217 self.text_first,
218 );
219
220 let text_color = self
221 .visuals
222 .or_else(|| Theme::label_visuals_from_ctx(ui.ctx()))
223 .as_ref()
224 .map(|v| v.text)
225 .unwrap_or(button_visuals.text_color());
226 ui.painter().add(TextShape::new(
227 text_pos,
228 galley.clone(),
229 text_color,
230 ));
231
232 if let Some(image_rect) = image_rect_opt {
233 if let Some(image) = &self.image {
234 image.paint_at(ui, image_rect);
235 }
236 }
237 }
238 }
239}
240
241fn layout_content_within_rect(
242 ui: &Ui,
243 rect: Rect,
244 galley: &egui::Galley,
245 image: &Option<Image<'static>>,
246 spacing: f32,
247 text_first: bool,
248) -> (Pos2, Option<Rect>) {
249 let text_size = galley.size();
250 let image_size = image
251 .as_ref()
252 .map(|image| Label::image_display_size(image, ui))
253 .unwrap_or(Vec2::ZERO);
254
255 let total_content_height = text_size.y.max(image_size.y);
256 let top_y = ui
257 .layout()
258 .align_size_within_rect(Vec2::new(0.0, total_content_height), rect)
259 .min
260 .y;
261
262 let (text_start_x, image_final_rect) = if text_first {
263 let text_start_x = rect.left();
265 let image_start_x = text_start_x + text_size.x + spacing;
266 let image_final_rect = image.as_ref().map(|_| {
267 let image_pos = Pos2::new(
268 image_start_x,
269 top_y + (total_content_height - image_size.y) * 0.5,
270 );
271 Rect::from_min_size(image_pos, image_size)
272 });
273 (text_start_x, image_final_rect)
274 } else {
275 let image_start_x = rect.left();
277 let text_start_x = image_start_x + image_size.x + spacing;
278 let image_final_rect = image.as_ref().map(|_| {
279 let image_pos = Pos2::new(
280 image_start_x,
281 top_y + (total_content_height - image_size.y) * 0.5,
282 );
283 Rect::from_min_size(image_pos, image_size)
284 });
285 (text_start_x, image_final_rect)
286 };
287
288 let text_pos = Pos2::new(
289 text_start_x,
290 top_y + (total_content_height - text_size.y) * 0.5,
291 );
292 (text_pos, image_final_rect)
293}
294
295impl Widget for Label {
296 fn ui(self, ui: &mut Ui) -> Response {
297 let image_size = self
299 .image
300 .as_ref()
301 .map(|image| Self::image_display_size(image, ui))
302 .unwrap_or(Vec2::ZERO);
303
304 let effective_wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
305
306 let available_width_for_text = match effective_wrap_mode {
307 TextWrapMode::Extend => f32::INFINITY,
308 _ => {
309 if self.image.is_some() {
311 (ui.available_width() - self.spacing - image_size.x).max(10.0)
312 } else {
313 ui.available_width()
314 }
315 }
316 };
317
318 let (galley, content_size) = self.galley_and_size(ui, available_width_for_text);
319 let desired_size = if self.fill_width {
320 Vec2::new(ui.available_width(), content_size.y)
321 } else {
322 content_size
323 };
324
325 let sense = self.sense.unwrap_or(if self.interactive {
327 Sense::hover()
328 } else {
329 Sense::empty()
330 });
331 let (rect, response) = ui.allocate_exact_size(desired_size, sense);
332
333 if ui.is_rect_visible(rect) {
335 let visuals = ui.style().interact_selectable(&response, self.selected);
336 let label_visuals = self.visuals.or_else(|| Theme::label_visuals_from_ctx(ui.ctx()));
337
338 let fill = if self.selected {
339 label_visuals.as_ref().map(|v| v.bg_selected).unwrap_or(visuals.bg_fill)
340 } else if response.hovered() || response.has_focus() {
341 match self.interactive {
342 true => label_visuals.as_ref().map(|v| v.bg_hover).unwrap_or(visuals.weak_bg_fill),
343 false => Color32::TRANSPARENT,
344 }
345 } else {
346 label_visuals.as_ref().map(|v| v.bg).unwrap_or(Color32::TRANSPARENT)
347 };
348
349 let stroke = if self.selected {
350 label_visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke)
351 } else if response.hovered() || response.has_focus() {
352 match self.interactive {
353 true => label_visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke),
354 false => Stroke::new(0.0, Color32::TRANSPARENT),
355 }
356 } else {
357 label_visuals.as_ref().map(|v| v.border).unwrap_or(Stroke::NONE)
358 };
359
360 let expansion = self.expansion.unwrap_or(visuals.expansion);
361 let corner =
362 label_visuals.as_ref().map(|v| v.corner_radius).unwrap_or(visuals.corner_radius);
363 let background_rect = rect.expand(expansion);
364
365 ui.painter().add(RectShape::new(
366 background_rect,
367 corner,
368 fill,
369 stroke,
370 StrokeKind::Inside,
371 ));
372
373 let (text_pos, image_rect_opt) = layout_content_within_rect(
375 ui,
376 rect,
377 &galley,
378 &self.image,
379 self.spacing,
380 self.text_first,
381 );
382
383 let text_color = label_visuals.as_ref().map(|v| v.text).unwrap_or(visuals.text_color());
384
385 ui.painter().add(TextShape::new(
386 text_pos,
387 galley.clone(),
388 text_color,
389 ));
390
391 if let Some(image_rect) = image_rect_opt {
392 if let Some(image) = self.image {
393 image.paint_at(ui, image_rect);
394 }
395 }
396 }
397 response
398 }
399}