zeus_widgets/
label.rs

1use egui::{
2   Align, Color32, FontSelection, Image, Pos2, Rect, Response, Sense, Stroke, StrokeKind,
3   TextWrapMode, Ui, Vec2, Widget, WidgetText,
4   epaint::{RectShape, TextShape},
5   style::WidgetVisuals,
6   text::LayoutJob,
7};
8use std::sync::Arc;
9
10#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
11#[derive(Clone)]
12pub struct Label {
13   text: WidgetText,
14   pub(crate) image: Option<Image<'static>>,
15   spacing: f32,
16   expansion: Option<f32>,
17   pub(crate) sense: Option<Sense>,
18   wrap_mode: Option<TextWrapMode>,
19   selectable: Option<bool>,
20   text_first: bool,
21   selected: bool,
22   interactive: bool,
23   fill_width: bool,
24}
25impl Label {
26   /// Create a new `Label` with text and an optional image.
27   /// By default the image is shown after the text
28   pub fn new(text: impl Into<WidgetText>, image: Option<Image<'static>>) -> Self {
29      Self {
30         text: text.into(),
31         image,
32         spacing: 6.0,
33         expansion: None,
34         sense: None,
35         wrap_mode: None,
36         selectable: None,
37         text_first: true,
38         selected: false,
39         interactive: true,
40         fill_width: false,
41      }
42   }
43
44   /// Set the space between the text and the image.
45   pub fn spacing(mut self, spacing: f32) -> Self {
46      self.spacing = spacing;
47      self
48   }
49
50   pub fn expand(mut self, expansion: Option<f32>) -> Self {
51      self.expansion = expansion;
52      self
53   }
54
55   /// Make the label respond to clicks and/or drags.
56   /// This will also turn the `selectable` to false
57   pub fn sense(mut self, sense: Sense) -> Self {
58      self.sense = Some(sense);
59      self.selectable = Some(false);
60      self
61   }
62
63   /// Make the label interactive
64   /// 
65   /// True by default, If false on hover/click there will be no bg color
66   pub fn interactive(mut self, interactive: bool) -> Self {
67      self.interactive = interactive;
68      self
69   }
70
71   pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
72      self.wrap_mode = Some(wrap_mode);
73      self
74   }
75
76   pub fn wrap(mut self) -> Self {
77      self.wrap_mode = Some(TextWrapMode::Wrap);
78      self
79   }
80
81   /// Set whether the text can be selected with the mouse.
82   pub fn selectable(mut self, selectable: bool) -> Self {
83      self.selectable = Some(selectable);
84      self
85   }
86
87   /// Show the image first and then the text
88   pub fn image_on_left(mut self) -> Self {
89      self.text_first = false;
90      self
91   }
92
93   pub fn selected(mut self, selected: bool) -> Self {
94      self.selected = selected;
95      self
96   }
97
98   pub fn fill_width(mut self, fill: bool) -> Self {
99      self.fill_width = fill;
100      self
101   }
102
103   /// Calculate the size needed by the widget.
104   ///
105   /// `available_width` is the width available *for the text part* after accounting for image/spacing.
106   pub fn galley_and_size(
107      &self,
108      ui: &Ui,
109      available_width_for_text: f32,
110   ) -> (Arc<egui::Galley>, Vec2) {
111      let layout_job = self.prepare_layout_job(ui, available_width_for_text);
112      let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
113      let text_size = galley.size();
114
115      let image_size = if let Some(image) = &self.image {
116         image.calc_size(ui.available_size(), image.size())
117      } else {
118         Vec2::ZERO
119      };
120
121      let total_width = text_size.x
122         + if self.image.is_some() {
123            self.spacing + image_size.x
124         } else {
125            0.0
126         };
127
128      let total_height = text_size.y.max(image_size.y);
129      (galley, Vec2::new(total_width, total_height))
130   }
131
132   fn prepare_layout_job(&self, ui: &Ui, wrap_width: f32) -> LayoutJob {
133      let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
134      let layout_job = self.text.clone().into_layout_job(
135         ui.style(),
136         FontSelection::Default,
137         ui.text_valign(),
138      );
139
140      // remove the Arc
141      let mut layout_job: LayoutJob = (*layout_job).clone();
142      match wrap_mode {
143         TextWrapMode::Extend => {
144            layout_job.wrap.max_width = f32::INFINITY;
145         }
146         TextWrapMode::Wrap => {
147            layout_job.wrap.max_width = wrap_width;
148         }
149         TextWrapMode::Truncate => {
150            layout_job.wrap.max_width = wrap_width;
151            layout_job.wrap.max_rows = 1;
152            layout_job.wrap.break_anywhere = true;
153         }
154      }
155
156      layout_job.halign = Align::LEFT;
157      layout_job
158   }
159   pub(crate) fn paint_content_within_rect(
160      &self,
161      ui: &mut Ui,
162      rect: Rect,
163      button_visuals: &WidgetVisuals,
164   ) {
165      // Estimate available width for text layout within the provided rect
166      let available_width_for_text = if self.image.is_some() {
167         (rect.width()
168            - self.image.as_ref().map_or(0.0, |img| img.size().map_or(0.0, |s| s.x))
169            - self.spacing)
170            .max(0.0)
171      } else {
172         rect.width()
173      };
174
175      // Calculate galley based on available width
176      let (galley, _) = self.galley_and_size(ui, available_width_for_text);
177      if ui.is_rect_visible(rect) {
178         let (text_pos, image_rect_opt) = layout_content_within_rect(
179            ui,
180            rect,
181            &galley,
182            &self.image,
183            self.spacing,
184            self.text_first,
185         );
186
187         let text_color = button_visuals.text_color();
188         ui.painter().add(TextShape::new(
189            text_pos,
190            galley.clone(),
191            text_color,
192         ));
193
194         if let Some(image_rect) = image_rect_opt {
195            if let Some(image) = &self.image {
196               image.paint_at(ui, image_rect);
197            }
198         }
199      }
200   }
201}
202impl Widget for Label {
203   fn ui(self, ui: &mut Ui) -> Response {
204      // Calculate Size (Content Only)
205      let image_size = if let Some(image) = &self.image {
206         image.calc_size(ui.available_size(), image.size())
207      } else {
208         Vec2::ZERO
209      };
210
211      let available_width_for_text = if self.fill_width {
212         if self.image.is_some() {
213            (ui.available_width() - self.spacing - image_size.x).max(10.0)
214         } else {
215            ui.available_width()
216         }
217      } else {
218         f32::INFINITY
219      };
220
221      let (galley, content_size) = self.galley_and_size(ui, available_width_for_text);
222      let desired_size = if self.fill_width {
223         Vec2::new(ui.available_width(), content_size.y)
224      } else {
225         content_size
226      };
227
228      // Allocate Space (Content Size Only)
229      let sense = self.sense.unwrap_or(Sense::hover());
230      let (rect, response) = ui.allocate_exact_size(desired_size, sense);
231
232      // Paint
233      if ui.is_rect_visible(rect) {
234         let mut visuals = ui.style().interact_selectable(&response, self.selected);
235
236         if self.selected {
237            visuals.weak_bg_fill = visuals.bg_fill;
238         }
239
240         let fill = if self.selected {
241            visuals.bg_fill
242         } else if response.hovered() || response.has_focus() {
243            match self.interactive {
244               true => visuals.weak_bg_fill,
245               false => Color32::TRANSPARENT,
246            }
247         } else {
248            Color32::TRANSPARENT
249         };
250
251         let stroke = if self.selected || response.hovered() || response.has_focus() {
252            visuals.bg_stroke
253         } else {
254            Stroke::NONE
255         };
256
257         let expansion = self.expansion.unwrap_or(visuals.expansion);
258         let background_rect = rect.expand(expansion);
259         ui.painter().add(RectShape::new(
260            background_rect,
261            visuals.corner_radius,
262            fill,
263            stroke,
264            StrokeKind::Inside,
265         ));
266
267         // Layout and Paint Content
268         let (text_pos, image_rect_opt) = layout_content_within_rect(
269            ui,
270            rect,
271            &galley,
272            &self.image,
273            self.spacing,
274            self.text_first,
275         );
276
277         let text_color = visuals.text_color();
278         ui.painter().add(TextShape::new(
279            text_pos,
280            galley.clone(),
281            text_color,
282         ));
283
284         if let Some(image_rect) = image_rect_opt {
285            if let Some(image) = self.image {
286               image.paint_at(ui, image_rect);
287            }
288         }
289      }
290      response
291   }
292}
293fn layout_content_within_rect(
294   ui: &Ui,
295   rect: Rect,
296   galley: &egui::Galley,
297   image: &Option<Image<'static>>,
298   spacing: f32,
299   text_first: bool,
300) -> (Pos2, Option<Rect>) {
301   let text_size = galley.size();
302   let image_size = if let Some(image) = image {
303      image.calc_size(ui.available_size(), image.size())
304   } else {
305      Vec2::ZERO
306   };
307
308   let total_content_height = text_size.y.max(image_size.y);
309   let top_y = ui
310      .layout()
311      .align_size_within_rect(Vec2::new(0.0, total_content_height), rect)
312      .min
313      .y;
314
315   let (text_start_x, image_final_rect) = if text_first {
316      // Text first, image second
317      let text_start_x = rect.left();
318      let image_start_x = text_start_x + text_size.x + spacing;
319      let image_final_rect = image.as_ref().map(|_| {
320         let image_pos = Pos2::new(
321            image_start_x,
322            top_y + (total_content_height - image_size.y) * 0.5,
323         );
324         Rect::from_min_size(image_pos, image_size)
325      });
326      (text_start_x, image_final_rect)
327   } else {
328      // Image first, text second
329      let image_start_x = rect.left();
330      let text_start_x = image_start_x + image_size.x + spacing;
331      let image_final_rect = image.as_ref().map(|_| {
332         let image_pos = Pos2::new(
333            image_start_x,
334            top_y + (total_content_height - image_size.y) * 0.5,
335         );
336         Rect::from_min_size(image_pos, image_size)
337      });
338      (text_start_x, image_final_rect)
339   };
340
341   let text_pos = Pos2::new(
342      text_start_x,
343      top_y + (total_content_height - text_size.y) * 0.5,
344   );
345   (text_pos, image_final_rect)
346}