1use crate::get_global_color;
2use eframe::egui::{self, Color32, FontId, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget};
3
4pub struct MaterialSwitch<'a> {
40 selected: &'a mut bool,
42 text: Option<String>,
44 enabled: bool,
46 selected_icon: Option<char>,
48 unselected_icon: Option<char>,
50 show_track_outline: bool,
52}
53
54impl<'a> MaterialSwitch<'a> {
55 pub fn new(selected: &'a mut bool) -> Self {
63 Self {
64 selected,
65 text: None,
66 enabled: true,
67 selected_icon: None,
68 unselected_icon: None,
69 show_track_outline: true, }
71 }
72
73 pub fn text(mut self, text: impl Into<String>) -> Self {
80 self.text = Some(text.into());
81 self
82 }
83
84 pub fn enabled(mut self, enabled: bool) -> Self {
91 self.enabled = enabled;
92 self
93 }
94
95 pub fn selected_icon(mut self, icon: char) -> Self {
100 self.selected_icon = Some(icon);
101 self
102 }
103
104 pub fn unselected_icon(mut self, icon: char) -> Self {
109 self.unselected_icon = Some(icon);
110 self
111 }
112
113 pub fn with_icons(mut self, selected: char, unselected: char) -> Self {
119 self.selected_icon = Some(selected);
120 self.unselected_icon = Some(unselected);
121 self
122 }
123
124 pub fn show_track_outline(mut self, show: bool) -> Self {
129 self.show_track_outline = show;
130 self
131 }
132}
133
134impl<'a> Widget for MaterialSwitch<'a> {
135 fn ui(self, ui: &mut Ui) -> Response {
136 let switch_width = 52.0;
138 let switch_height = 32.0;
139 let track_height = 32.0;
140
141 let desired_size = if let Some(ref text) = self.text {
142 let text_width = ui.fonts(|fonts| {
143 fonts.glyph_width(&egui::FontId::default(), ' ') * text.len() as f32
144 });
145 Vec2::new(switch_width + 8.0 + text_width, switch_height)
146 } else {
147 Vec2::new(switch_width, switch_height)
148 };
149
150 let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
151
152 if response.clicked() && self.enabled {
153 *self.selected = !*self.selected;
154 response.mark_changed();
155 }
156
157 let is_pressed = response.is_pointer_button_down_on();
159 let is_hovered = response.hovered();
160 let is_focused = response.has_focus();
161
162 let primary_color = get_global_color("primary");
164 let on_primary = get_global_color("onPrimary");
165 let primary_container = get_global_color("primaryContainer");
166 let on_primary_container = get_global_color("onPrimaryContainer");
167 let surface_container_highest = get_global_color("surfaceContainerHighest");
168 let on_surface = get_global_color("onSurface");
169 let on_surface_variant = get_global_color("onSurfaceVariant");
170 let outline = get_global_color("outline");
171
172 let switch_rect = Rect::from_min_size(
174 Pos2::new(rect.min.x, rect.center().y - switch_height / 2.0),
175 Vec2::new(switch_width, switch_height),
176 );
177
178 let track_rect =
179 Rect::from_center_size(switch_rect.center(), Vec2::new(switch_width, track_height));
180
181 let has_icon = if *self.selected {
183 self.selected_icon.is_some()
184 } else {
185 self.unselected_icon.is_some()
186 };
187
188 let base_thumb_size_on = 24.0;
189 let base_thumb_size_off = if has_icon { 24.0 } else { 16.0 };
190 let pressed_thumb_size = 28.0;
191
192 let thumb_size = if is_pressed {
193 pressed_thumb_size
194 } else if *self.selected {
195 base_thumb_size_on
196 } else {
197 base_thumb_size_off
198 };
199
200 let thumb_travel = switch_width - base_thumb_size_on - 4.0;
201 let thumb_x = if *self.selected {
202 switch_rect.min.x + 2.0 + thumb_travel
203 } else {
204 switch_rect.min.x + 2.0
205 };
206
207 let thumb_center = Pos2::new(thumb_x + thumb_size / 2.0, switch_rect.center().y);
208
209 let (track_color, thumb_color, track_outline_color, icon_color) = if !self.enabled {
211 let disabled_track = if *self.selected {
213 on_surface.linear_multiply(0.12)
214 } else {
215 surface_container_highest.linear_multiply(0.12)
216 };
217 let disabled_thumb = if *self.selected {
218 on_surface.linear_multiply(1.0)
219 } else {
220 on_surface.linear_multiply(0.38)
221 };
222 let disabled_outline = on_surface.linear_multiply(0.12);
223 let disabled_icon = if *self.selected {
224 on_surface.linear_multiply(0.38)
225 } else {
226 surface_container_highest.linear_multiply(0.38)
227 };
228 (disabled_track, disabled_thumb, disabled_outline, disabled_icon)
229 } else if *self.selected {
230 let track = primary_color;
232 let thumb = if is_pressed || is_hovered || is_focused {
233 primary_container
234 } else {
235 on_primary
236 };
237 let outline = Color32::TRANSPARENT;
238 let icon = if is_pressed || is_hovered || is_focused {
239 on_primary_container
240 } else {
241 on_primary_container
242 };
243 (track, thumb, outline, icon)
244 } else {
245 let track = if is_pressed || is_hovered || is_focused {
247 surface_container_highest
248 } else {
249 surface_container_highest
250 };
251 let thumb = if is_pressed || is_hovered || is_focused {
252 on_surface_variant
253 } else {
254 outline
255 };
256 let track_outline = outline;
257 let icon = surface_container_highest;
258 (track, thumb, track_outline, icon)
259 };
260
261 ui.painter()
263 .rect_filled(track_rect, track_height / 2.0, track_color);
264
265 if self.show_track_outline && track_outline_color != Color32::TRANSPARENT {
267 ui.painter().rect_stroke(
268 track_rect,
269 track_height / 2.0,
270 Stroke::new(2.0, track_outline_color),
271 StrokeKind::Outside,
272 );
273 }
274
275 if self.enabled {
277 let overlay_radius = 20.0; let overlay_color = if *self.selected {
279 if is_pressed {
280 primary_color.linear_multiply(0.1)
281 } else if is_hovered {
282 primary_color.linear_multiply(0.08)
283 } else if is_focused {
284 primary_color.linear_multiply(0.1)
285 } else {
286 Color32::TRANSPARENT
287 }
288 } else {
289 if is_pressed {
290 on_surface.linear_multiply(0.1)
291 } else if is_hovered {
292 on_surface.linear_multiply(0.08)
293 } else if is_focused {
294 on_surface.linear_multiply(0.1)
295 } else {
296 Color32::TRANSPARENT
297 }
298 };
299
300 if overlay_color != Color32::TRANSPARENT {
301 ui.painter()
302 .circle_filled(thumb_center, overlay_radius, overlay_color);
303 }
304 }
305
306 ui.painter()
308 .circle_filled(thumb_center, thumb_size / 2.0, thumb_color);
309
310 let current_icon = if *self.selected {
312 self.selected_icon
313 } else {
314 self.unselected_icon
315 };
316
317 if let Some(icon) = current_icon {
318 let icon_size = 16.0;
319 let icon_font = FontId::proportional(icon_size);
320
321 ui.painter().text(
322 thumb_center,
323 egui::Align2::CENTER_CENTER,
324 icon.to_string(),
325 icon_font,
326 icon_color,
327 );
328 }
329
330 if let Some(ref text) = self.text {
332 let text_pos = Pos2::new(switch_rect.max.x + 8.0, rect.center().y);
333
334 let text_color = if self.enabled {
335 on_surface
336 } else {
337 on_surface.linear_multiply(0.38)
338 };
339
340 ui.painter().text(
341 text_pos,
342 egui::Align2::LEFT_CENTER,
343 text,
344 egui::FontId::default(),
345 text_color,
346 );
347 }
348
349 response
350 }
351}
352
353pub fn switch(selected: &mut bool) -> MaterialSwitch<'_> {
354 MaterialSwitch::new(selected)
355}