1use std::rc::Rc;
4
5use frus_core::{Color, Point, Rect, ResolvedTextStyle, Scene};
6use frus_layout::{Dimension, Style};
7
8use crate::disabled::{disabled_container, disabled_content};
9use crate::flex::Flex;
10use crate::interaction::{Key, KeyResponse, Status};
11use crate::theme::Theme;
12use crate::widget::Widget;
13
14const TIP_H: f32 = 20.0;
16const TIP_GAP: f32 = 6.0;
17fn tip_style(theme: Option<&Theme>) -> ResolvedTextStyle {
23 theme
24 .and_then(|t| t.widgets.slider.value_indicator_text_style)
25 .unwrap_or_else(|| crate::theme::type_scale(theme).label_large)
26 .resolved()
27}
28const KEY_STEP: f32 = 0.05;
30
31const H: f32 = 24.0;
32const TRACK_H: f32 = 6.0;
33const THUMB: f32 = 18.0;
34const THUMB_ELEVATION: f32 = 1.0;
38const THUMB_PRESSED_ELEVATION: f32 = 6.0;
39
40fn paint_thumb_shadow(
53 thumb: Rect,
54 told: (Option<f32>, Option<f32>),
55 status: &Status,
56 theme: &Theme,
57 scene: &mut Scene,
58) {
59 let t = &theme.widgets.slider;
60 let rest = told.0.or(t.thumb_elevation).unwrap_or(THUMB_ELEVATION);
61 let pressed = told
62 .1
63 .or(t.pressed_thumb_elevation)
64 .unwrap_or(THUMB_PRESSED_ELEVATION);
65 let depth = rest + (pressed - rest) * status.press_progress.clamp(0.0, 1.0);
66 if depth <= 0.0 {
67 return;
68 }
69 let blur = depth * 2.0 + 4.0;
70 scene.shadow(
71 Rect::new(
72 thumb.x - blur,
73 thumb.y + depth * 0.5 - blur,
74 thumb.width + 2.0 * blur,
75 thumb.height + 2.0 * blur,
76 ),
77 theme.scheme.shadow.with_alpha(0.30).fade(status.opacity),
78 frus_core::BorderRadius::uniform(thumb.width * 0.5).inflate(blur),
79 blur,
80 );
81}
82
83pub struct Slider<Msg = crate::callback::Callback> {
85 value: f32,
86 min: f32,
87 max: f32,
88 width: f32,
89 divisions: Option<usize>,
90 label: Option<Rc<dyn Fn(f32) -> String>>,
92 enabled: bool,
93 colors: SliderColors,
94 thumb_elevation: Option<f32>,
95 pressed_thumb_elevation: Option<f32>,
96 on_change: Option<Box<dyn Fn(f32) -> Msg>>,
97 on_change_start: Option<Box<dyn Fn(f32) -> Msg>>,
99 on_change_end: Option<Box<dyn Fn(f32) -> Msg>>,
101}
102
103#[derive(Clone, Copy, Debug, Default, PartialEq)]
106pub(crate) struct SliderColors {
107 pub active_track: Option<Color>,
108 pub inactive_track: Option<Color>,
109 pub thumb: Option<Color>,
110 pub thumb_border: Option<Color>,
111}
112
113impl SliderColors {
114 fn resolve(&self, theme: &Theme) -> (Color, Color, Color, Color) {
116 let rail = self
121 .inactive_track
122 .or(theme.widgets.slider.inactive_track_color)
123 .unwrap_or(theme.scheme.secondary_container);
124 let filled = self
125 .active_track
126 .or(theme.widgets.slider.active_track_color)
127 .unwrap_or(theme.primary);
128 let thumb = self
129 .thumb
130 .or(theme.widgets.slider.thumb_color)
131 .unwrap_or(Color::WHITE);
132 let ring = self
136 .thumb_border
137 .or(theme.widgets.slider.thumb_border_color)
138 .unwrap_or(filled);
139 (rail, filled, thumb, ring)
140 }
141}
142
143impl<Msg> Slider<Msg> {
144 pub fn new(value: f32) -> Self {
147 Self {
148 value,
149 min: 0.0,
150 max: 1.0,
151 width: 220.0,
152 divisions: None,
153 label: None,
154 enabled: true,
155 colors: SliderColors::default(),
156 thumb_elevation: None,
157 pressed_thumb_elevation: None,
158 on_change: None,
159 on_change_start: None,
160 on_change_end: None,
161 }
162 }
163
164 pub fn active_color(mut self, color: Color) -> Self {
168 self.colors.active_track = Some(color);
169 self
170 }
171
172 pub fn inactive_color(mut self, color: Color) -> Self {
174 self.colors.inactive_track = Some(color);
175 self
176 }
177
178 pub fn thumb_color(mut self, color: Color) -> Self {
180 self.colors.thumb = Some(color);
181 self
182 }
183
184 pub fn thumb_border_color(mut self, color: Color) -> Self {
186 self.colors.thumb_border = Some(color);
187 self
188 }
189
190 pub fn thumb_elevation(mut self, elevation: f32) -> Self {
193 self.thumb_elevation = Some(elevation);
194 self
195 }
196
197 pub fn pressed_thumb_elevation(mut self, elevation: f32) -> Self {
199 self.pressed_thumb_elevation = Some(elevation);
200 self
201 }
202
203 pub fn range(mut self, min: f32, max: f32) -> Self {
213 self.min = min.min(max);
214 self.max = min.max(max);
215 self
216 }
217
218 pub fn divisions(mut self, n: usize) -> Self {
222 self.divisions = Some(n.max(1));
223 self
224 }
225
226 pub fn value_label(mut self, label: impl Fn(f32) -> String + 'static) -> Self {
230 self.label = Some(Rc::new(label));
231 self
232 }
233
234 pub fn width(mut self, width: f32) -> Self {
236 self.width = width;
237 self
238 }
239
240 pub fn on_change<R: crate::callback::IntoMsg<Msg>>(
242 mut self,
243 on_change: impl Fn(f32) -> R + 'static,
244 ) -> Self {
245 self.on_change = Some(Box::new(crate::callback::handler1(on_change)));
246 self
247 }
248
249 pub fn on_change_start<R: crate::callback::IntoMsg<Msg>>(
258 mut self,
259 on_start: impl Fn(f32) -> R + 'static,
260 ) -> Self {
261 self.on_change_start = Some(Box::new(crate::callback::handler1(on_start)));
262 self
263 }
264
265 pub fn on_change_end<R: crate::callback::IntoMsg<Msg>>(
271 mut self,
272 on_end: impl Fn(f32) -> R + 'static,
273 ) -> Self {
274 self.on_change_end = Some(Box::new(crate::callback::handler1(on_end)));
275 self
276 }
277
278 pub fn enabled(mut self, enabled: bool) -> Self {
285 self.enabled = enabled;
286 self
287 }
288
289 fn held(&self) -> f32 {
291 self.value.clamp(self.min, self.max)
292 }
293
294 fn fraction(&self) -> f32 {
297 let span = self.max - self.min;
298 if span <= 0.0 {
299 0.0
300 } else {
301 ((self.held() - self.min) / span).clamp(0.0, 1.0)
302 }
303 }
304
305 fn snap(&self, fraction: f32) -> f32 {
307 match self.divisions {
308 Some(n) if n > 0 => (fraction * n as f32).round() / n as f32,
309 _ => fraction,
310 }
311 }
312
313 fn at(&self, fraction: f32) -> f32 {
315 self.min + self.snap(fraction.clamp(0.0, 1.0)) * (self.max - self.min)
316 }
317
318 fn key_step(&self) -> f32 {
321 match self.divisions {
322 Some(n) if n > 0 => 1.0 / n as f32,
323 _ => KEY_STEP,
324 }
325 }
326
327 fn content_h(&self) -> f32 {
330 if self.label.is_some() {
331 H + TIP_H + TIP_GAP
332 } else {
333 H
334 }
335 }
336}
337
338impl<Msg> Widget<Msg> for Slider<Msg> {
339 fn style(&self) -> Style {
340 Style {
341 width: Dimension::Length(self.width),
342 height: Dimension::Length(self.content_h()),
343 ..Default::default()
344 }
345 }
346
347 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
348 &[]
349 }
350
351 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
352 let o = status.opacity;
353 let track_top = bounds.y + bounds.height - H;
356 let track_y = track_top + (H - TRACK_H) * 0.5;
357 let (rail, filled_color, thumb, ring) = if self.enabled {
361 self.colors.resolve(theme)
362 } else {
363 let dead = disabled_content(theme);
364 (disabled_container(theme), dead, dead, dead)
365 };
366 scene.draw_rect(
368 Rect::new(bounds.x, track_y, bounds.width, TRACK_H),
369 rail.fade(o),
370 TRACK_H * 0.5,
371 0.0,
372 Color::TRANSPARENT,
373 );
374 let filled = bounds.width * self.fraction();
376 scene.draw_rect(
377 Rect::new(bounds.x, track_y, filled, TRACK_H),
378 filled_color.fade(o),
379 TRACK_H * 0.5,
380 0.0,
381 Color::TRANSPARENT,
382 );
383 let cx = bounds.x + filled;
385 let thumb_box = Rect::new(
386 cx - THUMB * 0.5,
387 track_top + (H - THUMB) * 0.5,
388 THUMB,
389 THUMB,
390 );
391 paint_thumb_shadow(
392 thumb_box,
393 (self.thumb_elevation, self.pressed_thumb_elevation),
394 &status,
395 theme,
396 scene,
397 );
398 scene.draw_rect(thumb_box, thumb.fade(o), THUMB * 0.5, 2.0, ring.fade(o));
399
400 if let Some(label) = self.label.as_ref().filter(|_| self.enabled) {
403 let reveal = if status.focused {
404 o
405 } else {
406 status.hover_progress * o
407 };
408 if reveal > 0.01 {
409 paint_tip(cx, bounds.y, label(self.held()), theme, reveal, scene);
410 }
411 }
412 }
413
414 fn on_click(&self) -> Option<Msg> {
415 None
416 }
417
418 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
419 let held = self.held();
425 let spoken = match self.label.as_ref() {
426 Some(label) => label(held),
427 None if self.min == 0.0 && self.max == 1.0 => {
428 format!("{}%", (held * 100.0).round())
429 }
430 None => format!("{held}"),
431 };
432 let semantics = frus_core::SemanticsProperties::new(frus_core::Role::Slider)
433 .value(spoken)
434 .range(self.min, held, self.max);
435 Some(if self.enabled {
436 semantics
437 } else {
438 semantics.disabled(true)
439 })
440 }
441
442 fn draggable(&self) -> bool {
443 self.enabled
444 }
445
446 fn focusable(&self) -> bool {
447 self.enabled && self.on_change.is_some()
448 }
449
450 fn on_key(&self, key: &Key) -> KeyResponse<Msg> {
451 if !self.enabled {
454 return KeyResponse::Ignored;
455 }
456 let delta = match key {
459 Key::Left { .. } => -self.key_step(),
460 Key::Right { .. } => self.key_step(),
461 Key::Home { .. } => -2.0,
462 Key::End { .. } => 2.0,
463 _ => return KeyResponse::Ignored,
464 };
465 let moved = self.at(self.fraction() + delta);
466 KeyResponse::Handled(self.on_change.as_ref().map(|make| make(moved)))
467 }
468
469 fn on_drag(&self, fraction: f32) -> Option<Msg> {
470 if !self.enabled {
473 return None;
474 }
475 self.on_change.as_ref().map(|make| make(self.at(fraction)))
476 }
477
478 fn on_drag_start(&self, fraction: f32) -> Option<Msg> {
479 if !self.enabled {
480 return None;
481 }
482 self.on_change_start
486 .as_ref()
487 .map(|make| make(self.at(fraction)))
488 }
489
490 fn on_drag_end(&self, fraction: f32) -> Option<Msg> {
491 if !self.enabled {
492 return None;
493 }
494 self.on_change_end
495 .as_ref()
496 .map(|make| make(self.at(fraction)))
497 }
498}
499
500struct Spacer {
502 width: f32,
503 height: f32,
504}
505
506impl<Msg: Clone> Widget<Msg> for Spacer {
507 fn style(&self) -> Style {
508 Style {
509 width: Dimension::Length(self.width),
510 height: Dimension::Length(self.height),
511 ..Default::default()
512 }
513 }
514
515 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
516 &[]
517 }
518
519 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
520
521 fn on_click(&self) -> Option<Msg> {
522 None
523 }
524}
525
526#[derive(Copy, Clone)]
528enum Side {
529 Low,
530 High,
531}
532
533struct RangeThumb<Msg> {
536 side: Side,
537 low: f32,
538 high: f32,
539 track: f32,
541 height: f32,
544 divisions: Option<usize>,
545 label: Option<Rc<dyn Fn(f32) -> String>>,
547 enabled: bool,
549 elevations: (Option<f32>, Option<f32>),
551 on_change: Option<Rc<dyn Fn(f32, f32) -> Msg>>,
552}
553
554impl<Msg> RangeThumb<Msg> {
555 fn snap(&self, v: f32) -> f32 {
557 match self.divisions {
558 Some(n) if n > 0 => (v * n as f32).round() / n as f32,
559 _ => v,
560 }
561 }
562
563 fn value(&self) -> f32 {
565 match self.side {
566 Side::Low => self.low,
567 Side::High => self.high,
568 }
569 }
570
571 fn moved(&self, delta: f32) -> (f32, f32) {
574 match self.side {
575 Side::Low => (
576 self.snap((self.low + delta).clamp(0.0, self.high)),
577 self.high,
578 ),
579 Side::High => (
580 self.low,
581 self.snap((self.high + delta).clamp(self.low, 1.0)),
582 ),
583 }
584 }
585
586 fn key_step(&self) -> f32 {
588 match self.divisions {
589 Some(n) if n > 0 => 1.0 / n as f32,
590 _ => KEY_STEP,
591 }
592 }
593}
594
595impl<Msg: Clone> Widget<Msg> for RangeThumb<Msg> {
596 fn style(&self) -> Style {
597 Style {
598 width: Dimension::Length(THUMB),
599 height: Dimension::Length(self.height),
600 ..Default::default()
601 }
602 }
603
604 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
605 &[]
606 }
607
608 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
609 let o = status.opacity;
610 let y = bounds.y + bounds.height - H + (H - THUMB) * 0.5;
612 let border = if status.focused && self.enabled {
614 3.0
615 } else {
616 2.0
617 };
618 let (fill, ring) = if self.enabled {
619 (Color::WHITE, theme.primary)
620 } else {
621 let dead = disabled_content(theme);
622 (dead, dead)
623 };
624 let thumb = Rect::new(bounds.x, y, THUMB, THUMB);
625 paint_thumb_shadow(thumb, self.elevations, &status, theme, scene);
626 scene.draw_rect(thumb, fill.fade(o), THUMB * 0.5, border, ring.fade(o));
627 if let Some(label) = self.label.as_ref().filter(|_| self.enabled) {
630 let active = status.focused || status.hover_progress > 0.01;
631 if active {
632 let reveal = if status.focused {
633 o
634 } else {
635 status.hover_progress * o
636 };
637 paint_tip(
638 bounds.x + THUMB * 0.5,
639 bounds.y,
640 label(self.value()),
641 theme,
642 reveal,
643 scene,
644 );
645 }
646 }
647 }
648
649 fn on_click(&self) -> Option<Msg> {
650 None
651 }
652
653 fn focusable(&self) -> bool {
654 self.enabled && self.on_change.is_some()
655 }
656
657 fn draggable(&self) -> bool {
658 self.enabled
659 }
660
661 fn on_drag_delta(&self, dx: f32) -> Option<Msg> {
662 if !self.enabled || dx == 0.0 || self.track <= 0.0 {
663 return None;
664 }
665 let (low, high) = self.moved(dx / self.track);
666 self.on_change.as_ref().map(|make| make(low, high))
667 }
668
669 fn on_key(&self, key: &Key) -> KeyResponse<Msg> {
670 if !self.enabled {
673 return KeyResponse::Ignored;
674 }
675 let delta = match key {
678 Key::Left { .. } => -self.key_step(),
679 Key::Right { .. } => self.key_step(),
680 Key::Home { .. } => -2.0, Key::End { .. } => 2.0, _ => return KeyResponse::Ignored,
683 };
684 let (low, high) = self.moved(delta);
685 KeyResponse::Handled(self.on_change.as_ref().map(|make| make(low, high)))
686 }
687}
688
689pub struct RangeSlider<Msg = crate::callback::Callback> {
694 low: f32,
695 high: f32,
696 width: f32,
697 divisions: Option<usize>,
698 label: Option<Rc<dyn Fn(f32) -> String>>,
701 enabled: bool,
702 thumb_elevation: Option<f32>,
703 pressed_thumb_elevation: Option<f32>,
704 on_change: Option<Rc<dyn Fn(f32, f32) -> Msg>>,
705 on_change_start: Option<Rc<dyn Fn(f32, f32) -> Msg>>,
707 on_change_end: Option<Rc<dyn Fn(f32, f32) -> Msg>>,
709 children: Vec<Box<dyn Widget<Msg>>>,
710}
711
712impl<Msg: Clone + 'static> RangeSlider<Msg> {
713 pub fn new(low: f32, high: f32) -> Self {
715 let low = low.clamp(0.0, 1.0);
716 let high = high.clamp(0.0, 1.0);
717 let mut slider = Self {
718 low: low.min(high),
719 high: low.max(high),
720 width: 220.0,
721 divisions: None,
722 label: None,
723 enabled: true,
724 thumb_elevation: None,
725 pressed_thumb_elevation: None,
726 on_change: None,
727 on_change_start: None,
728 on_change_end: None,
729 children: Vec::new(),
730 };
731 slider.rebuild();
732 slider
733 }
734
735 pub fn width(mut self, width: f32) -> Self {
737 self.width = width;
738 self.rebuild();
739 self
740 }
741
742 pub fn divisions(mut self, n: usize) -> Self {
745 self.divisions = Some(n.max(1));
746 self.rebuild();
747 self
748 }
749
750 pub fn on_change<R: crate::callback::IntoMsg<Msg>>(
752 mut self,
753 on_change: impl Fn(f32, f32) -> R + 'static,
754 ) -> Self {
755 self.on_change = Some(Rc::new(crate::callback::handler2(on_change)));
756 self.rebuild();
757 self
758 }
759
760 pub fn enabled(mut self, enabled: bool) -> Self {
765 self.enabled = enabled;
766 self.rebuild();
767 self
768 }
769
770 pub fn thumb_elevation(mut self, elevation: f32) -> Self {
773 self.thumb_elevation = Some(elevation);
774 self.rebuild();
775 self
776 }
777
778 pub fn pressed_thumb_elevation(mut self, elevation: f32) -> Self {
780 self.pressed_thumb_elevation = Some(elevation);
781 self.rebuild();
782 self
783 }
784
785 pub fn value_label(mut self, label: impl Fn(f32) -> String + 'static) -> Self {
788 self.label = Some(Rc::new(label));
789 self.rebuild();
790 self
791 }
792
793 pub fn on_change_start<R: crate::callback::IntoMsg<Msg>>(
798 mut self,
799 on_start: impl Fn(f32, f32) -> R + 'static,
800 ) -> Self {
801 self.on_change_start = Some(Rc::new(crate::callback::handler2(on_start)));
802 self
803 }
804
805 pub fn on_change_end<R: crate::callback::IntoMsg<Msg>>(
808 mut self,
809 on_end: impl Fn(f32, f32) -> R + 'static,
810 ) -> Self {
811 self.on_change_end = Some(Rc::new(crate::callback::handler2(on_end)));
812 self
813 }
814
815 fn interval_at(&self, fraction: f32) -> (f32, f32) {
821 let f = self.snap(fraction.clamp(0.0, 1.0));
822 if f <= self.low {
823 (f, self.high)
824 } else if f >= self.high {
825 (self.low, f)
826 } else if f - self.low <= self.high - f {
827 (f, self.high)
828 } else {
829 (self.low, f)
830 }
831 }
832
833 fn rebuild(&mut self) {
835 let height = self.content_h();
836 let thumb = |side: Side| RangeThumb {
837 side,
838 low: self.low,
839 high: self.high,
840 track: self.width,
841 height,
842 divisions: self.divisions,
843 label: self.label.clone(),
844 enabled: self.enabled,
845 elevations: (self.thumb_elevation, self.pressed_thumb_elevation),
846 on_change: self.on_change.clone(),
847 };
848 let lo_gap = (self.low * self.width - THUMB * 0.5).max(0.0);
849 let mid_gap = ((self.high - self.low) * self.width - THUMB).max(0.0);
850 let row = Flex::row()
851 .child(Spacer {
852 width: lo_gap,
853 height,
854 })
855 .child(thumb(Side::Low))
856 .child(Spacer {
857 width: mid_gap,
858 height,
859 })
860 .child(thumb(Side::High));
861 self.children = vec![Box::new(row)];
862 }
863}
864
865fn paint_tip(cx: f32, top: f32, text: String, theme: &Theme, o: f32, scene: &mut Scene) {
867 let style = tip_style(Some(theme));
868 let tw = frus_text::measure_resolved(&text, &style).width;
869 let bw = tw + 12.0;
870 let bx = cx - bw * 0.5;
871 scene.draw_rect(
872 Rect::new(bx, top, bw, TIP_H),
873 theme.primary.fade(o),
874 TIP_H * 0.5,
875 0.0,
876 Color::TRANSPARENT,
877 );
878 let ty = top + (TIP_H - style.line_height()) * 0.5;
879 scene.text(
880 Point::new(bx + 6.0, ty),
881 text,
882 &style,
883 theme.on_primary.fade(o),
884 );
885}
886
887impl<Msg> RangeSlider<Msg> {
888 fn content_h(&self) -> f32 {
890 if self.label.is_some() {
891 TIP_H + TIP_GAP + H
892 } else {
893 H
894 }
895 }
896
897 fn snap(&self, v: f32) -> f32 {
899 match self.divisions {
900 Some(n) if n > 0 => (v * n as f32).round() / n as f32,
901 _ => v,
902 }
903 }
904}
905
906impl<Msg: Clone + 'static> Widget<Msg> for RangeSlider<Msg> {
907 fn style(&self) -> Style {
908 Style {
909 width: Dimension::Length(self.width),
910 height: Dimension::Length(self.content_h()),
911 ..Default::default()
912 }
913 }
914
915 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
916 &self.children
917 }
918
919 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
920 let o = status.opacity;
921 let base_y = bounds.y + bounds.height - H;
923 let track_y = base_y + (H - TRACK_H) * 0.5;
924 let (rail, span) = if self.enabled {
926 (theme.scheme.secondary_container, theme.primary)
927 } else {
928 (disabled_container(theme), disabled_content(theme))
929 };
930 scene.draw_rect(
931 Rect::new(bounds.x, track_y, bounds.width, TRACK_H),
932 rail.fade(o),
933 TRACK_H * 0.5,
934 0.0,
935 Color::TRANSPARENT,
936 );
937 let lo = bounds.x + bounds.width * self.low;
938 let hi = bounds.x + bounds.width * self.high;
939 scene.draw_rect(
940 Rect::new(lo, track_y, (hi - lo).max(0.0), TRACK_H),
941 span.fade(o),
942 TRACK_H * 0.5,
943 0.0,
944 Color::TRANSPARENT,
945 );
946 }
948
949 fn on_click(&self) -> Option<Msg> {
950 None
951 }
952
953 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
954 let pct = |v: f32| (v * 100.0).round();
956 let semantics = frus_core::SemanticsProperties::new(frus_core::Role::Slider)
957 .value(format!("{}%–{}%", pct(self.low), pct(self.high)));
958 Some(if self.enabled {
959 semantics
960 } else {
961 semantics.disabled(true)
962 })
963 }
964
965 fn draggable(&self) -> bool {
966 self.enabled && self.on_change.is_some()
969 }
970
971 fn on_drag(&self, fraction: f32) -> Option<Msg> {
972 if !self.enabled {
973 return None;
974 }
975 let (low, high) = self.interval_at(fraction);
977 self.on_change.as_ref().map(|make| make(low, high))
978 }
979
980 fn on_drag_start(&self, fraction: f32) -> Option<Msg> {
981 if !self.enabled {
982 return None;
983 }
984 let (low, high) = self.interval_at(fraction);
985 self.on_change_start.as_ref().map(|make| make(low, high))
986 }
987
988 fn on_drag_end(&self, fraction: f32) -> Option<Msg> {
989 if !self.enabled {
990 return None;
991 }
992 let (low, high) = self.interval_at(fraction);
993 self.on_change_end.as_ref().map(|make| make(low, high))
994 }
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000
1001 #[derive(Clone, Debug, PartialEq)]
1002 enum Msg {
1003 Value(f32),
1004 Start(f32),
1005 End(f32),
1006 Range(f32, f32),
1007 RangeStart(f32, f32),
1008 RangeEnd(f32, f32),
1009 }
1010
1011 #[test]
1014 fn a_slider_that_was_not_asked_sends_no_bracket() {
1015 let slider = Slider::new(0.0).on_change(Msg::Value);
1016 assert_eq!(Widget::on_drag_start(&slider, 0.5), None);
1017 assert_eq!(Widget::on_drag_end(&slider, 0.5), None);
1018 }
1019
1020 #[test]
1025 fn the_bracket_speaks_in_values_not_fractions() {
1026 let slider = Slider::new(0.0)
1027 .range(0.0, 100.0)
1028 .on_change(Msg::Value)
1029 .on_change_start(Msg::Start)
1030 .on_change_end(Msg::End);
1031 assert_eq!(Widget::on_drag_start(&slider, 0.25), Some(Msg::Start(25.0)));
1032 assert_eq!(Widget::on_drag(&slider, 0.25), Some(Msg::Value(25.0)));
1033 assert_eq!(Widget::on_drag_end(&slider, 0.75), Some(Msg::End(75.0)));
1034 }
1035
1036 #[test]
1039 fn the_bracket_snaps_like_the_stream() {
1040 let slider = Slider::new(0.0)
1041 .divisions(4)
1042 .on_change(Msg::Value)
1043 .on_change_start(Msg::Start)
1044 .on_change_end(Msg::End);
1045 assert_eq!(Widget::on_drag(&slider, 0.3), Some(Msg::Value(0.25)));
1047 assert_eq!(Widget::on_drag_start(&slider, 0.3), Some(Msg::Start(0.25)));
1048 assert_eq!(Widget::on_drag_end(&slider, 0.3), Some(Msg::End(0.25)));
1049 }
1050
1051 #[test]
1054 fn a_disabled_slider_brackets_nothing() {
1055 let slider = Slider::new(0.0)
1056 .enabled(false)
1057 .on_change(Msg::Value)
1058 .on_change_start(Msg::Start)
1059 .on_change_end(Msg::End);
1060 assert_eq!(Widget::on_drag_start(&slider, 0.5), None);
1061 assert_eq!(Widget::on_drag(&slider, 0.5), None);
1062 assert_eq!(Widget::on_drag_end(&slider, 0.5), None);
1063 }
1064
1065 #[test]
1068 fn a_range_slider_brackets_the_interval() {
1069 let range = RangeSlider::new(0.2, 0.8)
1070 .on_change(Msg::Range)
1071 .on_change_start(Msg::RangeStart)
1072 .on_change_end(Msg::RangeEnd);
1073 assert_eq!(
1075 Widget::on_drag_start(&range, 0.3),
1076 Some(Msg::RangeStart(0.3, 0.8))
1077 );
1078 assert_eq!(Widget::on_drag(&range, 0.3), Some(Msg::Range(0.3, 0.8)));
1079 assert_eq!(
1081 Widget::on_drag_end(&range, 0.9),
1082 Some(Msg::RangeEnd(0.2, 0.9))
1083 );
1084 }
1085
1086 #[test]
1087 fn drag_maps_to_value() {
1088 let slider = Slider::new(0.0).on_change(Msg::Value);
1089 assert_eq!(Widget::on_drag(&slider, 0.5), Some(Msg::Value(0.5)));
1090 assert_eq!(Widget::on_drag(&slider, 1.5), Some(Msg::Value(1.0)));
1092 }
1093
1094 fn thumbs(rs: &RangeSlider<Msg>) -> Vec<&dyn Widget<Msg>> {
1096 let row = &Widget::<Msg>::children(rs)[0];
1097 row.children()
1098 .iter()
1099 .map(|c| c.as_ref())
1100 .filter(|c| c.draggable())
1101 .collect()
1102 }
1103
1104 fn range_of(msg: Option<Msg>) -> (f32, f32) {
1105 match msg {
1106 Some(Msg::Range(lo, hi)) => (lo, hi),
1107 other => panic!("expected Range, got {other:?}"),
1108 }
1109 }
1110
1111 fn range_of_key(resp: KeyResponse<Msg>) -> (f32, f32) {
1112 match resp {
1113 KeyResponse::Handled(msg) => range_of(msg),
1114 other => panic!("expected Handled, got {other:?}"),
1115 }
1116 }
1117
1118 #[test]
1119 fn each_thumb_moves_its_own_side_and_sticks() {
1120 let rs = RangeSlider::new(0.2, 0.8).on_change(Msg::Range); let t = thumbs(&rs);
1122 assert_eq!(t.len(), 2, "two draggable thumbs");
1123 let (lo, hi) = range_of(t[0].on_drag_delta(22.0));
1125 assert!(
1126 (lo - 0.3).abs() < 1e-4 && (hi - 0.8).abs() < 1e-4,
1127 "low moves ({lo}, {hi})"
1128 );
1129 let (lo, hi) = range_of(t[1].on_drag_delta(-22.0));
1131 assert!(
1132 (lo - 0.2).abs() < 1e-4 && (hi - 0.7).abs() < 1e-4,
1133 "high moves ({lo}, {hi})"
1134 );
1135 let (lo, hi) = range_of(t[0].on_drag_delta(10_000.0));
1137 assert!(
1138 (lo - 0.8).abs() < 1e-4 && (hi - 0.8).abs() < 1e-4,
1139 "stuck to the top ({lo}, {hi})"
1140 );
1141 assert_eq!(t[0].on_drag_delta(0.0), None);
1143 }
1144
1145 #[test]
1146 fn arrow_keys_move_focused_thumb_by_a_step() {
1147 let rs = RangeSlider::new(0.4, 0.6)
1148 .divisions(10)
1149 .on_change(Msg::Range); let t = thumbs(&rs);
1151 let (lo, hi) = range_of_key(t[0].on_key(&Key::Right {
1153 shift: false,
1154 word: false,
1155 }));
1156 assert!(
1157 (lo - 0.5).abs() < 1e-4 && (hi - 0.6).abs() < 1e-4,
1158 "→ ({lo}, {hi})"
1159 );
1160 let (lo, hi) = range_of_key(t[0].on_key(&Key::Left {
1161 shift: false,
1162 word: false,
1163 }));
1164 assert!(
1165 (lo - 0.3).abs() < 1e-4 && (hi - 0.6).abs() < 1e-4,
1166 "← ({lo}, {hi})"
1167 );
1168 assert!(t[0].focusable() && t[1].focusable());
1170 }
1171
1172 #[test]
1173 fn track_click_moves_nearest_thumb() {
1174 let rs = RangeSlider::new(0.2, 0.8).on_change(Msg::Range);
1175 assert!(Widget::<Msg>::draggable(&rs), "the track answers clicks");
1177 let (lo, hi) = range_of(Widget::on_drag(&rs, 0.25));
1178 assert!(
1179 (lo - 0.25).abs() < 1e-4 && (hi - 0.8).abs() < 1e-4,
1180 "near the low end ({lo}, {hi})"
1181 );
1182 let (lo, hi) = range_of(Widget::on_drag(&rs, 0.9));
1183 assert!(
1184 (lo - 0.2).abs() < 1e-4 && (hi - 0.9).abs() < 1e-4,
1185 "near the high end ({lo}, {hi})"
1186 );
1187 }
1188
1189 #[test]
1190 fn home_end_snap_thumb_to_bounds() {
1191 let rs = RangeSlider::new(0.3, 0.7).on_change(Msg::Range);
1192 let t = thumbs(&rs);
1193 let home = Key::Home {
1194 shift: false,
1195 doc: false,
1196 };
1197 let end = Key::End {
1198 shift: false,
1199 doc: false,
1200 };
1201 let (lo, hi) = range_of_key(t[0].on_key(&home));
1203 assert!(
1204 (lo - 0.0).abs() < 1e-4 && (hi - 0.7).abs() < 1e-4,
1205 "low Home ({lo}, {hi})"
1206 );
1207 let (lo, hi) = range_of_key(t[0].on_key(&end));
1208 assert!(
1209 (lo - 0.7).abs() < 1e-4 && (hi - 0.7).abs() < 1e-4,
1210 "low End ({lo}, {hi})"
1211 );
1212 let (lo, hi) = range_of_key(t[1].on_key(&end));
1214 assert!(
1215 (lo - 0.3).abs() < 1e-4 && (hi - 1.0).abs() < 1e-4,
1216 "upper End ({lo}, {hi})"
1217 );
1218 }
1219
1220 #[test]
1221 fn value_label_reserves_height() {
1222 let plain = RangeSlider::new(0.2, 0.8).on_change(Msg::Range);
1223 let tipped = RangeSlider::new(0.2, 0.8)
1224 .on_change(Msg::Range)
1225 .value_label(|v| format!("{}", v));
1226 let h = |rs: &RangeSlider<Msg>| match Widget::<Msg>::style(rs).height {
1227 Dimension::Length(v) => v,
1228 _ => 0.0,
1229 };
1230 assert!(h(&tipped) > h(&plain), "the tooltip reserves height");
1231 }
1232
1233 #[test]
1234 fn divisions_snap_to_steps() {
1235 let rs = RangeSlider::new(0.0, 1.0)
1237 .width(200.0)
1238 .divisions(10)
1239 .on_change(Msg::Range);
1240 let (lo, _) = range_of(thumbs(&rs)[0].on_drag_delta(25.0));
1241 assert!((lo - 0.1).abs() < 1e-4, "snapped to step 0.1, got {lo}");
1242 }
1243
1244 #[test]
1245 fn range_new_orders_and_clamps() {
1246 let rs = RangeSlider::new(0.9, 0.1).on_change(Msg::Range);
1248 let (lo, hi) = range_of(thumbs(&rs)[0].on_drag_delta(0.05 * 220.0));
1250 assert!(
1251 (lo - 0.15).abs() < 1e-4 && (hi - 0.9).abs() < 1e-4,
1252 "reordered ({lo}, {hi})"
1253 );
1254 }
1255
1256 fn thumbs_by_position(rs: &RangeSlider<Msg>) -> Vec<&dyn Widget<Msg>> {
1260 let row = &Widget::<Msg>::children(rs)[0];
1261 let kids = row.children();
1262 vec![kids[1].as_ref(), kids[3].as_ref()]
1263 }
1264
1265 #[test]
1266 fn a_disabled_slider_is_inert_but_still_says_where_it_sits() {
1267 let dead = Slider::new(0.4).on_change(Msg::Value).enabled(false);
1268 assert!(!Widget::<Msg>::draggable(&dead), "it takes no drag");
1269 assert_eq!(
1270 Widget::on_drag(&dead, 0.9),
1271 None,
1272 "and a drag in flight does not land"
1273 );
1274 let semantics = Widget::<Msg>::semantics(&dead).expect("still announced");
1275 assert!(semantics.disabled, "announced as unavailable");
1276 assert_eq!(semantics.value.as_deref(), Some("40%"), "value survives");
1277 }
1278
1279 #[test]
1283 fn a_disabled_range_slider_takes_no_drag_no_key_and_no_focus() {
1284 let dead = RangeSlider::new(0.2, 0.8)
1285 .on_change(Msg::Range)
1286 .enabled(false);
1287 assert!(!Widget::<Msg>::draggable(&dead), "the track takes no drag");
1288 assert_eq!(Widget::on_drag(&dead, 0.5), None);
1289 for (i, thumb) in thumbs_by_position(&dead).into_iter().enumerate() {
1290 assert!(!thumb.draggable(), "thumb {i} still drags");
1291 assert!(!thumb.focusable(), "thumb {i} still takes focus");
1292 assert_eq!(thumb.on_drag_delta(20.0), None, "thumb {i} still moves");
1293 assert!(
1294 matches!(
1295 thumb.on_key(&Key::Right {
1296 shift: false,
1297 word: false
1298 }),
1299 KeyResponse::Ignored
1300 ),
1301 "thumb {i} still answers an arrow"
1302 );
1303 }
1304 let semantics = Widget::<Msg>::semantics(&dead).expect("still announced");
1305 assert!(semantics.disabled);
1306 assert_eq!(
1307 semantics.value.as_deref(),
1308 Some("20%–80%"),
1309 "interval survives"
1310 );
1311 }
1312
1313 #[test]
1316 fn a_disabled_slider_takes_both_halves_of_the_rule() {
1317 for theme in [Theme::dark(), Theme::light()] {
1318 let mut scene = Scene::new();
1319 Widget::<Msg>::paint(
1320 &Slider::<Msg>::new(0.5).enabled(false),
1321 Rect::new(0.0, 0.0, 200.0, H),
1322 Status {
1323 opacity: 1.0,
1324 ..Default::default()
1325 },
1326 &theme,
1327 &mut scene,
1328 );
1329 let fills: Vec<frus_core::Color> = scene
1331 .primitives()
1332 .iter()
1333 .filter_map(|p| match p {
1334 frus_core::Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
1335 _ => None,
1336 })
1337 .collect();
1338 assert_eq!(fills[0], disabled_container(&theme), "the rail");
1339 assert_eq!(fills[1], disabled_content(&theme), "the travelled part");
1340 assert_eq!(fills[2], disabled_content(&theme), "the thumb");
1341 let from_surface = |c: Color| {
1345 (c.r - theme.scheme.surface.r).abs()
1346 + (c.g - theme.scheme.surface.g).abs()
1347 + (c.b - theme.scheme.surface.b).abs()
1348 };
1349 assert!(
1350 from_surface(fills[0]) < from_surface(fills[1]),
1351 "the rail is the quieter of the two"
1352 );
1353 }
1354 }
1355 #[test]
1359 fn a_thumb_casts_a_shadow_that_rises_while_it_is_held() {
1360 fn heights(widget: &dyn Widget<Msg>, bounds: Rect, press: f32, theme: &Theme) -> Vec<f32> {
1362 let status = Status {
1363 opacity: 1.0,
1364 press_progress: press,
1365 ..Default::default()
1366 };
1367 let mut scene = Scene::new();
1368 widget.paint(bounds, status, theme, &mut scene);
1369 scene
1370 .primitives()
1371 .iter()
1372 .filter_map(|p| match p {
1373 frus_core::Primitive::Rect { blur, .. } if *blur > 0.0 => {
1374 Some((blur - 4.0) / 2.0)
1375 }
1376 _ => None,
1377 })
1378 .collect()
1379 }
1380 let theme = Theme::default();
1381 let track = Rect::new(0.0, 0.0, 220.0, H);
1382 let slider = Slider::new(0.5).on_change(Msg::Value);
1383 assert_eq!(heights(&slider, track, 0.0, &theme), vec![1.0]);
1384 assert_eq!(heights(&slider, track, 1.0, &theme), vec![6.0]);
1385 assert_eq!(
1386 heights(&slider, track, 0.5, &theme),
1387 vec![3.5],
1388 "on its way up"
1389 );
1390 let grounded = Slider::new(0.5)
1391 .on_change(Msg::Value)
1392 .thumb_elevation(0.0)
1393 .pressed_thumb_elevation(0.0);
1394 assert!(heights(&grounded, track, 1.0, &theme).is_empty());
1395
1396 let mut flat = Theme::default();
1397 flat.widgets.slider.thumb_elevation = Some(0.0);
1398 flat.widgets.slider.pressed_thumb_elevation = Some(0.0);
1399 assert!(
1400 heights(&slider, track, 1.0, &flat).is_empty(),
1401 "the theme's word"
1402 );
1403 let told = Slider::new(0.5).on_change(Msg::Value).thumb_elevation(2.0);
1404 assert_eq!(
1405 heights(&told, track, 0.0, &flat),
1406 vec![2.0],
1407 "and the slider's over it"
1408 );
1409
1410 let cell = Rect::new(0.0, 0.0, THUMB, H);
1411 let range = RangeSlider::new(0.25, 0.75).on_change(Msg::Range);
1412 for thumb in thumbs(&range) {
1413 assert_eq!(heights(thumb, cell, 0.0, &theme), vec![1.0]);
1414 assert_eq!(heights(thumb, cell, 1.0, &theme), vec![6.0]);
1415 }
1416 let raised = RangeSlider::new(0.25, 0.75)
1417 .on_change(Msg::Range)
1418 .thumb_elevation(3.0);
1419 for thumb in thumbs(&raised) {
1420 assert_eq!(
1421 heights(thumb, cell, 0.0, &theme),
1422 vec![3.0],
1423 "told on the slider"
1424 );
1425 }
1426 }
1427}
1428
1429#[cfg(test)]
1430mod range_tests {
1431 use super::*;
1432 use crate::interaction::{Key, KeyResponse};
1433
1434 #[derive(Clone, Debug, PartialEq)]
1435 enum Msg {
1436 Value(f32),
1437 }
1438
1439 fn value_of(msg: Option<Msg>) -> f32 {
1440 match msg {
1441 Some(Msg::Value(v)) => v,
1442 other => panic!("expected a value, got {other:?}"),
1443 }
1444 }
1445
1446 #[test]
1448 fn a_drag_lands_in_the_caller_s_units() {
1449 let slider = Slider::new(20.0).range(20.0, 200.0).on_change(Msg::Value);
1450 assert_eq!(value_of(Widget::on_drag(&slider, 0.0)), 20.0);
1451 assert_eq!(value_of(Widget::on_drag(&slider, 0.5)), 110.0);
1452 assert_eq!(value_of(Widget::on_drag(&slider, 1.0)), 200.0);
1453 assert_eq!(value_of(Widget::on_drag(&slider, 1.4)), 200.0);
1455 }
1456
1457 #[test]
1459 fn a_backwards_range_is_taken_the_way_round_it_makes_sense() {
1460 let slider = Slider::new(0.0).range(200.0, 20.0).on_change(Msg::Value);
1461 assert_eq!(value_of(Widget::on_drag(&slider, 0.0)), 20.0);
1462 assert_eq!(value_of(Widget::on_drag(&slider, 1.0)), 200.0);
1463 }
1464
1465 #[test]
1468 fn a_value_outside_the_travel_is_held_inside_it() {
1469 let low = Slider::<Msg>::new(-5.0).range(0.0, 10.0);
1470 assert_eq!(low.fraction(), 0.0);
1471 let high = Slider::<Msg>::new(40.0).range(0.0, 10.0);
1472 assert_eq!(high.fraction(), 1.0);
1473 }
1474
1475 #[test]
1477 fn divisions_snap_the_value() {
1478 let slider = Slider::new(0.0)
1479 .range(0.0, 100.0)
1480 .divisions(4)
1481 .on_change(Msg::Value);
1482 assert_eq!(value_of(Widget::on_drag(&slider, 0.30)), 25.0);
1483 assert_eq!(value_of(Widget::on_drag(&slider, 0.40)), 50.0);
1484 assert_eq!(value_of(Widget::on_drag(&slider, 0.99)), 100.0);
1485 }
1486
1487 #[test]
1490 fn the_arrows_move_by_a_step() {
1491 let free = Slider::new(50.0).range(0.0, 100.0).on_change(Msg::Value);
1492 let step = |s: &Slider<Msg>, key: Key| match Widget::on_key(s, &key) {
1493 KeyResponse::Handled(msg) => value_of(msg),
1494 other => panic!("expected the key to be taken, got {other:?}"),
1495 };
1496 assert_eq!(
1497 step(
1498 &free,
1499 Key::Right {
1500 shift: false,
1501 word: false
1502 }
1503 ),
1504 55.0
1505 );
1506 assert_eq!(
1507 step(
1508 &free,
1509 Key::Left {
1510 shift: false,
1511 word: false
1512 }
1513 ),
1514 45.0
1515 );
1516
1517 let stepped = Slider::new(50.0)
1518 .range(0.0, 100.0)
1519 .divisions(4)
1520 .on_change(Msg::Value);
1521 assert_eq!(
1522 step(
1523 &stepped,
1524 Key::Right {
1525 shift: false,
1526 word: false
1527 }
1528 ),
1529 75.0
1530 );
1531
1532 let at_end = Slider::new(100.0).range(0.0, 100.0).on_change(Msg::Value);
1533 assert_eq!(
1534 step(
1535 &at_end,
1536 Key::Right {
1537 shift: false,
1538 word: false
1539 }
1540 ),
1541 100.0
1542 );
1543 assert_eq!(
1544 step(
1545 &at_end,
1546 Key::Home {
1547 shift: false,
1548 doc: false
1549 }
1550 ),
1551 0.0
1552 );
1553 assert_eq!(
1554 step(
1555 &at_end,
1556 Key::End {
1557 shift: false,
1558 doc: false
1559 }
1560 ),
1561 100.0
1562 );
1563 }
1564
1565 #[test]
1567 fn it_takes_the_focus_only_when_it_can_answer() {
1568 assert!(!Widget::<Msg>::focusable(&Slider::<Msg>::new(0.5)));
1569 assert!(Widget::focusable(&Slider::new(0.5).on_change(Msg::Value)));
1570 assert!(!Widget::focusable(
1571 &Slider::new(0.5).on_change(Msg::Value).enabled(false)
1572 ));
1573 }
1574
1575 #[test]
1577 fn a_frozen_slider_answers_no_key() {
1578 let slider = Slider::new(0.5).on_change(Msg::Value).enabled(false);
1579 assert!(matches!(
1580 Widget::on_key(
1581 &slider,
1582 &Key::Right {
1583 shift: false,
1584 word: false
1585 }
1586 ),
1587 KeyResponse::Ignored
1588 ));
1589 }
1590
1591 #[test]
1593 fn a_reader_is_told_the_real_value() {
1594 let plain = Slider::<Msg>::new(0.25);
1595 let s = Widget::<Msg>::semantics(&plain).expect("a slider says where it is");
1596 assert_eq!(
1597 s.value.as_deref(),
1598 Some("25%"),
1599 "a bare 0..1 reads as a share"
1600 );
1601
1602 let ranged = Slider::<Msg>::new(110.0).range(20.0, 200.0);
1603 let s = Widget::<Msg>::semantics(&ranged).expect("a slider says where it is");
1604 assert_eq!(s.range, Some((20.0, 110.0, 200.0)));
1605
1606 let priced = Slider::<Msg>::new(110.0)
1607 .range(20.0, 200.0)
1608 .value_label(|v| format!("{v} EUR"));
1609 let s = Widget::<Msg>::semantics(&priced).expect("a slider says where it is");
1610 assert_eq!(
1611 s.value.as_deref(),
1612 Some("110 EUR"),
1613 "said the caller's way when it gave a formatter"
1614 );
1615 }
1616
1617 #[test]
1622 fn the_value_bubble_wears_the_reference_s_step() {
1623 let slider = Slider::<Msg>::new(0.3).value_label(|v| format!("{v}"));
1624 let styles = |theme: &Theme| {
1625 let mut scene = Scene::new();
1626 Widget::<Msg>::paint(
1627 &slider,
1628 Rect::new(0.0, 0.0, 220.0, 50.0),
1629 Status {
1630 focused: true,
1631 ..Status::default()
1632 },
1633 theme,
1634 &mut scene,
1635 );
1636 scene
1637 .primitives()
1638 .iter()
1639 .filter_map(|p| match p {
1640 frus_core::Primitive::Text { size, weight, .. } => {
1641 Some((*size, weight.to_u16()))
1642 }
1643 _ => None,
1644 })
1645 .collect::<Vec<_>>()
1646 };
1647 let plain = Theme::default();
1648 assert_eq!(
1649 styles(&plain),
1650 vec![(
1651 plain.text.label_large.size.unwrap(),
1652 frus_core::FontWeight::Medium.to_u16()
1653 )]
1654 );
1655 let mut themed = Theme::default();
1657 themed.widgets.slider.value_indicator_text_style = Some(frus_core::TextStyle::new(25.0));
1658 assert_eq!(styles(&themed).first().map(|s| s.0), Some(25.0));
1659 }
1660
1661 #[test]
1663 fn a_tooltip_makes_the_control_taller() {
1664 let bare = Widget::<Msg>::style(&Slider::<Msg>::new(0.5)).height;
1665 let tipped =
1666 Widget::<Msg>::style(&Slider::<Msg>::new(0.5).value_label(|v| format!("{v}"))).height;
1667 assert_eq!(bare, frus_layout::Dimension::Length(H));
1668 assert_eq!(
1669 tipped,
1670 frus_layout::Dimension::Length(H + TIP_H + TIP_GAP),
1671 "the tooltip's zone is above the track, not over it"
1672 );
1673 }
1674}
1675
1676#[cfg(test)]
1677mod color_tests {
1678 use super::*;
1679 use frus_core::Primitive;
1680
1681 const BRAND: Color = Color::rgb(0.0, 0.6, 0.3);
1682 const RAIL: Color = Color::rgb(0.9, 0.9, 0.2);
1683
1684 fn painted(slider: &Slider<()>, theme: &Theme) -> (Color, Color, Color, Color) {
1687 let mut scene = Scene::new();
1688 Widget::<()>::paint(
1689 slider,
1690 Rect::new(0.0, 0.0, 220.0, H),
1691 Status {
1692 opacity: 1.0,
1693 ..Default::default()
1694 },
1695 theme,
1696 &mut scene,
1697 );
1698 let rects: Vec<(Color, Color)> = scene
1699 .primitives()
1700 .iter()
1701 .filter_map(|p| match p {
1702 Primitive::Rect {
1703 color,
1704 border_color,
1705 blur,
1706 ..
1707 } if *blur == 0.0 => Some((*color, *border_color)),
1708 _ => None,
1709 })
1710 .collect();
1711 (rects[0].0, rects[1].0, rects[2].0, rects[2].1)
1712 }
1713
1714 #[test]
1716 fn the_defaults_are_what_they_were() {
1717 let theme = Theme::default();
1718 let (rail, filled, thumb, ring) = painted(&Slider::<()>::new(0.5), &theme);
1719 assert_eq!(rail, theme.scheme.secondary_container);
1720 assert_eq!(filled, theme.primary);
1721 assert_eq!(thumb, Color::WHITE);
1722 assert_eq!(ring, theme.primary);
1723 }
1724
1725 #[test]
1728 fn the_ring_follows_the_track_it_was_not_told_about() {
1729 let theme = Theme::default();
1730 let (_, filled, _, ring) = painted(&Slider::<()>::new(0.5).active_color(BRAND), &theme);
1731 assert_eq!((filled, ring), (BRAND, BRAND));
1732 let (_, _, _, named) = painted(
1733 &Slider::<()>::new(0.5)
1734 .active_color(BRAND)
1735 .thumb_border_color(RAIL),
1736 &theme,
1737 );
1738 assert_eq!(named, RAIL, "unless it is named");
1739 }
1740
1741 #[test]
1743 fn each_half_of_the_track_takes_its_own_colour() {
1744 let theme = Theme::default();
1745 let (rail, filled, thumb, _) = painted(
1746 &Slider::<()>::new(0.5)
1747 .active_color(BRAND)
1748 .inactive_color(RAIL)
1749 .thumb_color(RAIL),
1750 &theme,
1751 );
1752 assert_eq!((rail, filled, thumb), (RAIL, BRAND, RAIL));
1753 }
1754
1755 #[test]
1757 fn the_theme_answers_and_the_instance_overrules_it() {
1758 let mut theme = Theme::default();
1759 theme.widgets.slider.active_track_color = Some(RAIL);
1760 assert_eq!(painted(&Slider::<()>::new(0.5), &theme).1, RAIL);
1761 assert_eq!(
1762 painted(&Slider::<()>::new(0.5).active_color(BRAND), &theme).1,
1763 BRAND
1764 );
1765 }
1766}