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::{CornerRadius, Pos2, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget};
7
8/// Authentic macOS Toggle / Switch.
9///
10/// Features animated sliding knob via `CASpring`, 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    enabled: bool,
30    theme: Option<&'a MacTheme>,
31}
32
33impl<'a> MacToggle<'a> {
34    /// Creates a new toggle switch bound to a mutable `bool` reference.
35    pub fn new(value: &'a mut bool) -> Self {
36        Self {
37            value,
38            enabled: true,
39            theme: None,
40        }
41    }
42
43    /// Enables or disables the toggle switch.
44    pub fn enabled(mut self, enabled: bool) -> Self {
45        self.enabled = enabled;
46        self
47    }
48
49    /// Injects dynamic theme to eliminate hardcoded metrics and colors.
50    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
51        self.theme = Some(theme);
52        self
53    }
54}
55
56impl<'a> Widget for MacToggle<'a> {
57    fn ui(self, ui: &mut Ui) -> Response {
58        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
59        let theme: &MacTheme = &theme_cow;
60
61        let metrics = theme.toggle();
62        let height = metrics.height.points();
63        let width = metrics.width.points();
64        let radius = metrics.corner_radius.points() as u8;
65
66        let desired_size = Vec2::new(width, height);
67        let sense = if self.enabled {
68            Sense::click()
69        } else {
70            Sense::hover()
71        };
72        let (rect, mut response) = ui.allocate_exact_size(desired_size, sense);
73        if self.enabled && response.clicked() {
74            *self.value = !*self.value;
75            response.mark_changed();
76        }
77
78        // CASpring thumb interpolation stored in egui temp memory
79        let spring = theme.motion().swiftui_bouncy.to_spring();
80        let spring_key = response.id.with("toggle_caspring");
81        let target_val = if *self.value { 1.0f32 } else { 0.0f32 };
82        let anim_progress = spring
83            .animate_f32(ui.ctx(), spring_key, target_val)
84            .clamp(0.0, 1.0);
85
86        let alpha_factor = if self.enabled {
87            1.0
88        } else {
89            theme.disabled_opacity()
90        };
91        let painter = ui.painter_at(rect);
92        let bg_color = theme
93            .control()
94            .lerp_to_gamma(theme.system_green(), anim_progress)
95            .gamma_multiply(alpha_factor);
96
97        painter.rect(
98            rect,
99            CornerRadius::same(radius),
100            bg_color,
101            Stroke::new(
102                metrics.border_width.points(),
103                theme.separator().gamma_multiply(alpha_factor),
104            ),
105            StrokeKind::Inside,
106        );
107
108        let knob_radius = metrics.knob_size.points() * 0.5;
109        let x_off = rect.min.x + metrics.knob_offset_off.points() + knob_radius;
110        let x_on = rect.min.x + metrics.knob_offset_on.points() + knob_radius;
111        let knob_x = x_off + (x_on - x_off) * anim_progress;
112        let knob_center = Pos2::new(knob_x, rect.center().y);
113
114        painter.circle_filled(
115            knob_center + Vec2::new(0.0, metrics.border_width.points()),
116            knob_radius,
117            theme.drop_shadow().gamma_multiply(alpha_factor),
118        );
119        painter.circle_filled(
120            knob_center,
121            knob_radius,
122            theme.knob_fill().gamma_multiply(alpha_factor),
123        );
124
125        response
126    }
127}