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: true,
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 pub fn galley_and_size(
135 &self,
136 ui: &Ui,
137 available_width_for_text: f32,
138 ) -> (Arc<egui::Galley>, Vec2) {
139 let layout_job = self.prepare_layout_job(ui, available_width_for_text);
140 let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
141 let text_size = galley.size();
142
143 let image_size = if let Some(image) = &self.image {
144 image.calc_size(ui.available_size(), image.size())
145 } else {
146 Vec2::ZERO
147 };
148
149 let total_width = text_size.x
150 + if self.image.is_some() {
151 self.spacing + image_size.x
152 } else {
153 0.0
154 };
155
156 let total_height = text_size.y.max(image_size.y);
157 (galley, Vec2::new(total_width, total_height))
158 }
159
160 fn prepare_layout_job(&self, ui: &Ui, wrap_width: f32) -> LayoutJob {
161 let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
162 let layout_job = self.text.clone().into_layout_job(
163 ui.style(),
164 FontSelection::Default,
165 ui.text_valign(),
166 );
167
168 let mut layout_job: LayoutJob = (*layout_job).clone();
170 match wrap_mode {
171 TextWrapMode::Extend => {
172 layout_job.wrap.max_width = f32::INFINITY;
173 }
174 TextWrapMode::Wrap => {
175 layout_job.wrap.max_width = wrap_width;
176 }
177 TextWrapMode::Truncate => {
178 layout_job.wrap.max_width = wrap_width;
179 layout_job.wrap.max_rows = 1;
180 layout_job.wrap.break_anywhere = true;
181 }
182 }
183
184 layout_job.halign = Align::LEFT;
185 layout_job
186 }
187 pub(crate) fn paint_content_within_rect(
188 &self,
189 ui: &mut Ui,
190 rect: Rect,
191 button_visuals: &WidgetVisuals,
192 ) {
193 let available_width_for_text = if self.image.is_some() {
195 (rect.width()
196 - self.image.as_ref().map_or(0.0, |img| img.size().map_or(0.0, |s| s.x))
197 - self.spacing)
198 .max(0.0)
199 } else {
200 rect.width()
201 };
202
203 let (galley, _) = self.galley_and_size(ui, available_width_for_text);
205 if ui.is_rect_visible(rect) {
206 let (text_pos, image_rect_opt) = layout_content_within_rect(
207 ui,
208 rect,
209 &galley,
210 &self.image,
211 self.spacing,
212 self.text_first,
213 );
214
215 let text_color = self
216 .visuals
217 .or_else(|| Theme::label_visuals_from_ctx(ui.ctx()))
218 .as_ref()
219 .map(|v| v.text)
220 .unwrap_or(button_visuals.text_color());
221 ui.painter().add(TextShape::new(
222 text_pos,
223 galley.clone(),
224 text_color,
225 ));
226
227 if let Some(image_rect) = image_rect_opt {
228 if let Some(image) = &self.image {
229 image.paint_at(ui, image_rect);
230 }
231 }
232 }
233 }
234}
235
236fn layout_content_within_rect(
237 ui: &Ui,
238 rect: Rect,
239 galley: &egui::Galley,
240 image: &Option<Image<'static>>,
241 spacing: f32,
242 text_first: bool,
243) -> (Pos2, Option<Rect>) {
244 let text_size = galley.size();
245 let image_size = if let Some(image) = image {
246 image.calc_size(ui.available_size(), image.size())
247 } else {
248 Vec2::ZERO
249 };
250
251 let total_content_height = text_size.y.max(image_size.y);
252 let top_y = ui
253 .layout()
254 .align_size_within_rect(Vec2::new(0.0, total_content_height), rect)
255 .min
256 .y;
257
258 let (text_start_x, image_final_rect) = if text_first {
259 let text_start_x = rect.left();
261 let image_start_x = text_start_x + text_size.x + spacing;
262 let image_final_rect = image.as_ref().map(|_| {
263 let image_pos = Pos2::new(
264 image_start_x,
265 top_y + (total_content_height - image_size.y) * 0.5,
266 );
267 Rect::from_min_size(image_pos, image_size)
268 });
269 (text_start_x, image_final_rect)
270 } else {
271 let image_start_x = rect.left();
273 let text_start_x = image_start_x + image_size.x + spacing;
274 let image_final_rect = image.as_ref().map(|_| {
275 let image_pos = Pos2::new(
276 image_start_x,
277 top_y + (total_content_height - image_size.y) * 0.5,
278 );
279 Rect::from_min_size(image_pos, image_size)
280 });
281 (text_start_x, image_final_rect)
282 };
283
284 let text_pos = Pos2::new(
285 text_start_x,
286 top_y + (total_content_height - text_size.y) * 0.5,
287 );
288 (text_pos, image_final_rect)
289}
290
291impl Widget for Label {
292 fn ui(self, ui: &mut Ui) -> Response {
293 let image_size = if let Some(image) = &self.image {
295 image.calc_size(ui.available_size(), image.size())
296 } else {
297 Vec2::ZERO
298 };
299
300 let effective_wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
301
302 let available_width_for_text = match effective_wrap_mode {
303 TextWrapMode::Extend => f32::INFINITY,
304 _ => {
305 if self.image.is_some() {
307 (ui.available_width() - self.spacing - image_size.x).max(10.0)
308 } else {
309 ui.available_width()
310 }
311 }
312 };
313
314 let (galley, content_size) = self.galley_and_size(ui, available_width_for_text);
315 let desired_size = if self.fill_width {
316 Vec2::new(ui.available_width(), content_size.y)
317 } else {
318 content_size
319 };
320
321 let sense = self.sense.unwrap_or(Sense::hover());
323 let (rect, response) = ui.allocate_exact_size(desired_size, sense);
324
325 if ui.is_rect_visible(rect) {
327 let visuals = ui.style().interact_selectable(&response, self.selected);
328 let label_visuals = self.visuals.or_else(|| Theme::label_visuals_from_ctx(ui.ctx()));
329
330 let fill = if self.selected {
331 label_visuals.as_ref().map(|v| v.bg_selected).unwrap_or(visuals.bg_fill)
332 } else if response.hovered() || response.has_focus() {
333 match self.interactive {
334 true => label_visuals.as_ref().map(|v| v.bg_hover).unwrap_or(visuals.weak_bg_fill),
335 false => Color32::TRANSPARENT,
336 }
337 } else {
338 label_visuals.as_ref().map(|v| v.bg).unwrap_or(Color32::TRANSPARENT)
339 };
340
341 let stroke = if self.selected {
342 label_visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke)
343 } else if response.hovered() || response.has_focus() {
344 match self.interactive {
345 true => label_visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke),
346 false => Stroke::new(0.0, Color32::TRANSPARENT),
347 }
348 } else {
349 label_visuals.as_ref().map(|v| v.border).unwrap_or(Stroke::NONE)
350 };
351
352 let expansion = self.expansion.unwrap_or(visuals.expansion);
353 let corner =
354 label_visuals.as_ref().map(|v| v.corner_radius).unwrap_or(visuals.corner_radius);
355 let background_rect = rect.expand(expansion);
356
357 ui.painter().add(RectShape::new(
358 background_rect,
359 corner,
360 fill,
361 stroke,
362 StrokeKind::Inside,
363 ));
364
365 let (text_pos, image_rect_opt) = layout_content_within_rect(
367 ui,
368 rect,
369 &galley,
370 &self.image,
371 self.spacing,
372 self.text_first,
373 );
374
375 let text_color = label_visuals.as_ref().map(|v| v.text).unwrap_or(visuals.text_color());
376
377 ui.painter().add(TextShape::new(
378 text_pos,
379 galley.clone(),
380 text_color,
381 ));
382
383 if let Some(image_rect) = image_rect_opt {
384 if let Some(image) = self.image {
385 image.paint_at(ui, image_rect);
386 }
387 }
388 }
389 response
390 }
391}