1use crate::get_global_color;
2use 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.painter().layout_no_wrap(
143 text.clone(),
144 egui::FontId::default(),
145 egui::Color32::WHITE,
146 ).size().x;
147 Vec2::new(switch_width + 8.0 + text_width, switch_height)
148 } else {
149 Vec2::new(switch_width, switch_height)
150 };
151
152 let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
153
154 if response.clicked() && self.enabled {
155 *self.selected = !*self.selected;
156 response.mark_changed();
157 }
158
159 let is_pressed = response.is_pointer_button_down_on();
161 let is_hovered = response.hovered();
162 let is_focused = response.has_focus();
163
164 let primary_color = get_global_color("primary");
166 let on_primary = get_global_color("onPrimary");
167 let primary_container = get_global_color("primaryContainer");
168 let on_primary_container = get_global_color("onPrimaryContainer");
169 let surface_container_highest = get_global_color("surfaceContainerHighest");
170 let on_surface = get_global_color("onSurface");
171 let on_surface_variant = get_global_color("onSurfaceVariant");
172 let outline = get_global_color("outline");
173
174 let switch_rect = Rect::from_min_size(
176 Pos2::new(rect.min.x, rect.center().y - switch_height / 2.0),
177 Vec2::new(switch_width, switch_height),
178 );
179
180 let track_rect =
181 Rect::from_center_size(switch_rect.center(), Vec2::new(switch_width, track_height));
182
183 let has_icon = if *self.selected {
185 self.selected_icon.is_some()
186 } else {
187 self.unselected_icon.is_some()
188 };
189
190 let base_thumb_size_on = 24.0;
191 let base_thumb_size_off = if has_icon { 24.0 } else { 16.0 };
192 let pressed_thumb_size = 28.0;
193
194 let thumb_size = if is_pressed {
195 pressed_thumb_size
196 } else if *self.selected {
197 base_thumb_size_on
198 } else {
199 base_thumb_size_off
200 };
201
202 let thumb_travel = switch_width - base_thumb_size_on - 4.0;
203 let thumb_x = if *self.selected {
204 switch_rect.min.x + 2.0 + thumb_travel
205 } else {
206 switch_rect.min.x + 2.0
207 };
208
209 let thumb_center = Pos2::new(thumb_x + thumb_size / 2.0, switch_rect.center().y);
210
211 let (track_color, thumb_color, track_outline_color, icon_color) = if !self.enabled {
213 let disabled_track = if *self.selected {
215 on_surface.linear_multiply(0.12)
216 } else {
217 surface_container_highest.linear_multiply(0.12)
218 };
219 let disabled_thumb = if *self.selected {
220 on_surface.linear_multiply(1.0)
221 } else {
222 on_surface.linear_multiply(0.38)
223 };
224 let disabled_outline = on_surface.linear_multiply(0.12);
225 let disabled_icon = if *self.selected {
226 on_surface.linear_multiply(0.38)
227 } else {
228 surface_container_highest.linear_multiply(0.38)
229 };
230 (disabled_track, disabled_thumb, disabled_outline, disabled_icon)
231 } else if *self.selected {
232 let track = primary_color;
234 let thumb = if is_pressed || is_hovered || is_focused {
235 primary_container
236 } else {
237 on_primary
238 };
239 let outline = Color32::TRANSPARENT;
240 let icon = if is_pressed || is_hovered || is_focused {
241 on_primary_container
242 } else {
243 on_primary_container
244 };
245 (track, thumb, outline, icon)
246 } else {
247 let track = if is_pressed || is_hovered || is_focused {
249 surface_container_highest
250 } else {
251 surface_container_highest
252 };
253 let thumb = if is_pressed || is_hovered || is_focused {
254 on_surface_variant
255 } else {
256 outline
257 };
258 let track_outline = outline;
259 let icon = surface_container_highest;
260 (track, thumb, track_outline, icon)
261 };
262
263 ui.painter()
265 .rect_filled(track_rect, track_height / 2.0, track_color);
266
267 if self.show_track_outline && track_outline_color != Color32::TRANSPARENT {
269 ui.painter().rect_stroke(
270 track_rect,
271 track_height / 2.0,
272 Stroke::new(2.0, track_outline_color),
273 StrokeKind::Outside,
274 );
275 }
276
277 if self.enabled {
279 let overlay_radius = 20.0; let overlay_color = if *self.selected {
281 if is_pressed {
282 primary_color.linear_multiply(0.1)
283 } else if is_hovered {
284 primary_color.linear_multiply(0.08)
285 } else if is_focused {
286 primary_color.linear_multiply(0.1)
287 } else {
288 Color32::TRANSPARENT
289 }
290 } else {
291 if is_pressed {
292 on_surface.linear_multiply(0.1)
293 } else if is_hovered {
294 on_surface.linear_multiply(0.08)
295 } else if is_focused {
296 on_surface.linear_multiply(0.1)
297 } else {
298 Color32::TRANSPARENT
299 }
300 };
301
302 if overlay_color != Color32::TRANSPARENT {
303 ui.painter()
304 .circle_filled(thumb_center, overlay_radius, overlay_color);
305 }
306 }
307
308 ui.painter()
310 .circle_filled(thumb_center, thumb_size / 2.0, thumb_color);
311
312 let current_icon = if *self.selected {
314 self.selected_icon
315 } else {
316 self.unselected_icon
317 };
318
319 if let Some(icon) = current_icon {
320 let icon_size = 16.0;
321 let icon_font = FontId::proportional(icon_size);
322
323 ui.painter().text(
324 thumb_center,
325 egui::Align2::CENTER_CENTER,
326 icon.to_string(),
327 icon_font,
328 icon_color,
329 );
330 }
331
332 if let Some(ref text) = self.text {
334 let text_pos = Pos2::new(switch_rect.max.x + 8.0, rect.center().y);
335
336 let text_color = if self.enabled {
337 on_surface
338 } else {
339 on_surface.linear_multiply(0.38)
340 };
341
342 ui.painter().text(
343 text_pos,
344 egui::Align2::LEFT_CENTER,
345 text,
346 egui::FontId::default(),
347 text_color,
348 );
349 }
350
351 response
352 }
353}
354
355pub fn switch(selected: &mut bool) -> MaterialSwitch<'_> {
356 MaterialSwitch::new(selected)
357}