egui_macview/controls/
segmented.rs1use crate::theme::MacTheme;
7use egui::{Color32, CornerRadius, Pos2, Rect, Response, Stroke, StrokeKind, Ui, Vec2};
8
9pub struct MacSegmentedControl<'a> {
34 options: &'a [&'a str],
35 selected_idx: usize,
36 theme: Option<&'a MacTheme>,
37 height: Option<f32>,
38}
39
40impl<'a> MacSegmentedControl<'a> {
41 pub fn new(options: &'a [&'a str], selected_idx: usize) -> Self {
43 Self {
44 options,
45 selected_idx,
46 theme: None,
47 height: None,
48 }
49 }
50
51 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
53 self.theme = Some(theme);
54 self
55 }
56
57 pub fn height(mut self, height: f32) -> Self {
59 self.height = Some(height);
60 self
61 }
62
63 pub fn show(self, ui: &mut Ui, mut on_select: impl FnMut(usize)) -> Response {
65 let fallback_theme;
66 let theme = match self.theme {
67 Some(t) => t,
68 None => {
69 fallback_theme = MacTheme::default();
70 &fallback_theme
71 }
72 };
73
74 let segmented_tokens = theme.segmented();
75 let metrics = &segmented_tokens.regular;
76 let height = self.height.unwrap_or_else(|| metrics.height.points());
77 let corner_radius = metrics.corner_radius.points();
78 let indicator_radius = metrics.indicator_radius.points();
79 let border_width = segmented_tokens.border_width.points();
80 let indicator_inset = segmented_tokens.indicator_inset.points();
81
82 let font_id = egui::FontId::proportional(segmented_tokens.font_size.points());
83 let padding_h = segmented_tokens.padding_horizontal.points();
84 let min_item_w = segmented_tokens.item_width.points();
85
86 let mut max_content_w = min_item_w;
87 for opt in self.options.iter() {
88 let galley = ui.painter().layout_no_wrap(
89 (*opt).to_string(),
90 font_id.clone(),
91 egui::Color32::WHITE,
92 );
93 let label_w = galley.size().x;
94 let padded_w = label_w + padding_h + padding_h;
95 if padded_w > max_content_w {
96 max_content_w = padded_w;
97 }
98 }
99 let segment_width = max_content_w;
100 let count = self.options.len().max(1);
101 let total_width = segment_width * count as f32;
102
103 let (rect, response) =
104 ui.allocate_exact_size(Vec2::new(total_width, height), egui::Sense::click());
105
106 let painter = ui.painter_at(rect);
107
108 painter.rect(
110 rect,
111 CornerRadius::same(corner_radius as u8),
112 theme.control_background(),
113 Stroke::new(border_width, theme.separator()),
114 StrokeKind::Inside,
115 );
116
117 let anim_idx = ui.ctx().animate_value_with_time(
119 response.id.with("seg_anim"),
120 self.selected_idx as f32,
121 segmented_tokens.animation_duration.points(),
122 );
123
124 let pill_x = rect.min.x + anim_idx * segment_width;
125 let animated_seg_rect = Rect::from_min_size(
126 Pos2::new(pill_x, rect.min.y),
127 Vec2::new(segment_width, height),
128 );
129 let pill_rect = animated_seg_rect.shrink(indicator_inset);
130
131 painter.rect_filled(
133 pill_rect.translate(Vec2::new(0.0, 1.0)),
134 CornerRadius::same(indicator_radius as u8),
135 Color32::from_black_alpha(20),
136 );
137
138 painter.rect(
139 pill_rect,
140 CornerRadius::same(indicator_radius as u8),
141 theme.control(),
142 Stroke::new(border_width, theme.separator()),
143 StrokeKind::Inside,
144 );
145
146 for (i, opt) in self.options.iter().enumerate() {
147 let seg_rect = Rect::from_min_size(
148 Pos2::new(rect.min.x + i as f32 * segment_width, rect.min.y),
149 Vec2::new(segment_width, height),
150 );
151
152 if response.clicked() {
154 if let Some(pos) = ui.input(|inp| inp.pointer.interact_pos()) {
155 if seg_rect.contains(pos) {
156 on_select(i);
157 }
158 }
159 }
160
161 let text_color = if i == self.selected_idx {
162 theme.label()
163 } else {
164 theme.secondary_label()
165 };
166
167 painter.text(
168 seg_rect.center(),
169 egui::Align2::CENTER_CENTER,
170 *opt,
171 font_id.clone(),
172 text_color,
173 );
174 }
175
176 response
177 }
178}