//! Display an interactive selector of a single value from a range of values. //! //! A [`Slider`] has some local [`State`]. use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::{ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; /// An horizontal bar and a handle that selects a single value from a range of /// values. /// /// A [`Slider`] will try to fill the horizontal space of its container. /// /// The [`Slider`] range of numeric values is generic and its step size defaults /// to 1 unit. /// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # /// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; /// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } /// /// let state = &mut slider::State::new(); /// let value = 50.0; /// /// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); /// ``` /// /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) #[allow(missing_debug_implementations)] pub struct Slider<'a, T, Message, Renderer: self::Renderer> { state: &'a mut State, range: RangeInclusive, step: T, value: T, on_change: Box Message>, on_release: Option, width: Length, height: u16, style: Renderer::Style, } impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> where T: Copy + From + std::cmp::PartialOrd, Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Slider`]. /// /// It expects: /// * the local [`State`] of the [`Slider`] /// * an inclusive range of possible values /// * the current value of the [`Slider`] /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. pub fn new( state: &'a mut State, range: RangeInclusive, value: T, on_change: F, ) -> Self where F: 'static + Fn(T) -> Message, { let value = if value >= *range.start() { value } else { *range.start() }; let value = if value <= *range.end() { value } else { *range.end() }; Slider { state, value, range, step: T::from(1), on_change: Box::new(on_change), on_release: None, width: Length::Fill, height: Renderer::DEFAULT_HEIGHT, style: Renderer::Style::default(), } } /// Sets the release message of the [`Slider`]. /// This is called when the mouse is released from the slider. /// /// Typically, the user's interaction with the slider is finished when this message is produced. /// This is useful if you need to spawn a long-running task from the slider's result, where /// the default on_change message could create too many events. pub fn on_release(mut self, on_release: Message) -> Self { self.on_release = Some(on_release); self } /// Sets the width of the [`Slider`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Slider`]. pub fn height(mut self, height: u16) -> Self { self.height = height; self } /// Sets the style of the [`Slider`]. pub fn style(mut self, style: impl Into) -> Self { self.style = style.into(); self } /// Sets the step size of the [`Slider`]. pub fn step(mut self, step: T) -> Self { self.step = step; self } } /// The local state of a [`Slider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, } impl State { /// Creates a new [`State`]. pub fn new() -> State { State::default() } } impl<'a, T, Message, Renderer> Widget for Slider<'a, T, Message, Renderer> where T: Copy + Into + num_traits::FromPrimitive, Message: Clone, Renderer: self::Renderer, { fn width(&self) -> Length { self.width } fn height(&self) -> Length { Length::Shrink } fn layout( &self, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let limits = limits.width(self.width).height(Length::Units(self.height)); let size = limits.resolve(Size::ZERO); layout::Node::new(size) } fn on_event( &mut self, event: Event, layout: Layout<'_>, cursor_position: Point, messages: &mut Vec, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, ) -> event::Status { let mut change = || { let bounds = layout.bounds(); if cursor_position.x <= bounds.x { messages.push((self.on_change)(*self.range.start())); } else if cursor_position.x >= bounds.x + bounds.width { messages.push((self.on_change)(*self.range.end())); } else { let step = self.step.into(); let start = (*self.range.start()).into(); let end = (*self.range.end()).into(); let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width); let steps = (percent * (end - start) / step).round(); let value = steps * step + start; if let Some(value) = T::from_f64(value) { messages.push((self.on_change)(value)); } } }; match event { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if layout.bounds().contains(cursor_position) { change(); self.state.is_dragging = true; return event::Status::Captured; } } mouse::Event::ButtonReleased(mouse::Button::Left) => { if self.state.is_dragging { if let Some(on_release) = self.on_release.clone() { messages.push(on_release); } self.state.is_dragging = false; return event::Status::Captured; } } mouse::Event::CursorMoved { .. } => { if self.state.is_dragging { change(); return event::Status::Captured; } } _ => {} }, _ => {} } event::Status::Ignored } fn draw( &self, renderer: &mut Renderer, _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, ) -> Renderer::Output { let start = *self.range.start(); let end = *self.range.end(); renderer.draw( layout.bounds(), cursor_position, start.into() as f32..=end.into() as f32, self.value.into() as f32, self.state.is_dragging, &self.style, ) } fn hash_layout(&self, state: &mut Hasher) { struct Marker; std::any::TypeId::of::().hash(state); self.width.hash(state); } } /// The renderer of a [`Slider`]. /// /// Your [renderer] will need to implement this trait before being /// able to use a [`Slider`] in your user interface. /// /// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default height of a [`Slider`]. const DEFAULT_HEIGHT: u16; /// Draws a [`Slider`]. /// /// It receives: /// * the current cursor position /// * the bounds of the [`Slider`] /// * the local state of the [`Slider`] /// * the range of values of the [`Slider`] /// * the current value of the [`Slider`] fn draw( &mut self, bounds: Rectangle, cursor_position: Point, range: RangeInclusive, value: f32, is_dragging: bool, style: &Self::Style, ) -> Self::Output; } impl<'a, T, Message, Renderer> From> for Element<'a, Message, Renderer> where T: 'a + Copy + Into + num_traits::FromPrimitive, Message: 'a + Clone, Renderer: 'a + self::Renderer, { fn from( slider: Slider<'a, T, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(slider) } }