Skip to main content

zeus_widgets/
multi_label.rs

1use super::Label;
2use egui::{Pos2, Rect, Response, Sense, StrokeKind, Ui, Vec2, Widget, epaint::RectShape};
3
4/// A widget that displays multiple [`Label`]s in a single horizontal line,
5/// with consistent vertical alignment (centered) and optional spacing between them.
6#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
7#[derive(Clone)]
8pub struct MultiLabel {
9   labels: Vec<Label>,
10   inter_spacing: f32,
11   sense: Option<Sense>,
12}
13
14impl MultiLabel {
15   /// Create a new `MultiLabel` from a vec of [`Label`].
16   /// The default spacing between labels is 6.0.
17   pub fn new(labels: Vec<Label>) -> Self {
18      Self {
19         labels,
20         inter_spacing: 6.0,
21         sense: None,
22      }
23   }
24
25   /// Set the space between each pair of labels.
26   pub fn inter_spacing(mut self, spacing: f32) -> Self {
27      self.inter_spacing = spacing;
28      self
29   }
30
31   /// Make the entire multi-label respond to clicks and/or drags.
32   /// This will use the same visuals for all inner content.
33   pub fn sense(mut self, sense: Sense) -> Self {
34      self.sense = Some(sense);
35      self
36   }
37}
38
39impl Widget for MultiLabel {
40   fn ui(self, ui: &mut Ui) -> Response {
41      if self.labels.is_empty() {
42         return ui.allocate_response(Vec2::ZERO, Sense::empty());
43      }
44
45      // Calculate Intrinsic Sizes (no wrapping)
46      let mut total_width = 0.0;
47      let mut max_height: f32 = 0.0;
48      let mut label_sizes: Vec<Vec2> = Vec::with_capacity(self.labels.len());
49
50      for label in &self.labels {
51         let (_, size) = label.galley_and_size(ui, f32::INFINITY);
52         label_sizes.push(size);
53         total_width += size.x;
54         max_height = max_height.max(size.y);
55      }
56
57      let num_gaps = (self.labels.len() as f32).max(0.0) - 1.0;
58      total_width += self.inter_spacing * num_gaps;
59      let desired_size = Vec2::new(total_width, max_height);
60
61      // Allocate Space
62      let sense = self.sense.unwrap_or(Sense::empty());
63      let (rect, response) = ui.allocate_exact_size(desired_size, sense);
64
65      // Paint
66      if ui.is_rect_visible(rect) {
67         let visuals = if self.sense.is_some() {
68            ui.style().interact(&response).clone()
69         } else {
70            ui.style().noninteractive().clone()
71         };
72
73         // Paint Background
74         let is_interactive = self.sense.is_some();
75         if is_interactive
76            && (response.hovered() || response.is_pointer_button_down_on() || response.has_focus())
77         {
78            let background_rect = rect.expand(visuals.expansion);
79            ui.painter().add(RectShape::new(
80               background_rect,
81               visuals.corner_radius,
82               visuals.weak_bg_fill,
83               visuals.bg_stroke,
84               StrokeKind::Inside,
85            ));
86         }
87
88         // Layout and Paint Each Label
89         let mut x = rect.left();
90         let mut i = 0;
91
92         for label in self.labels {
93            if i > 0 {
94               x += self.inter_spacing;
95            }
96
97            let label_size = label_sizes[i];
98            let y_offset = (max_height - label_size.y) * 0.5;
99            let label_rect = Rect::from_min_size(Pos2::new(x, rect.top() + y_offset), label_size);
100
101            label.paint_content_within_rect(ui, label_rect, &visuals);
102
103            x += label_size.x;
104            i += 1;
105         }
106      }
107      response
108   }
109}