1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
2use crate::get_global_color;
3
4pub struct MaterialSwitch<'a> {
38 selected: &'a mut bool,
40 text: Option<String>,
42 enabled: bool,
44}
45
46impl<'a> MaterialSwitch<'a> {
47 pub fn new(selected: &'a mut bool) -> Self {
55 Self {
56 selected,
57 text: None,
58 enabled: true,
59 }
60 }
61
62 pub fn text(mut self, text: impl Into<String>) -> Self {
69 self.text = Some(text.into());
70 self
71 }
72
73 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 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 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 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 ui.painter().rect_filled(
165 track_rect,
166 track_height / 2.0,
167 track_color,
168 );
169
170 ui.painter().circle_filled(
172 thumb_center,
173 thumb_size / 2.0,
174 thumb_color,
175 );
176
177 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 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 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}