egui_material3/
switch.rs

1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
2use crate::get_global_color;
3
4/// Material Design switch component following Material Design 3 specifications
5///
6/// Switches toggle the state of a single item on or off. They are the preferred way to 
7/// adjust settings on mobile and should be used instead of checkboxes in most cases.
8///
9/// ## Usage Examples
10/// ```rust
11/// # egui::__run_test_ui(|ui| {
12/// let mut wifi_enabled = false;
13/// let mut bluetooth_enabled = true;
14/// 
15/// // Basic switch
16/// ui.add(MaterialSwitch::new(&mut wifi_enabled));
17/// 
18/// // Switch with label text
19/// ui.add(MaterialSwitch::new(&mut bluetooth_enabled)
20///     .text("Enable Bluetooth"));
21/// 
22/// // Disabled switch
23/// let mut disabled_option = false;
24/// ui.add(MaterialSwitch::new(&mut disabled_option)
25///     .text("Unavailable option")
26///     .enabled(false));
27/// # });
28/// ```
29///
30/// ## Material Design Spec
31/// - Track width: 52dp, height: 32dp
32/// - Thumb diameter: 24dp (16dp when off)
33/// - Corner radius: 16dp (fully rounded)
34/// - Touch target: 48x48dp minimum
35/// - Colors: Primary when on, outline when off
36/// - Animation: 167ms cubic-bezier transition
37pub struct MaterialSwitch<'a> {
38    /// Mutable reference to the switch state (on/off)
39    selected: &'a mut bool,
40    /// Optional text label displayed next to the switch
41    text: Option<String>,
42    /// Whether the switch is interactive (enabled/disabled)
43    enabled: bool,
44}
45
46impl<'a> MaterialSwitch<'a> {
47    /// Create a new Material Design switch
48    /// 
49    /// ## Parameters
50    /// - `selected`: Mutable reference to boolean state representing on/off
51    /// 
52    /// ## Returns
53    /// A new MaterialSwitch instance with default settings
54    pub fn new(selected: &'a mut bool) -> Self {
55        Self {
56            selected,
57            text: None,
58            enabled: true,
59        }
60    }
61
62    /// Set optional text label for the switch
63    /// 
64    /// The text will be displayed to the right of the switch component.
65    /// 
66    /// ## Parameters
67    /// - `text`: Label text to display next to the switch
68    pub fn text(mut self, text: impl Into<String>) -> Self {
69        self.text = Some(text.into());
70        self
71    }
72
73    /// Set whether the switch is enabled or disabled
74    /// 
75    /// Disabled switches cannot be interacted with and are visually dimmed.
76    /// 
77    /// ## Parameters
78    /// - `enabled`: True for interactive, false for disabled
79    pub fn enabled(mut self, enabled: bool) -> Self {
80        self.enabled = enabled;
81        self
82    }
83}
84
85impl<'a> Widget for MaterialSwitch<'a> {
86    fn ui(self, ui: &mut Ui) -> Response {
87        let switch_width = 52.0;
88        let switch_height = 32.0;
89        
90        let desired_size = if let Some(ref text) = self.text {
91            let text_width = ui.fonts(|fonts| {
92                fonts.glyph_width(&egui::FontId::default(), ' ') * text.len() as f32
93            });
94            Vec2::new(switch_width + 8.0 + text_width, switch_height)
95        } else {
96            Vec2::new(switch_width, switch_height)
97        };
98
99        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
100
101        if response.clicked() && self.enabled {
102            *self.selected = !*self.selected;
103            response.mark_changed();
104        }
105
106        // Material Design colors
107        let primary_color = get_global_color("primary");
108        let on_primary = get_global_color("onPrimary");
109        let surface_variant = get_global_color("surfaceVariant");
110        let on_surface = get_global_color("onSurface");
111        let on_surface_variant = get_global_color("onSurfaceVariant");
112        let outline = get_global_color("outline");
113
114        // Calculate switch area
115        let switch_rect = Rect::from_min_size(
116            Pos2::new(rect.min.x, rect.center().y - switch_height / 2.0),
117            Vec2::new(switch_width, switch_height),
118        );
119
120        let track_height = 14.0;
121        let track_rect = Rect::from_center_size(
122            switch_rect.center(),
123            Vec2::new(switch_width, track_height),
124        );
125
126        let thumb_size = 24.0;
127        let thumb_travel = switch_width - thumb_size - 4.0;
128        let thumb_x = if *self.selected {
129            switch_rect.min.x + 2.0 + thumb_travel
130        } else {
131            switch_rect.min.x + 2.0
132        };
133        
134        let thumb_center = Pos2::new(thumb_x + thumb_size / 2.0, switch_rect.center().y);
135
136        // Determine colors based on state
137        let (track_color, thumb_color, thumb_outline) = if !self.enabled {
138            let disabled_track = get_global_color("surfaceVariant").linear_multiply(0.38);
139            let disabled_thumb = get_global_color("onSurface").linear_multiply(0.38);
140            (disabled_track, disabled_thumb, Color32::TRANSPARENT)
141        } else if *self.selected {
142            if response.hovered() {
143                (
144                    Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 200),
145                    Color32::from_rgba_premultiplied(on_primary.r().saturating_add(20), on_primary.g().saturating_add(20), on_primary.b().saturating_add(20), 255),
146                    Color32::TRANSPARENT,
147                )
148            } else {
149                (primary_color, on_primary, Color32::TRANSPARENT)
150            }
151        } else {
152            if response.hovered() {
153                (
154                    Color32::from_rgba_premultiplied(surface_variant.r(), surface_variant.g(), surface_variant.b(), 200),
155                    Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 200),
156                    outline,
157                )
158            } else {
159                (surface_variant, on_surface_variant, outline)
160            }
161        };
162
163        // Draw track
164        ui.painter().rect_filled(
165            track_rect,
166            track_height / 2.0,
167            track_color,
168        );
169
170        // Draw thumb
171        ui.painter().circle_filled(
172            thumb_center,
173            thumb_size / 2.0,
174            thumb_color,
175        );
176
177        // Draw thumb outline if needed
178        if thumb_outline != Color32::TRANSPARENT {
179            ui.painter().circle_stroke(
180                thumb_center,
181                thumb_size / 2.0,
182                Stroke::new(2.0, thumb_outline),
183            );
184        }
185
186        // Add ripple effect on hover
187        if response.hovered() && self.enabled {
188            let ripple_color = if *self.selected {
189                Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 30)
190            } else {
191                Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 30)
192            };
193            
194            ui.painter().circle_filled(
195                thumb_center,
196                thumb_size / 2.0 + 12.0,
197                ripple_color,
198            );
199        }
200
201        // Draw label text
202        if let Some(ref text) = self.text {
203            let text_pos = Pos2::new(
204                switch_rect.max.x + 8.0,
205                rect.center().y,
206            );
207            
208            let text_color = if self.enabled { on_surface } else {
209                get_global_color("onSurface").linear_multiply(0.38)
210            };
211
212            ui.painter().text(
213                text_pos,
214                egui::Align2::LEFT_CENTER,
215                text,
216                egui::FontId::default(),
217                text_color,
218            );
219        }
220
221        response
222    }
223}
224
225pub fn switch(selected: &mut bool) -> MaterialSwitch<'_> {
226    MaterialSwitch::new(selected)
227}