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 let target = if *checked { 1.0 } else { 0.0 };
139 self.toggle_spring.set_target(target);
140 let dt = ui.input(|i| i.stable_dt);
141 self.toggle_spring.update(dt);
142
143 if !self.toggle_spring.is_settled(0.001, 0.001) {
144 ui.ctx().request_repaint();
145 }
146
147 let response = ui
148 .horizontal(|ui| {
149 let (width, height) = self.size.dimensions();
150 let (rect, mut response) = ui.allocate_exact_size(
151 Vec2::new(width, height),
152 if self.disabled {
153 Sense::hover()
154 } else {
155 Sense::click()
156 },
157 );
158
159 if ui.is_rect_visible(rect) {
160 self.draw(ui, rect, *checked, &theme);
161 }
162
163 if response.clicked() && !self.disabled {
164 *checked = !*checked;
165 response.mark_changed();
166 }
167
168 if self.label.is_some() || self.description.is_some() {
170 ui.add_space(theme.spacing.sm);
171 ui.vertical(|ui| {
172 ui.spacing_mut().item_spacing.y = theme.spacing.xs;
173 if let Some(label) = &self.label {
174 let color = if self.disabled {
175 theme.muted_foreground().linear_multiply(0.5)
176 } else {
177 theme.foreground()
178 };
179 ui.label(
180 egui::RichText::new(label)
181 .size(theme.typography.base)
182 .color(color),
183 );
184 }
185 if let Some(desc) = &self.description {
186 ui.label(
187 egui::RichText::new(desc)
188 .size(theme.typography.sm)
189 .color(theme.muted_foreground()),
190 );
191 }
192 });
193 }
194
195 response
196 })
197 .inner;
198
199 if let Some(id) = self.id {
201 let state_id = id.with("switch_state");
202 ui.ctx().data_mut(|d| {
203 d.insert_temp(state_id, (*checked, self.toggle_spring.value));
204 });
205 }
206
207 SwitchResponse {
208 response,
209 changed: old_checked != *checked,
210 }
211 }
212
213 fn draw(&self, ui: &mut Ui, rect: egui::Rect, checked: bool, theme: &Theme) {
214 let painter = ui.painter();
215 let t = self.toggle_spring.value;
216
217 let bg_color = if self.disabled {
219 theme.muted().gamma_multiply(0.5)
220 } else if checked {
221 theme.primary()
222 } else {
223 theme.input()
224 };
225
226 let track_radius = rect.height() / 2.0;
227 painter.rect_filled(rect, CornerRadius::same(track_radius as u8), bg_color);
228
229 let response = ui.interact(rect, ui.id().with("switch_hover"), Sense::hover());
231 if response.hovered() && !self.disabled {
232 painter.rect_stroke(
233 rect.expand(2.0),
234 CornerRadius::same((track_radius + 2.0) as u8),
235 Stroke::new(2.0, theme.ring()),
236 egui::StrokeKind::Outside,
237 );
238 }
239
240 let thumb_size = self.size.thumb_size();
242 let thumb_radius = thumb_size / 2.0;
243 let thumb_padding = 2.0;
244 let thumb_travel = rect.width() - thumb_size - thumb_padding * 2.0;
245 let thumb_x = rect.min.x + thumb_padding + thumb_radius + thumb_travel * t;
246 let thumb_center = pos2(thumb_x, rect.center().y);
247
248 let thumb_color = if self.disabled {
249 theme.muted_foreground()
250 } else {
251 theme.background()
252 };
253
254 if !self.disabled {
256 painter.circle_filled(
257 thumb_center + vec2(0.0, 1.0),
258 thumb_radius,
259 Color32::from_rgba_unmultiplied(0, 0, 0, 20),
260 );
261 }
262
263 painter.circle_filled(thumb_center, thumb_radius, thumb_color);
264 }
265}
266
267impl Default for Switch {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273pub struct SwitchResponse {
275 pub response: Response,
277 pub changed: bool,
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_switch_creation() {
287 let switch = Switch::new();
288 assert_eq!(switch.size, SwitchSize::Medium);
289 assert!(!switch.disabled);
290 }
291
292 #[test]
293 fn test_switch_builder() {
294 let switch = Switch::new()
295 .size(SwitchSize::Large)
296 .label("Enable feature")
297 .disabled(true);
298
299 assert_eq!(switch.size, SwitchSize::Large);
300 assert_eq!(switch.label, Some("Enable feature".to_string()));
301 assert!(switch.disabled);
302 }
303
304 #[test]
305 fn test_switch_size_dimensions() {
306 assert_eq!(SwitchSize::Small.dimensions(), (36.0, 20.0));
307 assert_eq!(SwitchSize::Medium.dimensions(), (44.0, 24.0));
308 assert_eq!(SwitchSize::Large.dimensions(), (52.0, 28.0));
309 }
310}