1use crate::get_global_color;
2use eframe::egui::{self, Color32, FontId, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
3use std::ops::RangeInclusive;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SliderInteraction {
8 TapAndSlide,
10 TapOnly,
12 SlideOnly,
14 SlideThumb,
16}
17
18impl Default for SliderInteraction {
19 fn default() -> Self {
20 Self::TapAndSlide
21 }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum ThumbShape {
27 Round,
29 Handle,
31}
32
33impl Default for ThumbShape {
34 fn default() -> Self {
35 Self::Round
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct RangeValues {
42 pub start: f32,
43 pub end: f32,
44}
45
46impl RangeValues {
47 pub fn new(start: f32, end: f32) -> Self {
48 Self { start, end }
49 }
50}
51
52pub struct MaterialSlider<'a> {
57 value: &'a mut f32,
59 range: RangeInclusive<f32>,
61 text: Option<String>,
63 enabled: bool,
65 width: Option<f32>,
67 step: Option<f32>,
69 show_value: bool,
71 secondary_track_value: Option<f32>,
73 show_value_indicator: bool,
75 interaction_mode: SliderInteraction,
77 thumb_shape: ThumbShape,
79 overlay_color: Option<Color32>,
81 thumb_color: Option<Color32>,
83 secondary_active_color: Option<Color32>,
85}
86
87impl<'a> MaterialSlider<'a> {
88 pub fn new(value: &'a mut f32, range: RangeInclusive<f32>) -> Self {
89 Self {
90 value,
91 range,
92 text: None,
93 enabled: true,
94 width: None,
95 step: None,
96 show_value: true,
97 secondary_track_value: None,
98 show_value_indicator: false,
99 interaction_mode: SliderInteraction::default(),
100 thumb_shape: ThumbShape::default(),
101 overlay_color: None,
102 thumb_color: None,
103 secondary_active_color: None,
104 }
105 }
106
107 pub fn text(mut self, text: impl Into<String>) -> Self {
108 self.text = Some(text.into());
109 self
110 }
111
112 pub fn enabled(mut self, enabled: bool) -> Self {
113 self.enabled = enabled;
114 self
115 }
116
117 pub fn width(mut self, width: f32) -> Self {
118 self.width = Some(width);
119 self
120 }
121
122 pub fn step(mut self, step: f32) -> Self {
123 self.step = Some(step);
124 self
125 }
126
127 pub fn show_value(mut self, show_value: bool) -> Self {
128 self.show_value = show_value;
129 self
130 }
131
132 pub fn secondary_track_value(mut self, value: f32) -> Self {
133 self.secondary_track_value = Some(value);
134 self
135 }
136
137 pub fn show_value_indicator(mut self, show: bool) -> Self {
138 self.show_value_indicator = show;
139 self
140 }
141
142 pub fn interaction_mode(mut self, mode: SliderInteraction) -> Self {
143 self.interaction_mode = mode;
144 self
145 }
146
147 pub fn thumb_shape(mut self, shape: ThumbShape) -> Self {
148 self.thumb_shape = shape;
149 self
150 }
151
152 pub fn overlay_color(mut self, color: Color32) -> Self {
153 self.overlay_color = Some(color);
154 self
155 }
156
157 pub fn thumb_color(mut self, color: Color32) -> Self {
158 self.thumb_color = Some(color);
159 self
160 }
161
162 pub fn secondary_active_color(mut self, color: Color32) -> Self {
163 self.secondary_active_color = Some(color);
164 self
165 }
166}
167
168impl<'a> Widget for MaterialSlider<'a> {
169 fn ui(self, ui: &mut Ui) -> Response {
170 let slider_width = self.width.unwrap_or(200.0);
171 let height = 48.0;
172
173 let desired_size = if self.text.is_some() || self.show_value {
174 Vec2::new(slider_width + 100.0, height)
175 } else {
176 Vec2::new(slider_width, height)
177 };
178
179 let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click_and_drag());
180
181 let primary_color = get_global_color("primary");
183 let surface_variant = get_global_color("surfaceVariant");
184 let on_surface = get_global_color("onSurface");
185 let on_surface_variant = get_global_color("onSurfaceVariant");
186
187 let track_rect = Rect::from_min_size(
189 Pos2::new(rect.min.x, rect.center().y - 2.0),
190 Vec2::new(slider_width, 4.0),
191 );
192
193 let old_value = *self.value;
194
195 let can_interact = match self.interaction_mode {
197 SliderInteraction::TapAndSlide => response.clicked() || response.dragged(),
198 SliderInteraction::TapOnly => response.clicked(),
199 SliderInteraction::SlideOnly => response.dragged(),
200 SliderInteraction::SlideThumb => {
201 let normalized_value =
203 (*self.value - self.range.start()) / (self.range.end() - self.range.start());
204 let normalized_value = normalized_value.clamp(0.0, 1.0);
205 let thumb_x = track_rect.min.x + normalized_value * track_rect.width();
206 let thumb_center = Pos2::new(thumb_x, track_rect.center().y);
207
208 if let Some(mouse_pos) = response.interact_pointer_pos() {
209 let dist = (mouse_pos - thumb_center).length();
210 response.dragged() && dist < 20.0
211 } else {
212 false
213 }
214 }
215 };
216
217 if can_interact && self.enabled {
218 if let Some(mouse_pos) = response.interact_pointer_pos() {
219 let normalized =
220 ((mouse_pos.x - track_rect.min.x) / track_rect.width()).clamp(0.0, 1.0);
221 let mut new_value =
222 *self.range.start() + normalized * (self.range.end() - self.range.start());
223
224 if let Some(step) = self.step {
226 new_value = (new_value / step).round() * step;
227 }
228
229 *self.value = new_value.clamp(*self.range.start(), *self.range.end());
230 if (*self.value - old_value).abs() > f32::EPSILON {
231 response.mark_changed();
232 }
233 }
234 }
235
236 if !self.enabled {
237 response = response.on_disabled_hover_text("Slider is disabled");
238 }
239
240 let normalized_value =
242 (*self.value - self.range.start()) / (self.range.end() - self.range.start());
243 let normalized_value = normalized_value.clamp(0.0, 1.0);
244 let thumb_x = track_rect.min.x + normalized_value * track_rect.width();
245 let thumb_center = Pos2::new(thumb_x, track_rect.center().y);
246
247 let effective_thumb_color = self.thumb_color.unwrap_or(primary_color);
249 let (track_active_color, track_inactive_color, thumb_color) = if !self.enabled {
250 let disabled_color = get_global_color("onSurface").linear_multiply(0.38);
251 (disabled_color, disabled_color, disabled_color)
252 } else if response.hovered() || response.dragged() {
253 (
254 Color32::from_rgba_premultiplied(
255 primary_color.r(),
256 primary_color.g(),
257 primary_color.b(),
258 200,
259 ),
260 surface_variant,
261 Color32::from_rgba_premultiplied(
262 effective_thumb_color.r().saturating_add(20),
263 effective_thumb_color.g().saturating_add(20),
264 effective_thumb_color.b().saturating_add(20),
265 255,
266 ),
267 )
268 } else {
269 (primary_color, surface_variant, effective_thumb_color)
270 };
271
272 ui.painter()
274 .rect_filled(track_rect, 2.0, track_inactive_color);
275
276 if let Some(secondary_value) = self.secondary_track_value {
278 let secondary_normalized =
279 (secondary_value - self.range.start()) / (self.range.end() - self.range.start());
280 let secondary_normalized = secondary_normalized.clamp(0.0, 1.0);
281 let secondary_x = track_rect.min.x + secondary_normalized * track_rect.width();
282
283 if secondary_x > thumb_x {
284 let secondary_rect = Rect::from_min_size(
285 Pos2::new(thumb_x, track_rect.min.y),
286 Vec2::new(secondary_x - thumb_x, track_rect.height()),
287 );
288 let secondary_color = self.secondary_active_color.unwrap_or_else(|| {
289 Color32::from_rgba_premultiplied(
290 primary_color.r(),
291 primary_color.g(),
292 primary_color.b(),
293 128,
294 )
295 });
296 ui.painter().rect_filled(secondary_rect, 2.0, secondary_color);
297 }
298 }
299
300 let active_track_rect = Rect::from_min_size(
302 track_rect.min,
303 Vec2::new(thumb_x - track_rect.min.x, track_rect.height()),
304 );
305
306 if active_track_rect.width() > 0.0 {
307 ui.painter()
308 .rect_filled(active_track_rect, 2.0, track_active_color);
309 }
310
311 match self.thumb_shape {
313 ThumbShape::Round => {
314 let thumb_radius = if response.hovered() || response.dragged() {
315 12.0
316 } else {
317 10.0
318 };
319 ui.painter()
320 .circle_filled(thumb_center, thumb_radius, thumb_color);
321 }
322 ThumbShape::Handle => {
323 let handle_width = if response.hovered() || response.dragged() {
325 8.0
326 } else {
327 4.0
328 };
329 let handle_height = 20.0;
330 let handle_rect = Rect::from_center_size(
331 thumb_center,
332 Vec2::new(handle_width, handle_height),
333 );
334 ui.painter().rect_filled(handle_rect, 2.0, thumb_color);
335 }
336 }
337
338 if response.hovered() && self.enabled {
340 let ripple_color = self.overlay_color.unwrap_or_else(|| {
341 Color32::from_rgba_premultiplied(
342 primary_color.r(),
343 primary_color.g(),
344 primary_color.b(),
345 30,
346 )
347 });
348 let ripple_radius = match self.thumb_shape {
349 ThumbShape::Round => 28.0,
350 ThumbShape::Handle => 24.0,
351 };
352 ui.painter()
353 .circle_filled(thumb_center, ripple_radius, ripple_color);
354 }
355
356 if self.show_value_indicator && response.dragged() && self.enabled {
358 let value_text = if let Some(step) = self.step {
359 if step >= 1.0 {
360 format!("{:.0}", *self.value)
361 } else {
362 format!("{:.2}", *self.value)
363 }
364 } else {
365 format!("{:.2}", *self.value)
366 };
367
368 let indicator_font = FontId::proportional(12.0);
370 let galley = ui.fonts(|f| f.layout_no_wrap(value_text, indicator_font, on_surface));
371 let indicator_size = Vec2::new(galley.size().x + 16.0, galley.size().y + 8.0);
372 let indicator_pos = Pos2::new(
373 thumb_center.x - indicator_size.x / 2.0,
374 thumb_center.y - indicator_size.y - 16.0,
375 );
376 let indicator_rect = Rect::from_min_size(indicator_pos, indicator_size);
377
378 ui.painter().rect_filled(
380 indicator_rect,
381 4.0,
382 primary_color,
383 );
384
385 ui.painter().galley(
387 Pos2::new(
388 indicator_rect.center().x - galley.size().x / 2.0,
389 indicator_rect.center().y - galley.size().y / 2.0,
390 ),
391 galley,
392 Color32::WHITE,
393 );
394 }
395
396 if let Some(ref text) = self.text {
398 let text_pos = Pos2::new(track_rect.max.x + 16.0, rect.center().y - 16.0);
399 let text_color = if self.enabled {
400 on_surface
401 } else {
402 get_global_color("onSurface").linear_multiply(0.38)
403 };
404
405 ui.painter().text(
406 text_pos,
407 egui::Align2::LEFT_CENTER,
408 text,
409 egui::FontId::default(),
410 text_color,
411 );
412 }
413
414 if self.show_value {
416 let value_text = if let Some(step) = self.step {
417 if step >= 1.0 {
418 format!("{:.0}", *self.value)
419 } else {
420 format!("{:.2}", *self.value)
421 }
422 } else {
423 format!("{:.2}", *self.value)
424 };
425
426 let value_pos = Pos2::new(
427 track_rect.max.x + 16.0,
428 rect.center().y + if self.text.is_some() { 8.0 } else { 0.0 },
429 );
430
431 let value_color = if self.enabled {
432 on_surface_variant
433 } else {
434 get_global_color("onSurface").linear_multiply(0.38)
435 };
436
437 ui.painter().text(
438 value_pos,
439 egui::Align2::LEFT_CENTER,
440 &value_text,
441 egui::FontId::proportional(12.0),
442 value_color,
443 );
444 }
445
446 response
447 }
448}
449
450pub struct MaterialRangeSlider<'a> {
452 values: &'a mut RangeValues,
453 range: RangeInclusive<f32>,
454 text: Option<String>,
455 enabled: bool,
456 width: Option<f32>,
457 step: Option<f32>,
458 show_values: bool,
459 show_value_indicator: bool,
460 thumb_shape: ThumbShape,
461 min_separation: f32,
462}
463
464impl<'a> MaterialRangeSlider<'a> {
465 pub fn new(values: &'a mut RangeValues, range: RangeInclusive<f32>) -> Self {
466 Self {
467 values,
468 range,
469 text: None,
470 enabled: true,
471 width: None,
472 step: None,
473 show_values: true,
474 show_value_indicator: false,
475 thumb_shape: ThumbShape::default(),
476 min_separation: 0.0,
477 }
478 }
479
480 pub fn text(mut self, text: impl Into<String>) -> Self {
481 self.text = Some(text.into());
482 self
483 }
484
485 pub fn enabled(mut self, enabled: bool) -> Self {
486 self.enabled = enabled;
487 self
488 }
489
490 pub fn width(mut self, width: f32) -> Self {
491 self.width = Some(width);
492 self
493 }
494
495 pub fn step(mut self, step: f32) -> Self {
496 self.step = Some(step);
497 self
498 }
499
500 pub fn show_values(mut self, show: bool) -> Self {
501 self.show_values = show;
502 self
503 }
504
505 pub fn show_value_indicator(mut self, show: bool) -> Self {
506 self.show_value_indicator = show;
507 self
508 }
509
510 pub fn thumb_shape(mut self, shape: ThumbShape) -> Self {
511 self.thumb_shape = shape;
512 self
513 }
514
515 pub fn min_separation(mut self, separation: f32) -> Self {
516 self.min_separation = separation;
517 self
518 }
519}
520
521impl<'a> Widget for MaterialRangeSlider<'a> {
522 fn ui(self, ui: &mut Ui) -> Response {
523 let slider_width = self.width.unwrap_or(200.0);
524 let height = 48.0;
525
526 let desired_size = if self.text.is_some() || self.show_values {
527 Vec2::new(slider_width + 120.0, height)
528 } else {
529 Vec2::new(slider_width, height)
530 };
531
532 let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click_and_drag());
533
534 let primary_color = get_global_color("primary");
536 let surface_variant = get_global_color("surfaceVariant");
537 let on_surface = get_global_color("onSurface");
538 let on_surface_variant = get_global_color("onSurfaceVariant");
539
540 let track_rect = Rect::from_min_size(
542 Pos2::new(rect.min.x, rect.center().y - 2.0),
543 Vec2::new(slider_width, 4.0),
544 );
545
546 if response.dragged() && self.enabled {
548 if let Some(mouse_pos) = response.interact_pointer_pos() {
549 let normalized =
550 ((mouse_pos.x - track_rect.min.x) / track_rect.width()).clamp(0.0, 1.0);
551 let mut new_value =
552 *self.range.start() + normalized * (self.range.end() - self.range.start());
553
554 if let Some(step) = self.step {
556 new_value = (new_value / step).round() * step;
557 }
558
559 let dist_to_start = (new_value - self.values.start).abs();
561 let dist_to_end = (new_value - self.values.end).abs();
562
563 if dist_to_start < dist_to_end {
564 self.values.start = new_value.clamp(
566 *self.range.start(),
567 (self.values.end - self.min_separation).min(*self.range.end()),
568 );
569 } else {
570 self.values.end = new_value.clamp(
572 (self.values.start + self.min_separation).max(*self.range.start()),
573 *self.range.end(),
574 );
575 }
576 }
577 }
578
579 let start_normalized =
581 (self.values.start - self.range.start()) / (self.range.end() - self.range.start());
582 let start_normalized = start_normalized.clamp(0.0, 1.0);
583 let start_x = track_rect.min.x + start_normalized * track_rect.width();
584 let start_center = Pos2::new(start_x, track_rect.center().y);
585
586 let end_normalized =
587 (self.values.end - self.range.start()) / (self.range.end() - self.range.start());
588 let end_normalized = end_normalized.clamp(0.0, 1.0);
589 let end_x = track_rect.min.x + end_normalized * track_rect.width();
590 let end_center = Pos2::new(end_x, track_rect.center().y);
591
592 let (track_active_color, track_inactive_color, thumb_color) = if !self.enabled {
594 let disabled_color = get_global_color("onSurface").linear_multiply(0.38);
595 (disabled_color, disabled_color, disabled_color)
596 } else if response.hovered() || response.dragged() {
597 (
598 Color32::from_rgba_premultiplied(
599 primary_color.r(),
600 primary_color.g(),
601 primary_color.b(),
602 200,
603 ),
604 surface_variant,
605 primary_color,
606 )
607 } else {
608 (primary_color, surface_variant, primary_color)
609 };
610
611 ui.painter()
613 .rect_filled(track_rect, 2.0, track_inactive_color);
614
615 let active_track_rect = Rect::from_min_size(
617 Pos2::new(start_x, track_rect.min.y),
618 Vec2::new(end_x - start_x, track_rect.height()),
619 );
620
621 if active_track_rect.width() > 0.0 {
622 ui.painter()
623 .rect_filled(active_track_rect, 2.0, track_active_color);
624 }
625
626 let thumb_radius = if response.hovered() || response.dragged() {
628 12.0
629 } else {
630 10.0
631 };
632
633 match self.thumb_shape {
634 ThumbShape::Round => {
635 ui.painter()
636 .circle_filled(start_center, thumb_radius, thumb_color);
637 ui.painter()
638 .circle_filled(end_center, thumb_radius, thumb_color);
639 }
640 ThumbShape::Handle => {
641 let handle_width = if response.hovered() || response.dragged() {
642 8.0
643 } else {
644 4.0
645 };
646 let handle_height = 20.0;
647
648 let start_handle_rect = Rect::from_center_size(
649 start_center,
650 Vec2::new(handle_width, handle_height),
651 );
652 ui.painter()
653 .rect_filled(start_handle_rect, 2.0, thumb_color);
654
655 let end_handle_rect = Rect::from_center_size(
656 end_center,
657 Vec2::new(handle_width, handle_height),
658 );
659 ui.painter()
660 .rect_filled(end_handle_rect, 2.0, thumb_color);
661 }
662 }
663
664 if response.hovered() && self.enabled {
666 let ripple_color = Color32::from_rgba_premultiplied(
667 primary_color.r(),
668 primary_color.g(),
669 primary_color.b(),
670 30,
671 );
672 ui.painter()
673 .circle_filled(start_center, 28.0, ripple_color);
674 ui.painter()
675 .circle_filled(end_center, 28.0, ripple_color);
676 }
677
678 if let Some(ref text) = self.text {
680 let text_pos = Pos2::new(track_rect.max.x + 16.0, rect.center().y - 16.0);
681 let text_color = if self.enabled {
682 on_surface
683 } else {
684 get_global_color("onSurface").linear_multiply(0.38)
685 };
686
687 ui.painter().text(
688 text_pos,
689 egui::Align2::LEFT_CENTER,
690 text,
691 egui::FontId::default(),
692 text_color,
693 );
694 }
695
696 if self.show_values {
698 let format_value = |value: f32| {
699 if let Some(step) = self.step {
700 if step >= 1.0 {
701 format!("{:.0}", value)
702 } else {
703 format!("{:.2}", value)
704 }
705 } else {
706 format!("{:.2}", value)
707 }
708 };
709
710 let value_text = format!(
711 "{} - {}",
712 format_value(self.values.start),
713 format_value(self.values.end)
714 );
715
716 let value_pos = Pos2::new(
717 track_rect.max.x + 16.0,
718 rect.center().y + if self.text.is_some() { 8.0 } else { 0.0 },
719 );
720
721 let value_color = if self.enabled {
722 on_surface_variant
723 } else {
724 get_global_color("onSurface").linear_multiply(0.38)
725 };
726
727 ui.painter().text(
728 value_pos,
729 egui::Align2::LEFT_CENTER,
730 &value_text,
731 egui::FontId::proportional(12.0),
732 value_color,
733 );
734 }
735
736 response
737 }
738}
739
740pub fn slider<'a>(value: &'a mut f32, range: RangeInclusive<f32>) -> MaterialSlider<'a> {
741 MaterialSlider::new(value, range)
742}
743
744pub fn range_slider<'a>(
745 values: &'a mut RangeValues,
746 range: RangeInclusive<f32>,
747) -> MaterialRangeSlider<'a> {
748 MaterialRangeSlider::new(values, range)
749}