iced_widget/
slider.rs

1//! Sliders let users set a value by moving an indicator.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
7//! #
8//! use iced::widget::slider;
9//!
10//! struct State {
11//!    value: f32,
12//! }
13//!
14//! #[derive(Debug, Clone)]
15//! enum Message {
16//!     ValueChanged(f32),
17//! }
18//!
19//! fn view(state: &State) -> Element<'_, Message> {
20//!     slider(0.0..=100.0, state.value, Message::ValueChanged).into()
21//! }
22//!
23//! fn update(state: &mut State, message: Message) {
24//!     match message {
25//!         Message::ValueChanged(value) => {
26//!             state.value = value;
27//!         }
28//!     }
29//! }
30//! ```
31use crate::core::border::{self, Border};
32use crate::core::event::{self, Event};
33use crate::core::keyboard;
34use crate::core::keyboard::key::{self, Key};
35use crate::core::layout;
36use crate::core::mouse;
37use crate::core::renderer;
38use crate::core::touch;
39use crate::core::widget::tree::{self, Tree};
40use crate::core::widget::Id;
41use crate::core::{
42    self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
43    Rectangle, Shell, Size, Theme, Widget,
44};
45
46use std::ops::RangeInclusive;
47
48use iced_renderer::core::border::Radius;
49use iced_runtime::core::gradient::Linear;
50
51#[cfg(feature = "a11y")]
52use std::borrow::Cow;
53
54/// An horizontal bar and a handle that selects a single value from a range of
55/// values.
56///
57/// A [`Slider`] will try to fill the horizontal space of its container.
58///
59/// The [`Slider`] range of numeric values is generic and its step size defaults
60/// to 1 unit.
61///
62/// # Example
63/// ```no_run
64/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
65/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
66/// #
67/// use iced::widget::slider;
68///
69/// struct State {
70///    value: f32,
71/// }
72///
73/// #[derive(Debug, Clone)]
74/// enum Message {
75///     ValueChanged(f32),
76/// }
77///
78/// fn view(state: &State) -> Element<'_, Message> {
79///     slider(0.0..=100.0, state.value, Message::ValueChanged).into()
80/// }
81///
82/// fn update(state: &mut State, message: Message) {
83///     match message {
84///         Message::ValueChanged(value) => {
85///             state.value = value;
86///         }
87///     }
88/// }
89/// ```
90#[allow(missing_debug_implementations)]
91pub struct Slider<'a, T, Message, Theme = crate::Theme>
92where
93    Theme: Catalog,
94{
95    id: Id,
96    #[cfg(feature = "a11y")]
97    name: Option<Cow<'a, str>>,
98    #[cfg(feature = "a11y")]
99    description: Option<iced_accessibility::Description<'a>>,
100    #[cfg(feature = "a11y")]
101    label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
102    range: RangeInclusive<T>,
103    step: T,
104    shift_step: Option<T>,
105    value: T,
106    default: Option<T>,
107    breakpoints: &'a [T],
108    on_change: Box<dyn Fn(T) -> Message + 'a>,
109    on_release: Option<Message>,
110    width: Length,
111    height: f32,
112    class: Theme::Class<'a>,
113}
114
115impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
116where
117    T: Copy + From<u8> + PartialOrd,
118    Message: Clone,
119    Theme: Catalog,
120{
121    /// The default height of a [`Slider`].
122    pub const DEFAULT_HEIGHT: f32 = 16.0;
123
124    /// Creates a new [`Slider`].
125    ///
126    /// It expects:
127    ///   * an inclusive range of possible values
128    ///   * the current value of the [`Slider`]
129    ///   * a function that will be called when the [`Slider`] is dragged.
130    ///     It receives the new value of the [`Slider`] and must produce a
131    ///     `Message`.
132    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
133    where
134        F: 'a + Fn(T) -> Message,
135    {
136        let value = if value >= *range.start() {
137            value
138        } else {
139            *range.start()
140        };
141
142        let value = if value <= *range.end() {
143            value
144        } else {
145            *range.end()
146        };
147
148        Slider {
149            id: Id::unique(),
150            #[cfg(feature = "a11y")]
151            name: None,
152            #[cfg(feature = "a11y")]
153            description: None,
154            #[cfg(feature = "a11y")]
155            label: None,
156            value,
157            default: None,
158            range,
159            step: T::from(1),
160            shift_step: None,
161            breakpoints: &[],
162            on_change: Box::new(on_change),
163            on_release: None,
164            width: Length::Fill,
165            height: Self::DEFAULT_HEIGHT,
166            class: Theme::default(),
167        }
168    }
169
170    /// Sets the optional default value for the [`Slider`].
171    ///
172    /// If set, the [`Slider`] will reset to this value when ctrl-clicked or command-clicked.
173    pub fn default(mut self, default: impl Into<T>) -> Self {
174        self.default = Some(default.into());
175        self
176    }
177
178    /// Defines breakpoints to visibly mark on the slider.
179    ///
180    /// The slider will gravitate towards a breakpoint when near it.
181    pub fn breakpoints(mut self, breakpoints: &'a [T]) -> Self {
182        self.breakpoints = breakpoints;
183        self
184    }
185
186    /// Sets the release message of the [`Slider`].
187    /// This is called when the mouse is released from the slider.
188    ///
189    /// Typically, the user's interaction with the slider is finished when this message is produced.
190    /// This is useful if you need to spawn a long-running task from the slider's result, where
191    /// the default `on_change` message could create too many events.
192    pub fn on_release(mut self, on_release: Message) -> Self {
193        self.on_release = Some(on_release);
194        self
195    }
196
197    /// Sets the width of the [`Slider`].
198    pub fn width(mut self, width: impl Into<Length>) -> Self {
199        self.width = width.into();
200        self
201    }
202
203    /// Sets the height of the [`Slider`].
204    pub fn height(mut self, height: impl Into<Pixels>) -> Self {
205        self.height = height.into().0;
206        self
207    }
208
209    /// Sets the step size of the [`Slider`].
210    pub fn step(mut self, step: impl Into<T>) -> Self {
211        self.step = step.into();
212        self
213    }
214
215    /// Sets the optional "shift" step for the [`Slider`].
216    ///
217    /// If set, this value is used as the step while the shift key is pressed.
218    pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
219        self.shift_step = Some(shift_step.into());
220        self
221    }
222
223    /// Sets the style of the [`Slider`].
224    #[must_use]
225    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
226    where
227        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
228    {
229        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
230        self
231    }
232
233    /// Sets the style class of the [`Slider`].
234    #[cfg(feature = "advanced")]
235    #[must_use]
236    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
237        self.class = class.into();
238        self
239    }
240
241    #[cfg(feature = "a11y")]
242    /// Sets the name of the [`Slider`].
243    pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
244        self.name = Some(name.into());
245        self
246    }
247
248    #[cfg(feature = "a11y")]
249    /// Sets the description of the [`Slider`].
250    pub fn description_widget(
251        mut self,
252        description: &impl iced_accessibility::Describes,
253    ) -> Self {
254        self.description = Some(iced_accessibility::Description::Id(
255            description.description(),
256        ));
257        self
258    }
259
260    #[cfg(feature = "a11y")]
261    /// Sets the description of the [`Slider`].
262    pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
263        self.description =
264            Some(iced_accessibility::Description::Text(description.into()));
265        self
266    }
267
268    #[cfg(feature = "a11y")]
269    /// Sets the label of the [`Slider`].
270    pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
271        self.label =
272            Some(label.label().into_iter().map(|l| l.into()).collect());
273        self
274    }
275}
276
277impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
278    for Slider<'a, T, Message, Theme>
279where
280    T: Copy + Into<f64> + num_traits::FromPrimitive,
281    Message: Clone,
282    Theme: Catalog,
283    Renderer: core::Renderer,
284{
285    fn tag(&self) -> tree::Tag {
286        tree::Tag::of::<State>()
287    }
288
289    fn state(&self) -> tree::State {
290        tree::State::new(State::default())
291    }
292
293    fn size(&self) -> Size<Length> {
294        Size {
295            width: self.width,
296            height: Length::Shrink,
297        }
298    }
299
300    fn layout(
301        &self,
302        _tree: &mut Tree,
303        _renderer: &Renderer,
304        limits: &layout::Limits,
305    ) -> layout::Node {
306        layout::atomic(limits, self.width, self.height)
307    }
308
309    fn on_event(
310        &mut self,
311        tree: &mut Tree,
312        event: Event,
313        layout: Layout<'_>,
314        cursor: mouse::Cursor,
315        _renderer: &Renderer,
316        _clipboard: &mut dyn Clipboard,
317        shell: &mut Shell<'_, Message>,
318        _viewport: &Rectangle,
319    ) -> event::Status {
320        let state = tree.state.downcast_mut::<State>();
321
322        let is_dragging = state.is_dragging;
323        let current_value = self.value;
324
325        let locate = |cursor_position: Point| -> Option<T> {
326            let bounds = layout.bounds();
327            let new_value = if cursor_position.x <= bounds.x {
328                Some(*self.range.start())
329            } else if cursor_position.x >= bounds.x + bounds.width {
330                Some(*self.range.end())
331            } else {
332                let step = if state.keyboard_modifiers.shift() {
333                    self.shift_step.unwrap_or(self.step)
334                } else {
335                    self.step
336                }
337                .into();
338
339                let start = (*self.range.start()).into();
340                let end = (*self.range.end()).into();
341
342                let percent = f64::from(cursor_position.x - bounds.x)
343                    / f64::from(bounds.width);
344
345                let steps = (percent * (end - start) / step).round();
346                let value = steps * step + start;
347
348                T::from_f64(value.min(end))
349            };
350
351            new_value
352        };
353
354        let increment = |value: T| -> Option<T> {
355            let step = if state.keyboard_modifiers.shift() {
356                self.shift_step.unwrap_or(self.step)
357            } else {
358                self.step
359            }
360            .into();
361
362            let steps = (value.into() / step).round();
363            let new_value = step * (steps + 1.0);
364
365            if new_value > (*self.range.end()).into() {
366                return Some(*self.range.end());
367            }
368
369            T::from_f64(new_value)
370        };
371
372        let decrement = |value: T| -> Option<T> {
373            let step = if state.keyboard_modifiers.shift() {
374                self.shift_step.unwrap_or(self.step)
375            } else {
376                self.step
377            }
378            .into();
379
380            let steps = (value.into() / step).round();
381            let new_value = step * (steps - 1.0);
382
383            if new_value < (*self.range.start()).into() {
384                return Some(*self.range.start());
385            }
386
387            T::from_f64(new_value)
388        };
389
390        let change = |new_value: T| {
391            if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
392                shell.publish((self.on_change)(new_value));
393
394                self.value = new_value;
395            }
396        };
397
398        match event {
399            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
400            | Event::Touch(touch::Event::FingerPressed { .. }) => {
401                if let Some(cursor_position) =
402                    cursor.position_over(layout.bounds())
403                {
404                    if state.keyboard_modifiers.command() {
405                        let _ = self.default.map(change);
406                        state.is_dragging = false;
407                    } else {
408                        let _ = locate(cursor_position).map(change);
409                        state.is_dragging = true;
410                    }
411
412                    return event::Status::Captured;
413                }
414            }
415            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
416            | Event::Touch(touch::Event::FingerLifted { .. })
417            | Event::Touch(touch::Event::FingerLost { .. }) => {
418                if is_dragging {
419                    if let Some(on_release) = self.on_release.clone() {
420                        shell.publish(on_release);
421                    }
422                    state.is_dragging = false;
423
424                    return event::Status::Captured;
425                }
426            }
427            Event::Mouse(mouse::Event::CursorMoved { .. })
428            | Event::Touch(touch::Event::FingerMoved { .. }) => {
429                if is_dragging {
430                    let _ = cursor.position().and_then(locate).map(change);
431
432                    return event::Status::Captured;
433                }
434            }
435            Event::Mouse(mouse::Event::WheelScrolled { delta })
436                if state.keyboard_modifiers.control() =>
437            {
438                if cursor.is_over(layout.bounds()) {
439                    let delta = match delta {
440                        mouse::ScrollDelta::Lines { x: _, y } => y,
441                        mouse::ScrollDelta::Pixels { x: _, y } => y,
442                    };
443
444                    if delta < 0.0 {
445                        let _ = decrement(current_value).map(change);
446                    } else {
447                        let _ = increment(current_value).map(change);
448                    }
449
450                    return event::Status::Captured;
451                }
452            }
453            Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
454                if cursor.is_over(layout.bounds()) {
455                    match key {
456                        Key::Named(key::Named::ArrowUp) => {
457                            let _ = increment(current_value).map(change);
458                        }
459                        Key::Named(key::Named::ArrowDown) => {
460                            let _ = decrement(current_value).map(change);
461                        }
462                        _ => (),
463                    }
464
465                    return event::Status::Captured;
466                }
467            }
468            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
469                state.keyboard_modifiers = modifiers;
470            }
471            _ => {}
472        }
473
474        event::Status::Ignored
475    }
476
477    fn draw(
478        &self,
479        tree: &Tree,
480        renderer: &mut Renderer,
481        theme: &Theme,
482        _style: &renderer::Style,
483        layout: Layout<'_>,
484        cursor: mouse::Cursor,
485        _viewport: &Rectangle,
486    ) {
487        let state = tree.state.downcast_ref::<State>();
488        let bounds = layout.bounds();
489        let is_mouse_over = cursor.is_over(bounds);
490
491        let style = theme.style(
492            &self.class,
493            if state.is_dragging {
494                Status::Dragged
495            } else if is_mouse_over {
496                Status::Hovered
497            } else {
498                Status::Active
499            },
500        );
501
502        let border_width = style
503            .handle
504            .border_width
505            .min(bounds.height / 2.0)
506            .min(bounds.width / 2.0);
507
508        let (handle_width, handle_height, handle_border_radius) =
509            match style.handle.shape {
510                HandleShape::Circle { radius } => {
511                    let radius = (radius)
512                        .max(2.0 * border_width)
513                        .min(bounds.height / 2.0)
514                        .min(bounds.width / 2.0 + 2.0 * border_width);
515                    (radius * 2.0, radius * 2.0, Radius::from(radius))
516                }
517                HandleShape::Rectangle {
518                    height,
519                    width,
520                    border_radius,
521                } => {
522                    let width = (f32::from(width)).max(2.0 * border_width);
523                    let height = (f32::from(height)).max(2.0 * border_width);
524                    let mut border_radius: [f32; 4] = border_radius.into();
525                    for r in &mut border_radius {
526                        *r = (*r)
527                            .min(height / 2.0)
528                            .min(width / 2.0)
529                            .max(*r * (width + border_width * 2.0) / width);
530                    }
531
532                    (
533                        width,
534                        height,
535                        Radius {
536                            top_left: border_radius[0],
537                            top_right: border_radius[1],
538                            bottom_right: border_radius[2],
539                            bottom_left: border_radius[3],
540                        },
541                    )
542                }
543            };
544
545        let value = self.value.into() as f32;
546        let (range_start, range_end) = {
547            let (start, end) = self.range.clone().into_inner();
548
549            (start.into() as f32, end.into() as f32)
550        };
551
552        let offset = if range_start >= range_end {
553            0.0
554        } else {
555            (bounds.width - handle_width) * (value - range_start)
556                / (range_end - range_start)
557        };
558
559        let rail_y = bounds.y + bounds.height / 2.0;
560
561        // Draw the breakpoint indicators beneath the slider.
562        const BREAKPOINT_WIDTH: f32 = 2.0;
563        for &value in self.breakpoints {
564            let value: f64 = value.into();
565            let offset = if range_start >= range_end {
566                0.0
567            } else {
568                (bounds.width - BREAKPOINT_WIDTH) * (value as f32 - range_start)
569                    / (range_end - range_start)
570            };
571
572            renderer.fill_quad(
573                renderer::Quad {
574                    bounds: Rectangle {
575                        x: bounds.x + offset,
576                        y: rail_y + 6.0,
577                        width: BREAKPOINT_WIDTH,
578                        height: 8.0,
579                    },
580                    border: Border {
581                        radius: 0.0.into(),
582                        width: 0.0,
583                        color: Color::TRANSPARENT,
584                    },
585                    ..renderer::Quad::default()
586                },
587                crate::core::Background::Color(style.breakpoint.color),
588            );
589        }
590
591        renderer.fill_quad(
592            renderer::Quad {
593                bounds: Rectangle {
594                    x: bounds.x,
595                    y: rail_y - style.rail.width / 2.0,
596                    width: offset + handle_width / 2.0,
597                    height: style.rail.width,
598                },
599                border: style.rail.border,
600                ..renderer::Quad::default()
601            },
602            style.rail.backgrounds.0,
603        );
604
605        // TODO align the angle of the gradient for the slider?
606
607        renderer.fill_quad(
608            renderer::Quad {
609                bounds: Rectangle {
610                    x: bounds.x + offset + handle_width / 2.0,
611                    y: rail_y - style.rail.width / 2.0,
612                    width: bounds.width - offset - handle_width / 2.0,
613                    height: style.rail.width,
614                },
615                border: style.rail.border,
616                ..renderer::Quad::default()
617            },
618            style.rail.backgrounds.1,
619        );
620
621        // handle
622        renderer.fill_quad(
623            renderer::Quad {
624                bounds: Rectangle {
625                    x: bounds.x + offset,
626                    y: rail_y - (handle_height / 2.0),
627                    width: handle_width,
628                    height: handle_height,
629                },
630                border: Border {
631                    radius: handle_border_radius,
632                    width: style.handle.border_width,
633                    color: style.handle.border_color,
634                },
635                ..renderer::Quad::default()
636            },
637            style.handle.background,
638        );
639    }
640
641    fn mouse_interaction(
642        &self,
643        tree: &Tree,
644        layout: Layout<'_>,
645        cursor: mouse::Cursor,
646        _viewport: &Rectangle,
647        _renderer: &Renderer,
648    ) -> mouse::Interaction {
649        let state = tree.state.downcast_ref::<State>();
650        let bounds = layout.bounds();
651        let is_mouse_over = cursor.is_over(bounds);
652
653        if state.is_dragging {
654            mouse::Interaction::Grabbing
655        } else if is_mouse_over {
656            mouse::Interaction::Grab
657        } else {
658            mouse::Interaction::default()
659        }
660    }
661
662    #[cfg(feature = "a11y")]
663    fn a11y_nodes(
664        &self,
665        layout: Layout<'_>,
666        _state: &Tree,
667        cursor: mouse::Cursor,
668    ) -> iced_accessibility::A11yTree {
669        use iced_accessibility::{
670            accesskit::{NodeBuilder, NodeId, Rect, Role},
671            A11yTree,
672        };
673
674        let bounds = layout.bounds();
675        let is_hovered = cursor.is_over(bounds);
676        let Rectangle {
677            x,
678            y,
679            width,
680            height,
681        } = bounds;
682        let bounds = Rect::new(
683            x as f64,
684            y as f64,
685            (x + width) as f64,
686            (y + height) as f64,
687        );
688        let mut node = NodeBuilder::new(Role::Slider);
689        node.set_bounds(bounds);
690        if let Some(name) = self.name.as_ref() {
691            node.set_name(name.clone());
692        }
693        match self.description.as_ref() {
694            Some(iced_accessibility::Description::Id(id)) => {
695                node.set_described_by(
696                    id.iter()
697                        .cloned()
698                        .map(|id| NodeId::from(id))
699                        .collect::<Vec<_>>(),
700                );
701            }
702            Some(iced_accessibility::Description::Text(text)) => {
703                node.set_description(text.clone());
704            }
705            None => {}
706        }
707
708        if is_hovered {
709            node.set_hovered();
710        }
711
712        if let Some(label) = self.label.as_ref() {
713            node.set_labelled_by(label.clone());
714        }
715
716        if let Ok(min) = self.range.start().clone().try_into() {
717            node.set_min_numeric_value(min);
718        }
719        if let Ok(max) = self.range.end().clone().try_into() {
720            node.set_max_numeric_value(max);
721        }
722        if let Ok(value) = self.value.clone().try_into() {
723            node.set_numeric_value(value);
724        }
725        if let Ok(step) = self.step.clone().try_into() {
726            node.set_numeric_value_step(step);
727        }
728
729        // TODO: This could be a setting on the slider
730        node.set_live(iced_accessibility::accesskit::Live::Polite);
731
732        A11yTree::leaf(node, self.id.clone())
733    }
734
735    fn id(&self) -> Option<Id> {
736        Some(self.id.clone())
737    }
738
739    fn set_id(&mut self, id: Id) {
740        self.id = id;
741    }
742}
743
744impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
745    for Element<'a, Message, Theme, Renderer>
746where
747    T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
748    Message: Clone + 'a,
749    Theme: Catalog + 'a,
750    Renderer: core::Renderer + 'a,
751{
752    fn from(
753        slider: Slider<'a, T, Message, Theme>,
754    ) -> Element<'a, Message, Theme, Renderer> {
755        Element::new(slider)
756    }
757}
758
759#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
760struct State {
761    is_dragging: bool,
762    keyboard_modifiers: keyboard::Modifiers,
763}
764
765/// The possible status of a [`Slider`].
766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
767pub enum Status {
768    /// The [`Slider`] can be interacted with.
769    Active,
770    /// The [`Slider`] is being hovered.
771    Hovered,
772    /// The [`Slider`] is being dragged.
773    Dragged,
774}
775
776/// The appearance of a slider.
777#[derive(Debug, Clone, Copy, PartialEq)]
778pub struct Style {
779    /// The colors of the rail of the slider.
780    pub rail: Rail,
781    /// The appearance of the [`Handle`] of the slider.
782    pub handle: Handle,
783    /// The appearance of breakpoints.
784    pub breakpoint: Breakpoint,
785}
786
787/// The appearance of slider breakpoints.
788#[derive(Debug, Clone, Copy, PartialEq)]
789pub struct Breakpoint {
790    /// The color of the slider breakpoint.
791    pub color: Color,
792}
793
794impl Style {
795    /// Changes the [`HandleShape`] of the [`Style`] to a circle
796    /// with the given radius.
797    pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
798        self.handle.shape = HandleShape::Circle {
799            radius: radius.into().0,
800        };
801        self
802    }
803}
804
805/// The appearance of a slider rail
806#[derive(Debug, Clone, Copy, PartialEq)]
807pub struct Rail {
808    /// The backgrounds of the rail of the slider.
809    pub backgrounds: (Background, Background),
810    /// The width of the stroke of a slider rail.
811    pub width: f32,
812    /// The border of the rail.
813    pub border: Border,
814}
815
816/// The background color of the rail
817#[derive(Debug, Clone, Copy)]
818pub enum RailBackground {
819    /// Start and end colors of the rail
820    Pair(Color, Color),
821    /// Linear gradient for the background of the rail
822    /// includes an option for auto-selecting the angle
823    Gradient {
824        /// the linear gradient of the slider
825        gradient: Linear,
826        /// Let the widget determin the angle of the gradient
827        auto_angle: bool,
828    },
829}
830
831/// The appearance of the handle of a slider.
832#[derive(Debug, Clone, Copy, PartialEq)]
833pub struct Handle {
834    /// The shape of the handle.
835    pub shape: HandleShape,
836    /// The [`Background`] of the handle.
837    pub background: Background,
838    /// The border width of the handle.
839    pub border_width: f32,
840    /// The border [`Color`] of the handle.
841    pub border_color: Color,
842}
843
844/// The shape of the handle of a slider.
845#[derive(Debug, Clone, Copy, PartialEq)]
846pub enum HandleShape {
847    /// A circular handle.
848    Circle {
849        /// The radius of the circle.
850        radius: f32,
851    },
852    /// A rectangular shape.
853    Rectangle {
854        /// The width of the rectangle.
855        width: u16,
856        /// The height of the rectangle.
857        height: u16,
858        /// The border radius of the corners of the rectangle.
859        border_radius: border::Radius,
860    },
861}
862
863/// The theme catalog of a [`Slider`].
864pub trait Catalog: Sized {
865    /// The item class of the [`Catalog`].
866    type Class<'a>;
867
868    /// The default class produced by the [`Catalog`].
869    fn default<'a>() -> Self::Class<'a>;
870
871    /// The [`Style`] of a class with the given status.
872    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
873}
874
875/// A styling function for a [`Slider`].
876pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
877
878impl Catalog for Theme {
879    type Class<'a> = StyleFn<'a, Self>;
880
881    fn default<'a>() -> Self::Class<'a> {
882        Box::new(default)
883    }
884
885    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
886        class(self, status)
887    }
888}
889
890/// The default style of a [`Slider`].
891pub fn default(theme: &Theme, status: Status) -> Style {
892    let palette = theme.extended_palette();
893
894    let color = match status {
895        Status::Active => palette.primary.strong.color,
896        Status::Hovered => palette.primary.base.color,
897        Status::Dragged => palette.primary.strong.color,
898    };
899
900    Style {
901        rail: Rail {
902            backgrounds: (color.into(), palette.secondary.base.color.into()),
903            width: 4.0,
904            border: Border {
905                radius: 2.0.into(),
906                width: 0.0,
907                color: Color::TRANSPARENT,
908            },
909        },
910        handle: Handle {
911            shape: HandleShape::Circle { radius: 7.0 },
912            background: color.into(),
913            border_color: Color::TRANSPARENT,
914            border_width: 0.0,
915        },
916        breakpoint: Breakpoint {
917            color: palette.background.weak.text,
918        },
919    }
920}