1use crate::animation::SpringAnimation;
10use crate::ext::ArmasContextExt;
11use crate::Theme;
12use egui::{pos2, vec2, Color32, CornerRadius, Response, Sense, Stroke, Ui, Vec2};
13
14const SWITCH_WIDTH: f32 = 44.0; const SWITCH_HEIGHT: f32 = 24.0; const SWITCH_THUMB_SIZE: f32 = 20.0; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SwitchSize {
22 Small,
24 Medium,
26 Large,
28}
29
30impl SwitchSize {
31 const fn dimensions(self) -> (f32, f32) {
32 match self {
33 Self::Small => (36.0, 20.0),
34 Self::Medium => (SWITCH_WIDTH, SWITCH_HEIGHT),
35 Self::Large => (52.0, 28.0),
36 }
37 }
38
39 const fn thumb_size(self) -> f32 {
40 match self {
41 Self::Small => 16.0,
42 Self::Medium => SWITCH_THUMB_SIZE,
43 Self::Large => 24.0,
44 }
45 }
46}
47
48pub struct Switch {
63 id: Option<egui::Id>,
64 size: SwitchSize,
65 label: Option<String>,
66 description: Option<String>,
67 disabled: bool,
68 toggle_spring: SpringAnimation,
69}
70
71impl Switch {
72 #[must_use]
74 pub const fn new() -> Self {
75 Self {
76 id: None,
77 size: SwitchSize::Medium,
78 label: None,
79 description: None,
80 disabled: false,
81 toggle_spring: SpringAnimation::new(0.0, 0.0).params(800.0, 30.0),
82 }
83 }
84
85 #[must_use]
87 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
88 self.id = Some(id.into());
89 self
90 }
91
92 #[must_use]
94 pub const fn size(mut self, size: SwitchSize) -> Self {
95 self.size = size;
96 self
97 }
98
99 #[must_use]
101 pub fn label(mut self, label: impl Into<String>) -> Self {
102 self.label = Some(label.into());
103 self
104 }
105
106 #[must_use]
108 pub fn description(mut self, description: impl Into<String>) -> Self {
109 self.description = Some(description.into());
110 self
111 }
112
113 #[must_use]
115 pub const fn disabled(mut self, disabled: bool) -> Self {
116 self.disabled = disabled;
117 self
118 }
119
120 pub fn show(&mut self, ui: &mut Ui, checked: &mut bool) -> SwitchResponse {
122 let theme = ui.ctx().armas_theme();
123
124 if let Some(id) = self.id {
126 let state_id = id.with("switch_state");
127 let (stored_checked, stored_anim): (bool, f32) = ui.ctx().data_mut(|d| {
128 d.get_temp(state_id)
129 .unwrap_or((*checked, if *checked { 1.0 } else { 0.0 }))
130 });
131 *checked = stored_checked;
132 self.toggle_spring.value = stored_anim;
133 }
134
135 let old_checked = *checked;
136
137 if self.id.is_none() {
140 self.toggle_spring.value = if *checked { 1.0 } else { 0.0 };
141 }
142
143 let target = if *checked { 1.0 } else { 0.0 };
144 self.toggle_spring.set_target(target);
145 let dt = ui.input(|i| i.stable_dt);
146 self.toggle_spring.update(dt);
147
148 if !self.toggle_spring.is_settled(0.001, 0.001) {
149 ui.ctx().request_repaint();
150 }
151
152 let response = ui
153 .horizontal(|ui| {
154 let (width, height) = self.size.dimensions();
155 let (rect, mut response) = ui.allocate_exact_size(
156 Vec2::new(width, height),
157 if self.disabled {
158 Sense::hover()
159 } else {
160 Sense::click()
161 },
162 );
163
164 if ui.is_rect_visible(rect) {
165 self.draw(ui, rect, *checked, &theme);
166 }
167
168 if response.clicked() && !self.disabled {
169 *checked = !*checked;
170 response.mark_changed();
171 }
172
173 if self.label.is_some() || self.description.is_some() {
175 ui.add_space(theme.spacing.sm);
176 ui.vertical(|ui| {
177 ui.spacing_mut().item_spacing.y = theme.spacing.xs;
178 if let Some(label) = &self.label {
179 let color = if self.disabled {
180 theme.muted_foreground().linear_multiply(0.5)
181 } else {
182 theme.foreground()
183 };
184 ui.label(
185 egui::RichText::new(label)
186 .size(theme.typography.base)
187 .color(color),
188 );
189 }
190 if let Some(desc) = &self.description {
191 ui.label(
192 egui::RichText::new(desc)
193 .size(theme.typography.sm)
194 .color(theme.muted_foreground()),
195 );
196 }
197 });
198 }
199
200 response
201 })
202 .inner;
203
204 if let Some(id) = self.id {
206 let state_id = id.with("switch_state");
207 ui.ctx().data_mut(|d| {
208 d.insert_temp(state_id, (*checked, self.toggle_spring.value));
209 });
210 }
211
212 SwitchResponse {
213 response,
214 changed: old_checked != *checked,
215 }
216 }
217
218 fn draw(&self, ui: &mut Ui, rect: egui::Rect, checked: bool, theme: &Theme) {
219 let painter = ui.painter();
220 let t = self.toggle_spring.value;
221
222 let bg_color = if self.disabled {
224 theme.muted().gamma_multiply(0.5)
225 } else if checked {
226 theme.primary()
227 } else {
228 theme.input()
229 };
230
231 let track_radius = rect.height() / 2.0;
232 painter.rect_filled(rect, CornerRadius::same(track_radius as u8), bg_color);
233
234 let response = ui.interact(rect, ui.id().with("switch_hover"), Sense::hover());
236 if response.hovered() && !self.disabled {
237 painter.rect_stroke(
238 rect.expand(2.0),
239 CornerRadius::same((track_radius + 2.0) as u8),
240 Stroke::new(2.0, theme.ring()),
241 egui::StrokeKind::Outside,
242 );
243 }
244
245 let thumb_size = self.size.thumb_size();
247 let thumb_radius = thumb_size / 2.0;
248 let thumb_padding = 2.0;
249 let thumb_travel = rect.width() - thumb_size - thumb_padding * 2.0;
250 let thumb_x = rect.min.x + thumb_padding + thumb_radius + thumb_travel * t;
251 let thumb_center = pos2(thumb_x, rect.center().y);
252
253 let thumb_color = if self.disabled {
254 theme.muted_foreground()
255 } else {
256 theme.foreground()
257 };
258
259 if !self.disabled {
261 painter.circle_filled(
262 thumb_center + vec2(0.0, 1.0),
263 thumb_radius,
264 Color32::from_rgba_unmultiplied(0, 0, 0, 20),
265 );
266 }
267
268 painter.circle_filled(thumb_center, thumb_radius, thumb_color);
269 }
270}
271
272impl Default for Switch {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278pub struct SwitchResponse {
280 pub response: Response,
282 pub changed: bool,
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_switch_creation() {
292 let switch = Switch::new();
293 assert_eq!(switch.size, SwitchSize::Medium);
294 assert!(!switch.disabled);
295 }
296
297 #[test]
298 fn test_switch_builder() {
299 let switch = Switch::new()
300 .size(SwitchSize::Large)
301 .label("Enable feature")
302 .disabled(true);
303
304 assert_eq!(switch.size, SwitchSize::Large);
305 assert_eq!(switch.label, Some("Enable feature".to_string()));
306 assert!(switch.disabled);
307 }
308
309 #[test]
310 fn test_switch_size_dimensions() {
311 assert_eq!(SwitchSize::Small.dimensions(), (36.0, 20.0));
312 assert_eq!(SwitchSize::Medium.dimensions(), (44.0, 24.0));
313 assert_eq!(SwitchSize::Large.dimensions(), (52.0, 28.0));
314 }
315}