Skip to main content

frus_widgets/
slidershape.rs

1//! The parts a [`Slider`](crate::Slider) and a [`RangeSlider`](crate::RangeSlider) are
2//! painted with, each replaceable: the track, the tick marks, the thumb, the halo around it
3//! and the value indicator over it.
4//!
5//! A slider asks its own part first, then the theme's ([`SliderTheme`](crate::SliderTheme),
6//! which keeps a range slider's track, ticks, thumbs and indicator apart from a slider's), then
7//! takes the default. The defaults are the reference's parts, and paint what the sliders always
8//! painted.
9//!
10//! The parts paint; the slider keeps the geometry. The box, the track's inset by the halo's
11//! radius and the drag's travel stay the slider's, so a larger custom thumb or halo overflows
12//! the box rather than growing it.
13
14use std::fmt;
15use std::rc::Rc;
16
17use frus_core::{BorderRadius, Color, Point, Rect, Scene};
18
19use crate::theme::Theme;
20
21/// A slider's colours, resolved for one paint: the widget's, then the theme's, then the
22/// reference's — the disabled ones when it is disabled.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct SliderPalette {
25    /// The travelled part, or a range slider's span between its thumbs.
26    pub active_track: Color,
27    /// The rest of the track.
28    pub inactive_track: Color,
29    /// A slider's secondary value, from the thumb towards it.
30    pub secondary_track: Color,
31    /// The thumb's fill.
32    pub thumb: Color,
33    /// A ring around the thumb, if one is asked for.
34    pub thumb_border: Option<Color>,
35    /// A tick mark on the active part.
36    pub active_tick_mark: Color,
37    /// A tick mark on the rest.
38    pub inactive_tick_mark: Color,
39}
40
41/// What every part is painted with.
42#[derive(Clone, Copy, Debug)]
43pub struct SliderPaint<'a> {
44    /// The theme in force where the slider is.
45    pub theme: &'a Theme,
46    /// The slider's colours.
47    pub palette: SliderPalette,
48    /// Whether the slider can be moved.
49    pub enabled: bool,
50    /// Whether it has divisions.
51    pub discrete: bool,
52    /// The opacity everything is painted at.
53    pub opacity: f32,
54}
55
56impl SliderPaint<'_> {
57    /// Whether the travel runs from the right.
58    pub fn is_rtl(&self) -> bool {
59        self.theme.direction.is_rtl()
60    }
61}
62
63/// How a thumb is to be painted this time.
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct SliderThumb {
66    /// How far it stands off the track: the resting height, or on its way to the pressed one.
67    pub elevation: f32,
68    /// On a range slider, whether it lies over the other thumb — the reference rings the one
69    /// on top so that the two stay two.
70    pub on_top: bool,
71}
72
73/// Paints a slider's **track**: the reference's `SliderTrackShape` and
74/// `RangeSliderTrackShape` in one.
75pub trait SliderTrackShape {
76    /// Paints the track along `track` — the travel, inset in the slider's box, as tall as the
77    /// theme's track height — with the thumbs' centres across at `thumbs`: one for a slider,
78    /// the start and the end for a range slider. `secondary` is where a slider's secondary
79    /// value lies, if it has one.
80    fn paint(
81        &self,
82        scene: &mut Scene,
83        track: Rect,
84        thumbs: &[f32],
85        secondary: Option<f32>,
86        paint: &SliderPaint<'_>,
87    );
88}
89
90/// Paints a **tick mark**, one per division: the reference's `SliderTickMarkShape`.
91pub trait SliderTickMarkShape {
92    /// The mark's width on a track `track_height` tall. The slider leaves the marks out when
93    /// three of them would not fit between two, and when this is nothing.
94    fn width(&self, track_height: f32) -> f32;
95
96    /// Paints a mark centred on `centre`, on the active part of the track or not.
97    fn paint(
98        &self,
99        scene: &mut Scene,
100        centre: Point,
101        active: bool,
102        track_height: f32,
103        paint: &SliderPaint<'_>,
104    );
105}
106
107/// Paints a **thumb**: the reference's thumb shapes, a slider's and a range slider's.
108pub trait SliderThumbShape {
109    /// Paints the thumb centred on `centre`.
110    fn paint(&self, scene: &mut Scene, centre: Point, thumb: SliderThumb, paint: &SliderPaint<'_>);
111}
112
113/// Paints the **halo** around a hovered, focused or held thumb: the reference's overlay
114/// shape.
115pub trait SliderOverlayShape {
116    /// Paints the halo centred on `centre`, grown to `activation` (`0..=1`) in `color`.
117    fn paint(
118        &self,
119        scene: &mut Scene,
120        centre: Point,
121        activation: f32,
122        color: Color,
123        paint: &SliderPaint<'_>,
124    );
125}
126
127/// Paints the **value indicator** over a thumb: the reference's value indicator shapes.
128pub trait SliderValueIndicatorShape {
129    /// Paints `text` over the thumb centred on `centre`, grown to `scale` (`0..=1`).
130    fn paint(
131        &self,
132        scene: &mut Scene,
133        centre: Point,
134        text: String,
135        scale: f32,
136        paint: &SliderPaint<'_>,
137    );
138}
139
140/// A part held by a slider or a theme: shared, and compared by identity, so that a theme
141/// holding one still compares and clones as a value does.
142pub struct SliderShape<T: ?Sized>(Rc<T>);
143
144impl<T: ?Sized> Clone for SliderShape<T> {
145    fn clone(&self) -> Self {
146        Self(self.0.clone())
147    }
148}
149
150impl<T: ?Sized> PartialEq for SliderShape<T> {
151    fn eq(&self, other: &Self) -> bool {
152        Rc::ptr_eq(&self.0, &other.0)
153    }
154}
155
156impl<T: ?Sized> fmt::Debug for SliderShape<T> {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.write_str("SliderShape(..)")
159    }
160}
161
162impl<T: ?Sized> std::ops::Deref for SliderShape<T> {
163    type Target = T;
164
165    fn deref(&self) -> &T {
166        &self.0
167    }
168}
169
170impl SliderShape<dyn SliderTrackShape> {
171    /// A track shape to hold.
172    pub fn track(shape: impl SliderTrackShape + 'static) -> Self {
173        Self(Rc::new(shape))
174    }
175}
176
177impl SliderShape<dyn SliderTickMarkShape> {
178    /// A tick mark shape to hold.
179    pub fn tick_mark(shape: impl SliderTickMarkShape + 'static) -> Self {
180        Self(Rc::new(shape))
181    }
182}
183
184impl SliderShape<dyn SliderThumbShape> {
185    /// A thumb shape to hold.
186    pub fn thumb(shape: impl SliderThumbShape + 'static) -> Self {
187        Self(Rc::new(shape))
188    }
189}
190
191impl SliderShape<dyn SliderOverlayShape> {
192    /// A halo shape to hold.
193    pub fn overlay(shape: impl SliderOverlayShape + 'static) -> Self {
194        Self(Rc::new(shape))
195    }
196}
197
198impl SliderShape<dyn SliderValueIndicatorShape> {
199    /// A value indicator shape to hold.
200    pub fn value_indicator(shape: impl SliderValueIndicatorShape + 'static) -> Self {
201        Self(Rc::new(shape))
202    }
203}
204
205/// The part the widget names, else the theme's, else `fallback`.
206pub(crate) fn pick<'a, T: ?Sized>(
207    widget: &'a Option<SliderShape<T>>,
208    theme: &'a Option<SliderShape<T>>,
209    fallback: &'a T,
210) -> &'a T {
211    widget
212        .as_ref()
213        .or(theme.as_ref())
214        .map_or(fallback, |shape| &**shape)
215}
216
217/// How much thicker a slider's travelled part, or a range slider's span, is than the rest.
218const ACTIVE_EXTRA: f32 = 2.0;
219
220/// A **slider's** default track, the reference's rounded rectangle: the travelled part from
221/// the start to the thumb, two pixels thicker and rounded; the rest from the thumb to the end;
222/// each reaching half a track past the thumb, under it. A secondary value beyond the thumb
223/// runs from it, rounded at its far end.
224#[derive(Clone, Copy, Debug, Default, PartialEq)]
225pub struct RoundedRectSliderTrackShape;
226
227impl SliderTrackShape for RoundedRectSliderTrackShape {
228    fn paint(
229        &self,
230        scene: &mut Scene,
231        track: Rect,
232        thumbs: &[f32],
233        secondary: Option<f32>,
234        paint: &SliderPaint<'_>,
235    ) {
236        let track_h = track.height;
237        let Some(&thumb) = thumbs.first() else {
238            return;
239        };
240        if track_h <= 0.0 {
241            return;
242        }
243        let o = paint.opacity;
244        let rtl = paint.is_rtl();
245        let palette = &paint.palette;
246        let half = track_h * 0.5;
247        let cy = track.y + half;
248        let active_half = (track_h + ACTIVE_EXTRA) * 0.5;
249        let (left, right) = (track.x, track.x + track.width);
250        let (a0, a1) = if rtl {
251            (thumb - half, right)
252        } else {
253            (left, thumb + half)
254        };
255        let (i0, i1) = if rtl {
256            (left, thumb + half)
257        } else {
258            (thumb - half, right)
259        };
260        if i1 - i0 > track_h {
261            scene.draw_rect(
262                Rect::new(i0, cy - half, i1 - i0, track_h),
263                palette.inactive_track.fade(o),
264                half,
265                0.0,
266                Color::TRANSPARENT,
267            );
268        }
269        if a1 - a0 > track_h {
270            scene.draw_rect(
271                Rect::new(a0, cy - active_half, a1 - a0, 2.0 * active_half),
272                palette.active_track.fade(o),
273                active_half,
274                0.0,
275                Color::TRANSPARENT,
276            );
277        }
278        // The secondary value: from the thumb towards it, when it lies beyond.
279        if let Some(sx) = secondary {
280            let beyond = if rtl { sx < thumb } else { sx > thumb };
281            if beyond {
282                let (s0, s1) = if rtl { (sx, thumb) } else { (thumb, sx) };
283                let radius = if rtl {
284                    BorderRadius {
285                        top_left: half,
286                        bottom_left: half,
287                        ..BorderRadius::ZERO
288                    }
289                } else {
290                    BorderRadius {
291                        top_right: half,
292                        bottom_right: half,
293                        ..BorderRadius::ZERO
294                    }
295                };
296                scene.draw_rect(
297                    Rect::new(s0, cy - half, s1 - s0, track_h),
298                    palette.secondary_track.fade(o),
299                    radius,
300                    0.0,
301                    Color::TRANSPARENT,
302                );
303            }
304        }
305    }
306}
307
308/// A **range slider's** default track, the reference's rounded rectangle: the parts outside
309/// the thumbs rounded, the span between them square and two pixels thicker.
310#[derive(Clone, Copy, Debug, Default, PartialEq)]
311pub struct RoundedRectRangeSliderTrackShape;
312
313impl SliderTrackShape for RoundedRectRangeSliderTrackShape {
314    fn paint(
315        &self,
316        scene: &mut Scene,
317        track: Rect,
318        thumbs: &[f32],
319        _secondary: Option<f32>,
320        paint: &SliderPaint<'_>,
321    ) {
322        let track_h = track.height;
323        let (Some(&x0), Some(&x1)) = (thumbs.first(), thumbs.get(1)) else {
324            return;
325        };
326        if track_h <= 0.0 {
327            return;
328        }
329        let o = paint.opacity;
330        let palette = &paint.palette;
331        let (lo, hi) = (x0.min(x1), x0.max(x1));
332        let half = track_h * 0.5;
333        let cy = track.y + half;
334        let active_half = (track_h + ACTIVE_EXTRA) * 0.5;
335        let (left, right) = (track.x, track.x + track.width);
336        for (a, b) in [(left, lo + half), (hi - half, right)] {
337            if b - a > track_h {
338                scene.draw_rect(
339                    Rect::new(a, cy - half, b - a, track_h),
340                    palette.inactive_track.fade(o),
341                    half,
342                    0.0,
343                    Color::TRANSPARENT,
344                );
345            }
346        }
347        if hi - lo > 0.0 {
348            scene.draw_rect(
349                Rect::new(lo, cy - active_half, hi - lo, 2.0 * active_half),
350                palette.active_track.fade(o),
351                0.0,
352                0.0,
353                Color::TRANSPARENT,
354            );
355        }
356    }
357}
358
359/// The reference's **round tick mark**: a dot `radius` across from its centre, a quarter of
360/// the track's height unless given.
361#[derive(Clone, Copy, Debug, Default, PartialEq)]
362pub struct RoundSliderTickMarkShape {
363    /// The dot's radius; a quarter of the track's height when `None`.
364    pub radius: Option<f32>,
365}
366
367impl RoundSliderTickMarkShape {
368    fn radius_on(&self, track_height: f32) -> f32 {
369        self.radius.unwrap_or(track_height / 4.0)
370    }
371}
372
373impl SliderTickMarkShape for RoundSliderTickMarkShape {
374    fn width(&self, track_height: f32) -> f32 {
375        2.0 * self.radius_on(track_height)
376    }
377
378    fn paint(
379        &self,
380        scene: &mut Scene,
381        centre: Point,
382        active: bool,
383        track_height: f32,
384        paint: &SliderPaint<'_>,
385    ) {
386        let r = self.radius_on(track_height);
387        let color = if active {
388            paint.palette.active_tick_mark
389        } else {
390            paint.palette.inactive_tick_mark
391        };
392        scene.draw_rect(
393            Rect::new(centre.x - r, centre.y - r, 2.0 * r, 2.0 * r),
394            color.fade(paint.opacity),
395            r,
396            0.0,
397            Color::TRANSPARENT,
398        );
399    }
400}
401
402/// The reference's **round thumb**: a disc `radius` across from its centre over the shadow
403/// its elevation casts. A ring two pixels wide when the palette has a thumb border; on a range
404/// slider, a one-pixel ring in the surface colour on the thumb on top.
405#[derive(Clone, Copy, Debug, PartialEq)]
406pub struct RoundSliderThumbShape {
407    /// The disc's radius; the reference's 10.
408    pub radius: f32,
409}
410
411impl Default for RoundSliderThumbShape {
412    fn default() -> Self {
413        Self { radius: 10.0 }
414    }
415}
416
417impl SliderThumbShape for RoundSliderThumbShape {
418    fn paint(&self, scene: &mut Scene, centre: Point, thumb: SliderThumb, paint: &SliderPaint<'_>) {
419        let o = paint.opacity;
420        let r = self.radius;
421        let disc = Rect::new(centre.x - r, centre.y - r, 2.0 * r, 2.0 * r);
422        if thumb.elevation > 0.0 {
423            // The reference's shadows for this height (milestone 606).
424            frus_core::paint_elevation(
425                scene,
426                disc,
427                BorderRadius::uniform(r),
428                thumb.elevation,
429                paint.theme.scheme.shadow.fade(o),
430            );
431        }
432        let (ring_width, ring) = if thumb.on_top {
433            (1.0, paint.theme.scheme.surface.fade(o))
434        } else if let Some(border) = paint.palette.thumb_border {
435            (2.0, border.fade(o))
436        } else {
437            (0.0, Color::TRANSPARENT)
438        };
439        scene.draw_rect(disc, paint.palette.thumb.fade(o), r, ring_width, ring);
440    }
441}
442
443/// The reference's **round halo**: a disc `radius` across at full activation, grown from the
444/// thumb's centre.
445#[derive(Clone, Copy, Debug, PartialEq)]
446pub struct RoundSliderOverlayShape {
447    /// The disc's radius at full activation; the reference's 24.
448    pub radius: f32,
449}
450
451impl Default for RoundSliderOverlayShape {
452    fn default() -> Self {
453        Self { radius: 24.0 }
454    }
455}
456
457impl SliderOverlayShape for RoundSliderOverlayShape {
458    fn paint(
459        &self,
460        scene: &mut Scene,
461        centre: Point,
462        activation: f32,
463        color: Color,
464        paint: &SliderPaint<'_>,
465    ) {
466        let activation = activation.clamp(0.0, 1.0);
467        if activation <= 0.0 || color.a <= 0.0 {
468            return;
469        }
470        let r = self.radius * activation;
471        scene.draw_rect(
472            Rect::new(centre.x - r, centre.y - r, 2.0 * r, 2.0 * r),
473            color.fade(paint.opacity),
474            r,
475            0.0,
476            Color::TRANSPARENT,
477        );
478    }
479}
480
481/// The reference's **drop**, a slider's default indicator: a rounded label over a point that
482/// rests on the thumb.
483#[derive(Clone, Copy, Debug, Default, PartialEq)]
484pub struct DropSliderValueIndicatorShape;
485
486impl SliderValueIndicatorShape for DropSliderValueIndicatorShape {
487    fn paint(
488        &self,
489        scene: &mut Scene,
490        centre: Point,
491        text: String,
492        scale: f32,
493        paint: &SliderPaint<'_>,
494    ) {
495        crate::slider::paint_drop(centre, text, paint.theme, scale, paint.opacity, scene);
496    }
497}
498
499/// The reference's **rectangular** indicator, a range slider's default: a box with corners
500/// of 4 over a small triangle whose point is above the thumb.
501#[derive(Clone, Copy, Debug, Default, PartialEq)]
502pub struct RectangularSliderValueIndicatorShape;
503
504impl SliderValueIndicatorShape for RectangularSliderValueIndicatorShape {
505    fn paint(
506        &self,
507        scene: &mut Scene,
508        centre: Point,
509        text: String,
510        scale: f32,
511        paint: &SliderPaint<'_>,
512    ) {
513        crate::slider::paint_rect_indicator(centre, text, paint.theme, scale, paint.opacity, scene);
514    }
515}
516
517/// **No part at all**: the reference's `noThumb`, `noOverlay` and `noTickMark`, and a track
518/// or an indicator that paints nothing.
519#[derive(Clone, Copy, Debug, Default, PartialEq)]
520pub struct NoSliderShape;
521
522impl SliderTrackShape for NoSliderShape {
523    fn paint(&self, _: &mut Scene, _: Rect, _: &[f32], _: Option<f32>, _: &SliderPaint<'_>) {}
524}
525
526impl SliderTickMarkShape for NoSliderShape {
527    fn width(&self, _track_height: f32) -> f32 {
528        0.0
529    }
530
531    fn paint(&self, _: &mut Scene, _: Point, _: bool, _: f32, _: &SliderPaint<'_>) {}
532}
533
534impl SliderThumbShape for NoSliderShape {
535    fn paint(&self, _: &mut Scene, _: Point, _: SliderThumb, _: &SliderPaint<'_>) {}
536}
537
538impl SliderOverlayShape for NoSliderShape {
539    fn paint(&self, _: &mut Scene, _: Point, _: f32, _: Color, _: &SliderPaint<'_>) {}
540}
541
542impl SliderValueIndicatorShape for NoSliderShape {
543    fn paint(&self, _: &mut Scene, _: Point, _: String, _: f32, _: &SliderPaint<'_>) {}
544}
545
546/// The defaults, as statics, for a slider that names no part and a theme that names none.
547pub(crate) static SLIDER_TRACK: RoundedRectSliderTrackShape = RoundedRectSliderTrackShape;
548pub(crate) static RANGE_TRACK: RoundedRectRangeSliderTrackShape = RoundedRectRangeSliderTrackShape;
549pub(crate) static TICK_MARK: RoundSliderTickMarkShape = RoundSliderTickMarkShape { radius: None };
550pub(crate) static THUMB: RoundSliderThumbShape = RoundSliderThumbShape { radius: 10.0 };
551pub(crate) static OVERLAY: RoundSliderOverlayShape = RoundSliderOverlayShape { radius: 24.0 };
552pub(crate) static DROP: DropSliderValueIndicatorShape = DropSliderValueIndicatorShape;
553pub(crate) static RECTANGULAR: RectangularSliderValueIndicatorShape =
554    RectangularSliderValueIndicatorShape;
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use crate::interaction::Status;
560    use crate::widget::Widget;
561    use crate::{RangeSlider, Slider};
562    use frus_core::Primitive;
563    use std::cell::RefCell;
564
565    /// A box 248 wide: a track of 200 from 24, the halo's radius in.
566    const BOX: Rect = Rect::new(0.0, 0.0, 248.0, 48.0);
567
568    /// A part that paints nothing and writes down every call.
569    #[derive(Clone, Default)]
570    struct Log(Rc<RefCell<Vec<String>>>);
571
572    impl Log {
573        fn calls(&self) -> Vec<String> {
574            self.0.borrow().clone()
575        }
576
577        fn note(&self, line: String) {
578            self.0.borrow_mut().push(line);
579        }
580    }
581
582    impl SliderTrackShape for Log {
583        fn paint(
584            &self,
585            _: &mut Scene,
586            track: Rect,
587            thumbs: &[f32],
588            secondary: Option<f32>,
589            _: &SliderPaint<'_>,
590        ) {
591            self.note(format!("track {track:?} {thumbs:?} {secondary:?}"));
592        }
593    }
594
595    impl SliderTickMarkShape for Log {
596        fn width(&self, _: f32) -> f32 {
597            4.0
598        }
599
600        fn paint(&self, _: &mut Scene, centre: Point, active: bool, _: f32, _: &SliderPaint<'_>) {
601            self.note(format!("tick {} {active}", centre.x));
602        }
603    }
604
605    impl SliderThumbShape for Log {
606        fn paint(&self, _: &mut Scene, centre: Point, thumb: SliderThumb, _: &SliderPaint<'_>) {
607            self.note(format!(
608                "thumb {} {} {}",
609                centre.x, thumb.elevation, thumb.on_top
610            ));
611        }
612    }
613
614    impl SliderOverlayShape for Log {
615        fn paint(
616            &self,
617            _: &mut Scene,
618            centre: Point,
619            activation: f32,
620            _: Color,
621            _: &SliderPaint<'_>,
622        ) {
623            self.note(format!("overlay {} {activation}", centre.x));
624        }
625    }
626
627    impl SliderValueIndicatorShape for Log {
628        fn paint(
629            &self,
630            _: &mut Scene,
631            centre: Point,
632            text: String,
633            scale: f32,
634            _: &SliderPaint<'_>,
635        ) {
636            self.note(format!("indicator {} {text} {scale}", centre.x));
637        }
638    }
639
640    fn paint(widget: &dyn Widget<()>, status: Status, theme: &Theme) -> Vec<Primitive> {
641        let mut scene = Scene::new();
642        widget.paint(BOX, status, theme, &mut scene);
643        if widget.paints_over() {
644            widget.paint_over(BOX, status, theme, &mut scene);
645        }
646        scene.primitives().to_vec()
647    }
648
649    fn held() -> Status {
650        Status {
651            interaction: crate::interaction::Interaction::Pressed,
652            press_progress: 1.0,
653            ..Default::default()
654        }
655    }
656
657    /// **Each part a slider names paints in its place**: the track given the thumb and the
658    /// secondary value, the halo, a tick per division with the side it is on, the thumb with
659    /// its height, the value — and the defaults paint nothing more.
660    #[test]
661    fn a_slider_paints_with_the_parts_it_names() {
662        let log = Log::default();
663        let slider = Slider::<()>::new(0.5)
664            .divisions(2)
665            .secondary_track_value(0.75)
666            .value_label(|v| format!("{v}"))
667            .on_change(|_| ())
668            .track_shape(log.clone())
669            .tick_mark_shape(log.clone())
670            .thumb_shape(log.clone())
671            .overlay_shape(log.clone())
672            .value_indicator_shape(log.clone());
673        let painted = paint(&slider, held(), &Theme::default());
674        assert!(painted.is_empty(), "nothing of the defaults: {painted:?}");
675        assert_eq!(
676            log.calls(),
677            vec![
678                "track Rect { x: 24.0, y: 22.0, width: 200.0, height: 4.0 } [124.0] Some(173.0)",
679                "overlay 124 1",
680                "tick 26 true",
681                "tick 124 true",
682                "tick 222 false",
683                "thumb 124 6 false",
684                "indicator 124 0.5 1",
685            ]
686        );
687    }
688
689    /// **The theme's parts, a slider's apart from a range slider's**, and the widget's over
690    /// the theme's.
691    #[test]
692    fn the_theme_names_the_parts_for_each_slider() {
693        let for_slider = Log::default();
694        let for_range = Log::default();
695        let mine = Log::default();
696        let mut theme = Theme::default();
697        let t = &mut theme.widgets.slider;
698        t.thumb_shape = Some(SliderShape::thumb(for_slider.clone()));
699        t.range_thumb_shape = Some(SliderShape::thumb(for_range.clone()));
700        t.track_shape = Some(SliderShape::track(for_slider.clone()));
701        t.range_track_shape = Some(SliderShape::track(for_range.clone()));
702        t.value_indicator_shape = Some(SliderShape::value_indicator(for_slider.clone()));
703        t.range_value_indicator_shape = Some(SliderShape::value_indicator(for_range.clone()));
704
705        paint(&Slider::<()>::new(0.25), Status::default(), &theme);
706        assert_eq!(for_slider.calls().len(), 2, "{:?}", for_slider.calls());
707        assert!(for_range.calls().is_empty());
708
709        let range = RangeSlider::<()>::new(0.25, 0.75)
710            .value_label(|v| format!("{v}"))
711            .show_value_indicator(crate::ShowValueIndicator::AlwaysVisible);
712        paint(&range, Status::default(), &theme);
713        assert_eq!(
714            for_range.calls(),
715            vec![
716                "track Rect { x: 24.0, y: 22.0, width: 200.0, height: 4.0 } [74.0, 174.0] None",
717                "thumb 74 1 false",
718                "thumb 174 1 false",
719                "indicator 74 0.25 1",
720                "indicator 174 0.75 1",
721            ]
722        );
723        assert_eq!(for_slider.calls().len(), 2, "not the slider's indicator");
724
725        paint(
726            &Slider::<()>::new(0.25).thumb_shape(mine.clone()),
727            Status::default(),
728            &theme,
729        );
730        assert_eq!(mine.calls(), vec!["thumb 74 1 false"], "the widget's first");
731        assert_eq!(
732            for_slider.calls().len(),
733            3,
734            "only the theme's track this time"
735        );
736    }
737
738    /// **A range slider's parts**: the track given both thumbs, the thumb on top told so when
739    /// they overlap and alone raised, a tick per division with whether it lies between them,
740    /// both values, and a thumb stop's halo.
741    #[test]
742    fn a_range_slider_paints_with_the_parts_it_names() {
743        let log = Log::default();
744        let range = RangeSlider::<()>::new(0.5, 0.6)
745            .divisions(10)
746            .value_label(|v| format!("{v}"))
747            .on_change(|_, _| ())
748            .track_shape(log.clone())
749            .tick_mark_shape(log.clone())
750            .thumb_shape(log.clone())
751            .overlay_shape(log.clone())
752            .value_indicator_shape(log.clone());
753        let painted = paint(&range, held(), &Theme::default());
754        assert!(painted.is_empty(), "nothing of the defaults: {painted:?}");
755        let calls = log.calls();
756        assert_eq!(
757            calls[0],
758            "track Rect { x: 24.0, y: 22.0, width: 200.0, height: 4.0 } [124.0, 143.6] None"
759        );
760        let ticks: Vec<_> = calls.iter().filter(|c| c.starts_with("tick")).collect();
761        assert_eq!(ticks.len(), 11);
762        assert_eq!(
763            ticks.iter().filter(|c| c.ends_with("true")).count(),
764            2,
765            "{ticks:?}"
766        );
767        let rest: Vec<_> = calls
768            .iter()
769            .filter(|c| !c.starts_with("tick") && !c.starts_with("track"))
770            .collect();
771        assert_eq!(
772            rest,
773            vec![
774                "overlay 143.6 1",
775                "thumb 124 1 false",
776                "thumb 143.6 6 true",
777                "indicator 124 0.5 1",
778                "indicator 143.6 0.6 1",
779            ]
780        );
781
782        // A thumb stop's halo and its value, with the slider's parts.
783        let stop = &Widget::<()>::children(&range)[0].children()[0].children()[0];
784        let focused = Status {
785            focus_progress: 1.0,
786            ..Default::default()
787        };
788        let mut scene = Scene::new();
789        stop.paint(
790            Rect::new(100.0, 0.0, 48.0, 48.0),
791            focused,
792            &Theme::default(),
793            &mut scene,
794        );
795        assert!(scene.primitives().is_empty());
796        assert_eq!(
797            log.calls()[calls.len()..],
798            ["overlay 124 1", "indicator 124 0.5 1"]
799        );
800    }
801
802    /// **No part at all**: no thumb, no halo, no ticks, no track, no value.
803    #[test]
804    fn no_slider_shape_paints_nothing() {
805        let bare = Slider::<()>::new(0.5)
806            .divisions(4)
807            .value_label(|v| format!("{v}"))
808            .track_shape(NoSliderShape)
809            .tick_mark_shape(NoSliderShape)
810            .thumb_shape(NoSliderShape)
811            .overlay_shape(NoSliderShape)
812            .value_indicator_shape(NoSliderShape);
813        assert!(paint(&bare, held(), &Theme::default()).is_empty());
814    }
815
816    /// **The reference's parts, sized**: a larger thumb, halo and tick, and the rectangular
817    /// indicator on a slider.
818    #[test]
819    fn the_reference_parts_take_their_sizes() {
820        let theme = Theme::default();
821        let crisp = |p: &[Primitive]| -> Vec<Rect> {
822            p.iter()
823                .filter_map(|p| match p {
824                    Primitive::Rect { rect, blur, .. } if *blur == 0.0 => Some(*rect),
825                    _ => None,
826                })
827                .collect()
828        };
829        let big = Slider::<()>::new(0.5)
830            .on_change(|_| ())
831            .thumb_shape(RoundSliderThumbShape { radius: 14.0 })
832            .overlay_shape(RoundSliderOverlayShape { radius: 30.0 });
833        let rects = crisp(&paint(&big, held(), &theme));
834        assert!(
835            rects.contains(&Rect::new(110.0, 10.0, 28.0, 28.0)),
836            "{rects:?}"
837        );
838        assert!(
839            rects.contains(&Rect::new(94.0, -6.0, 60.0, 60.0)),
840            "{rects:?}"
841        );
842
843        let dotted = Slider::<()>::new(0.5)
844            .divisions(2)
845            .tick_mark_shape(RoundSliderTickMarkShape { radius: Some(3.0) });
846        let rects = crisp(&paint(&dotted, Status::default(), &theme));
847        assert_eq!(
848            rects
849                .iter()
850                .filter(|r| r.width == 6.0 && r.height == 6.0)
851                .count(),
852            3,
853            "{rects:?}"
854        );
855
856        // The rectangle over a slider: the drop is a path, the rectangle a rounded box.
857        let boxed = Slider::<()>::new(0.5)
858            .divisions(2)
859            .value_label(|v| format!("{v}"))
860            .on_change(|_| ())
861            .value_indicator_shape(RectangularSliderValueIndicatorShape);
862        let indicator = theme
863            .scheme
864            .surface
865            .fade(0.90)
866            .lerp(theme.scheme.on_surface, 0.60);
867        let found = paint(&boxed, held(), &theme);
868        assert!(found
869            .iter()
870            .any(|p| matches!(p, Primitive::Rect { color, .. } if *color == indicator)));
871    }
872
873    /// **A held part compares by identity**: a theme holding one is equal to its clone, and
874    /// two parts made apart are two.
875    #[test]
876    fn a_held_part_compares_by_identity() {
877        let one = SliderShape::thumb(RoundSliderThumbShape::default());
878        assert_eq!(one, one.clone());
879        assert_ne!(one, SliderShape::thumb(RoundSliderThumbShape::default()));
880        let mut theme = Theme::default();
881        theme.widgets.slider.thumb_shape = Some(one);
882        assert_eq!(theme.widgets.slider, theme.widgets.slider.clone());
883        assert_eq!(
884            format!("{:?}", SliderShape::overlay(NoSliderShape)),
885            "SliderShape(..)"
886        );
887    }
888
889    /// **A halo in no colour paints nothing**, rather than an invisible disc.
890    #[test]
891    fn a_transparent_halo_paints_nothing() {
892        let theme = Theme::default();
893        let paint = SliderPaint {
894            theme: &theme,
895            palette: SliderPalette {
896                active_track: Color::BLACK,
897                inactive_track: Color::BLACK,
898                secondary_track: Color::BLACK,
899                thumb: Color::BLACK,
900                thumb_border: None,
901                active_tick_mark: Color::BLACK,
902                inactive_tick_mark: Color::BLACK,
903            },
904            enabled: true,
905            discrete: false,
906            opacity: 1.0,
907        };
908        let mut scene = Scene::new();
909        let halo = RoundSliderOverlayShape::default();
910        halo.paint(
911            &mut scene,
912            Point::new(50.0, 24.0),
913            1.0,
914            Color::TRANSPARENT,
915            &paint,
916        );
917        halo.paint(
918            &mut scene,
919            Point::new(50.0, 24.0),
920            0.0,
921            Color::BLACK,
922            &paint,
923        );
924        assert!(scene.primitives().is_empty());
925        halo.paint(
926            &mut scene,
927            Point::new(50.0, 24.0),
928            0.5,
929            Color::BLACK,
930            &paint,
931        );
932        assert_eq!(scene.primitives().len(), 1);
933    }
934
935    /// **Ticks that would crowd are left out**, by the width the tick mark gives: three of
936    /// them must fit between two.
937    #[test]
938    fn crowded_ticks_are_left_out() {
939        let ticks = |n: usize| {
940            let log = Log::default();
941            let slider = Slider::<()>::new(0.5)
942                .divisions(n)
943                .tick_mark_shape(log.clone());
944            paint(&slider, Status::default(), &Theme::default());
945            log.calls().len()
946        };
947        // 196 pixels between the first and the last: 16 steps of 12.25 fit, 17 of 11.5 do not.
948        assert_eq!(ticks(16), 17);
949        assert_eq!(ticks(17), 0);
950    }
951}