egui_macview/controls/
segmented.rs1use crate::theme::MacTheme;
7use egui::{
8 Color32, CornerRadius, Key, Modifiers, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
9 Vec2,
10};
11
12pub struct MacSegmentedControl<'a> {
37 options: &'a [&'a str],
38 selected_idx: usize,
39 enabled: bool,
40 theme: Option<&'a MacTheme>,
41 height: Option<f32>,
42}
43
44impl<'a> MacSegmentedControl<'a> {
45 pub fn new(options: &'a [&'a str], selected_idx: usize) -> Self {
47 Self {
48 options,
49 selected_idx,
50 enabled: true,
51 theme: None,
52 height: None,
53 }
54 }
55
56 pub fn enabled(mut self, enabled: bool) -> Self {
58 self.enabled = enabled;
59 self
60 }
61
62 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
64 self.theme = Some(theme);
65 self
66 }
67
68 pub fn height(mut self, height: f32) -> Self {
70 self.height = Some(height);
71 self
72 }
73
74 pub fn show(self, ui: &mut Ui, mut on_select: impl FnMut(usize)) -> Response {
76 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
77 let theme: &MacTheme = &theme_cow;
78
79 let segmented_tokens = theme.segmented();
80 let metrics = &segmented_tokens.regular;
81 let height = self.height.unwrap_or_else(|| metrics.height.points());
82 let corner_radius = metrics.corner_radius.points();
83 let indicator_radius = metrics.indicator_radius.points();
84 let border_width = segmented_tokens.border_width.points();
85 let indicator_inset = segmented_tokens.indicator_inset.points();
86
87 let font_id = egui::FontId::proportional(segmented_tokens.font_size.points());
88 let padding_h = segmented_tokens.padding_horizontal.points();
89 let min_item_w = segmented_tokens.item_width.points();
90
91 let mut max_content_w = min_item_w;
92 let mut galleys = Vec::with_capacity(self.options.len());
93 for &opt in self.options {
94 let galley = theme.layout_tracked_galley(
95 ui,
96 opt,
97 font_id.clone(),
98 Color32::PLACEHOLDER,
99 f32::INFINITY,
100 );
101 let padded_w = galley.size().x + padding_h * 2.0;
102 if padded_w > max_content_w {
103 max_content_w = padded_w;
104 }
105 galleys.push(galley);
106 }
107 let segment_width = max_content_w;
108 let count = self.options.len().max(1);
109 let total_width = segment_width * count as f32;
110
111 let enabled = self.enabled && ui.is_enabled();
112 let sense = if enabled {
113 Sense::click()
114 } else {
115 Sense::hover()
116 };
117 let (rect, mut response) = ui.allocate_exact_size(Vec2::new(total_width, height), sense);
118 let mut active_idx = self.selected_idx.min(count.saturating_sub(1));
119
120 if enabled && response.has_focus() {
121 if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowRight))
122 && active_idx + 1 < count
123 {
124 active_idx += 1;
125 on_select(active_idx);
126 response.mark_changed();
127 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowLeft))
128 && active_idx > 0
129 {
130 active_idx -= 1;
131 on_select(active_idx);
132 response.mark_changed();
133 }
134 }
135
136 let alpha_factor = if enabled {
137 1.0
138 } else {
139 theme.disabled_opacity()
140 };
141 let painter = ui.painter_at(rect);
142
143 painter.rect(
144 rect,
145 CornerRadius::same(corner_radius as u8),
146 theme.control_background().gamma_multiply(alpha_factor),
147 Stroke::new(border_width, theme.separator().gamma_multiply(alpha_factor)),
148 StrokeKind::Inside,
149 );
150
151 let spring = theme.motion().swiftui_snappy.to_spring();
153 let spring_key = response.id.with("seg_caspring");
154 let target_idx = active_idx as f32;
155 let anim_idx = spring.animate_f32(ui.ctx(), spring_key, target_idx);
156
157 let pill_x = rect.min.x + anim_idx * segment_width;
158 let animated_seg_rect = Rect::from_min_size(
159 Pos2::new(pill_x, rect.min.y),
160 Vec2::new(segment_width, height),
161 );
162 let pill_rect = animated_seg_rect.shrink(indicator_inset);
163
164 painter.rect_filled(
165 pill_rect.translate(Vec2::new(0.0, border_width)),
166 CornerRadius::same(indicator_radius as u8),
167 theme.drop_shadow().gamma_multiply(alpha_factor),
168 );
169
170 painter.rect(
171 pill_rect,
172 CornerRadius::same(indicator_radius as u8),
173 theme.control().gamma_multiply(alpha_factor),
174 Stroke::new(border_width, theme.separator().gamma_multiply(alpha_factor)),
175 StrokeKind::Inside,
176 );
177
178 for (i, galley) in galleys.into_iter().enumerate() {
179 let seg_rect = Rect::from_min_size(
180 Pos2::new(rect.min.x + i as f32 * segment_width, rect.min.y),
181 Vec2::new(segment_width, height),
182 );
183
184 if enabled && response.clicked() {
185 if let Some(pos) = ui.input(|inp| inp.pointer.interact_pos()) {
186 if seg_rect.contains(pos) {
187 response.request_focus();
188 on_select(i);
189 response.mark_changed();
190 }
191 }
192 }
193
194 let is_hovered = enabled && ui.rect_contains_pointer(seg_rect);
195 let text_color = if i == active_idx || is_hovered {
196 theme.label().gamma_multiply(alpha_factor)
197 } else {
198 theme.secondary_label().gamma_multiply(alpha_factor)
199 };
200
201 let text_pos = egui::Align2::CENTER_CENTER
202 .anchor_size(seg_rect.center(), galley.size())
203 .min;
204 painter.galley(text_pos, galley, text_color);
205 }
206
207 response
208 }
209}