Skip to main content

egui_macview/controls/
toggle.rs

1//! Authentic macOS Toggle / Switch.
2//!
3//! Grounded in macOS HIG and W3C DTCG tokens via [`MacTheme`].
4
5use crate::theme::MacTheme;
6use egui::{Color32, CornerRadius, Pos2, Response, Stroke, StrokeKind, Ui, Vec2, Widget};
7
8/// Authentic macOS Toggle / Switch.
9///
10/// Features animated sliding knob, smooth green activation color transition,
11/// ambient drop shadow, and theme-driven metrics.
12///
13/// # Example
14///
15/// ```rust
16/// use egui_macview::{MacTheme, MacToggle};
17///
18/// # let ctx = egui::Context::default();
19/// # let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
20/// let theme = MacTheme::default();
21/// let mut enabled = true;
22///
23/// ui.add(MacToggle::new(&mut enabled).theme(&theme));
24/// # });
25/// # output.textures_delta.clear();
26/// ```
27pub struct MacToggle<'a> {
28    value: &'a mut bool,
29    theme: Option<&'a MacTheme>,
30}
31
32impl<'a> MacToggle<'a> {
33    /// Creates a new toggle switch bound to a mutable `bool` reference.
34    pub fn new(value: &'a mut bool) -> Self {
35        Self { value, theme: None }
36    }
37
38    /// Injects dynamic theme to eliminate hardcoded metrics and colors.
39    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
40        self.theme = Some(theme);
41        self
42    }
43}
44
45impl<'a> Widget for MacToggle<'a> {
46    fn ui(self, ui: &mut Ui) -> Response {
47        let fallback_theme;
48        let theme = match self.theme {
49            Some(t) => t,
50            None => {
51                fallback_theme = MacTheme::default();
52                &fallback_theme
53            }
54        };
55
56        let metrics = theme.toggle();
57        let height = metrics.height.points();
58        let width = metrics.width.points();
59        let radius = metrics.corner_radius.points() as u8;
60
61        let desired_size = Vec2::new(width, height);
62        let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
63        if response.clicked() {
64            *self.value = !*self.value;
65            response.mark_changed();
66        }
67
68        let anim_progress = ui.ctx().animate_bool_with_time(
69            response.id.with("toggle_anim"),
70            *self.value,
71            metrics.animation_duration.points(),
72        );
73
74        let painter = ui.painter_at(rect);
75        let bg_color = theme
76            .control()
77            .lerp_to_gamma(theme.system_green(), anim_progress);
78
79        painter.rect(
80            rect,
81            CornerRadius::same(radius),
82            bg_color,
83            Stroke::new(metrics.border_width.points(), theme.separator()),
84            StrokeKind::Inside,
85        );
86
87        let knob_radius = metrics.knob_size.points() * 0.5;
88        let x_off = rect.min.x + metrics.knob_offset_off.points() + knob_radius;
89        let x_on = rect.min.x + metrics.knob_offset_on.points() + knob_radius;
90        let knob_x = x_off + (x_on - x_off) * anim_progress;
91        let knob_center = Pos2::new(knob_x, rect.center().y);
92
93        // Subtle ambient shadow under knob
94        painter.circle_filled(
95            knob_center + Vec2::new(0.0, 1.0),
96            knob_radius,
97            Color32::from_black_alpha(30),
98        );
99        painter.circle_filled(knob_center, knob_radius, Color32::WHITE);
100
101        response
102    }
103}