1use crate::controls::MacControlSize;
7use crate::theme::MacTheme;
8use egui::{
9 Color32, CornerRadius, Key, Modifiers, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
10 Vec2,
11};
12
13pub struct MacSegmentedControl<'a> {
30 options: &'a [&'a str],
31 selected_idx: usize,
32 enabled: bool,
33 theme: Option<&'a MacTheme>,
34 height: Option<f32>,
35 control_size: MacControlSize,
36 small_square: bool,
37 fixed_rect: Option<Rect>,
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 enabled: true,
47 theme: None,
48 height: None,
49 control_size: MacControlSize::Regular,
50 small_square: false,
51 fixed_rect: None,
52 }
53 }
54
55 pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; self }
57 pub fn theme(mut self, theme: &'a MacTheme) -> Self { self.theme = Some(theme); self }
59 pub fn height(mut self, height: f32) -> Self { self.height = Some(height); self }
61 pub fn control_size(mut self, size: MacControlSize) -> Self { self.control_size = size; self }
63 pub fn small_square(mut self) -> Self { self.small_square = true; self }
65 pub fn fixed_rect(mut self, rect: Rect) -> Self { self.fixed_rect = Some(rect); self }
67
68 pub fn show_multi(self, ui: &mut Ui, active: &mut [bool]) -> Option<usize> {
71 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
72 let theme: &MacTheme = &theme_cow;
73 let seg = theme.segmented();
74 let (tier, font_sz) = match self.control_size {
75 MacControlSize::Regular | MacControlSize::Large => (&seg.regular, seg.font_size.points()),
76 MacControlSize::Small | MacControlSize::CompactInfobox => (&seg.small, seg.small_font_size.points()),
77 MacControlSize::Mini => (&seg.mini, seg.mini_font_size.points()),
78 };
79 let height = self.fixed_rect.map(|r| r.height()).or(self.height).unwrap_or_else(|| tier.height.points());
80 let corner_radius = if self.small_square {
81 seg.small_square_corner_radius.points()
82 } else {
83 tier.corner_radius.points()
84 };
85 let border_w = seg.border_width.points();
86 let font_id = egui::FontId::proportional(font_sz);
87 let padding_h = seg.padding_horizontal.points();
88 let mut max_content_w = seg.item_width.points();
89 let mut galleys = Vec::with_capacity(self.options.len());
90 for &opt in self.options {
91 let g = theme.layout_tracked_galley(ui, opt, font_id.clone(), Color32::PLACEHOLDER, f32::INFINITY);
92 max_content_w = max_content_w.max(g.size().x + padding_h * 2.0);
93 galleys.push(g);
94 }
95 let count = self.options.len().max(1);
96 let enabled = self.enabled && ui.is_enabled();
97 let sense = if enabled { Sense::click() } else { Sense::hover() };
98 let (rect, mut response) = if let Some(fixed) = self.fixed_rect {
99 let id = ui.next_auto_id().with("mac_segmented_multi_fixed");
100 ui.skip_ahead_auto_ids(1);
101 (fixed, ui.interact(fixed, id, sense))
102 } else {
103 ui.allocate_exact_size(Vec2::new(max_content_w * count as f32, height), sense)
104 };
105 let segment_width = rect.width() / count as f32;
106 let alpha = if enabled { 1.0 } else { theme.disabled_opacity() };
107 let painter = ui.painter_at(rect);
108 let border_stroke = Stroke::new(border_w, theme.box_border().gamma_multiply(alpha));
109 painter.rect(
110 rect,
111 CornerRadius::same(corner_radius as u8),
112 theme.segmented_small_square_inactive_bg().gamma_multiply(alpha),
113 border_stroke,
114 StrokeKind::Inside,
115 );
116
117 let mut toggled = None;
118 for (i, galley) in galleys.into_iter().enumerate() {
119 let seg_rect = Rect::from_min_size(
120 Pos2::new(rect.min.x + i as f32 * segment_width, rect.min.y),
121 Vec2::new(segment_width, rect.height()),
122 );
123 if enabled && response.clicked() {
124 if let Some(pos) = response.interact_pointer_pos() {
125 if seg_rect.contains(pos) {
126 if let Some(slot) = active.get_mut(i) {
127 *slot = !*slot;
128 }
129 toggled = Some(i);
130 response.mark_changed();
131 }
132 }
133 }
134 let is_on = active.get(i).copied().unwrap_or(false);
135 if is_on {
136 painter.rect_filled(
137 seg_rect.shrink(border_w),
138 CornerRadius::ZERO,
139 theme.segmented_small_square_active_bg().gamma_multiply(alpha),
140 );
141 }
142 let text_color = if is_on {
143 theme.label().gamma_multiply(alpha)
144 } else {
145 theme.secondary_label().gamma_multiply(alpha)
146 };
147 let text_pos = egui::Align2::CENTER_CENTER.anchor_size(seg_rect.center(), galley.size()).min;
148 painter.galley(text_pos, galley, text_color);
149 }
150 for i in 1..count {
151 let x = rect.min.x + i as f32 * segment_width;
152 painter.line_segment([Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)], border_stroke);
153 }
154 toggled
155 }
156
157 pub fn show(self, ui: &mut Ui, mut on_select: impl FnMut(usize)) -> Response {
159 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
160 let theme: &MacTheme = &theme_cow;
161 let seg = theme.segmented();
162 let (tier, font_sz) = match self.control_size {
163 MacControlSize::Regular | MacControlSize::Large => (&seg.regular, seg.font_size.points()),
164 MacControlSize::Small | MacControlSize::CompactInfobox => (&seg.small, seg.small_font_size.points()),
165 MacControlSize::Mini => (&seg.mini, seg.mini_font_size.points()),
166 };
167 let height = self.fixed_rect.map(|r| r.height()).or(self.height).unwrap_or_else(|| tier.height.points());
168 let corner_radius = if self.small_square {
169 seg.small_square_corner_radius.points()
170 } else {
171 tier.corner_radius.points()
172 };
173 let indicator_radius = tier.indicator_radius.points();
174 let border_width = seg.border_width.points();
175 let indicator_inset = seg.indicator_inset.points();
176 let font_id = egui::FontId::proportional(font_sz);
177 let padding_h = seg.padding_horizontal.points();
178 let mut max_content_w = seg.item_width.points();
179 let mut galleys = Vec::with_capacity(self.options.len());
180 for &opt in self.options {
181 let galley = theme.layout_tracked_galley(ui, opt, font_id.clone(), Color32::PLACEHOLDER, f32::INFINITY);
182 max_content_w = max_content_w.max(galley.size().x + padding_h * 2.0);
183 galleys.push(galley);
184 }
185 let count = self.options.len().max(1);
186 let enabled = self.enabled && ui.is_enabled();
187 let sense = if enabled { Sense::click() } else { Sense::hover() };
188 let (rect, mut response) = if let Some(fixed) = self.fixed_rect {
189 let id = ui.next_auto_id().with("mac_segmented_fixed");
190 ui.skip_ahead_auto_ids(1);
191 (fixed, ui.interact(fixed, id, sense))
192 } else {
193 ui.allocate_exact_size(Vec2::new(max_content_w * count as f32, height), sense)
194 };
195 let segment_width = rect.width() / count as f32;
196 let mut active_idx = self.selected_idx.min(count.saturating_sub(1));
197
198 if enabled && response.has_focus() {
199 if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowRight)) && active_idx + 1 < count {
200 active_idx += 1;
201 on_select(active_idx);
202 response.mark_changed();
203 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowLeft)) && active_idx > 0 {
204 active_idx -= 1;
205 on_select(active_idx);
206 response.mark_changed();
207 }
208 }
209
210 let alpha_factor = if enabled { 1.0 } else { theme.disabled_opacity() };
211 let painter = ui.painter_at(rect);
212 let bg_fill = if self.small_square {
213 theme.segmented_small_square_inactive_bg()
214 } else {
215 theme.control_background()
216 };
217 let border_col = if self.small_square { theme.box_border() } else { theme.separator() };
218 painter.rect(
219 rect,
220 CornerRadius::same(corner_radius as u8),
221 bg_fill.gamma_multiply(alpha_factor),
222 Stroke::new(border_width, border_col.gamma_multiply(alpha_factor)),
223 StrokeKind::Inside,
224 );
225
226 let spring = theme.motion().swiftui_snappy.to_spring();
227 let anim_idx = spring.animate_f32(ui.ctx(), response.id.with("seg_caspring"), active_idx as f32);
228 let pill_x = rect.min.x + anim_idx * segment_width;
229 let animated_seg_rect = Rect::from_min_size(Pos2::new(pill_x, rect.min.y), Vec2::new(segment_width, rect.height()));
230 if self.small_square {
231 painter.rect_filled(
232 animated_seg_rect.shrink(border_width),
233 CornerRadius::ZERO,
234 theme.segmented_small_square_active_bg().gamma_multiply(alpha_factor),
235 );
236 for i in 1..count {
237 let x = rect.min.x + i as f32 * segment_width;
238 painter.line_segment(
239 [Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)],
240 Stroke::new(border_width, border_col.gamma_multiply(alpha_factor)),
241 );
242 }
243 } else {
244 let pill_rect = animated_seg_rect.shrink(indicator_inset);
245 painter.rect_filled(
246 pill_rect.translate(Vec2::new(0.0, border_width)),
247 CornerRadius::same(indicator_radius as u8),
248 theme.drop_shadow().gamma_multiply(alpha_factor),
249 );
250 painter.rect(
251 pill_rect,
252 CornerRadius::same(indicator_radius as u8),
253 theme.control().gamma_multiply(alpha_factor),
254 Stroke::new(border_width, theme.separator().gamma_multiply(alpha_factor)),
255 StrokeKind::Inside,
256 );
257 }
258
259 for (i, galley) in galleys.into_iter().enumerate() {
260 let seg_rect = Rect::from_min_size(Pos2::new(rect.min.x + i as f32 * segment_width, rect.min.y), Vec2::new(segment_width, rect.height()));
261 if enabled && response.clicked() {
262 if let Some(pos) = response.interact_pointer_pos().or_else(|| ui.input(|inp| inp.pointer.interact_pos())) {
263 if seg_rect.contains(pos) {
264 response.request_focus();
265 on_select(i);
266 response.mark_changed();
267 }
268 }
269 }
270 let is_hovered = enabled && ui.rect_contains_pointer(seg_rect);
271 let text_color = if i == active_idx || is_hovered {
272 theme.label().gamma_multiply(alpha_factor)
273 } else {
274 theme.secondary_label().gamma_multiply(alpha_factor)
275 };
276 let text_pos = egui::Align2::CENTER_CENTER.anchor_size(seg_rect.center(), galley.size()).min;
277 painter.galley(text_pos, galley, text_color);
278 }
279 response
280 }
281}