//! Sliders let users set a value by moving an indicator. //! //! # Example //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; //! # //! use iced::widget::slider; //! //! struct State { //! value: f32, //! } //! //! #[derive(Debug, Clone)] //! enum Message { //! ValueChanged(f32), //! } //! //! fn view(state: &State) -> Element<'_, Message> { //! slider(0.0..=100.0, state.value, Message::ValueChanged).into() //! } //! //! fn update(state: &mut State, message: Message) { //! match message { //! Message::ValueChanged(value) => { //! state.value = value; //! } //! } //! } //! ``` use std::ops::RangeInclusive; pub use crate::slider::{ Catalog, Handle, HandleShape, Status, Style, StyleFn, default, }; use crate::core::border::Border; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell, Size, Widget, }; /// An vertical bar and a handle that selects a single value from a range of /// values. /// /// A [`VerticalSlider`] will try to fill the vertical space of its container. /// /// The [`VerticalSlider`] range of numeric values is generic and its step size defaults /// to 1 unit. /// /// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # /// use iced::widget::vertical_slider; /// /// struct State { /// value: f32, /// } /// /// #[derive(Debug, Clone)] /// enum Message { /// ValueChanged(f32), /// } /// /// fn view(state: &State) -> Element<'_, Message> { /// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into() /// } /// /// fn update(state: &mut State, message: Message) { /// match message { /// Message::ValueChanged(value) => { /// state.value = value; /// } /// } /// } /// ``` #[allow(missing_debug_implementations)] pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> where Theme: Catalog, { range: RangeInclusive, step: T, shift_step: Option, value: T, default: Option, on_change: Box Message + 'a>, on_release: Option, width: f32, height: Length, class: Theme::Class<'a>, status: Option, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> where T: Copy + From + std::cmp::PartialOrd, Message: Clone, Theme: Catalog, { /// The default width of a [`VerticalSlider`]. pub const DEFAULT_WIDTH: f32 = 16.0; /// Creates a new [`VerticalSlider`]. /// /// It expects: /// * an inclusive range of possible values /// * the current value of the [`VerticalSlider`] /// * a function that will be called when the [`VerticalSlider`] is dragged. /// It receives the new value of the [`VerticalSlider`] and must produce a /// `Message`. pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { value } else { *range.start() }; let value = if value <= *range.end() { value } else { *range.end() }; VerticalSlider { value, default: None, range, step: T::from(1), shift_step: None, on_change: Box::new(on_change), on_release: None, width: Self::DEFAULT_WIDTH, height: Length::Fill, class: Theme::default(), status: None, } } /// Sets the optional default value for the [`VerticalSlider`]. /// /// If set, the [`VerticalSlider`] will reset to this value when ctrl-clicked or command-clicked. pub fn default(mut self, default: impl Into) -> Self { self.default = Some(default.into()); self } /// Sets the release message of the [`VerticalSlider`]. /// 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 [`VerticalSlider`]. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into().0; self } /// Sets the height of the [`VerticalSlider`]. pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); self } /// Sets the step size of the [`VerticalSlider`]. pub fn step(mut self, step: T) -> Self { self.step = step; self } /// Sets the optional "shift" step for the [`VerticalSlider`]. /// /// If set, this value is used as the step while the shift key is pressed. pub fn shift_step(mut self, shift_step: impl Into) -> Self { self.shift_step = Some(shift_step.into()); self } /// Sets the style of the [`VerticalSlider`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self where Theme::Class<'a>: From>, { self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); self } /// Sets the style class of the [`VerticalSlider`]. #[cfg(feature = "advanced")] #[must_use] pub fn class(mut self, class: impl Into>) -> Self { self.class = class.into(); self } } impl Widget for VerticalSlider<'_, T, Message, Theme> where T: Copy + Into + num_traits::FromPrimitive, Message: Clone, Theme: Catalog, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::default()) } fn size(&self) -> Size { Size { width: Length::Shrink, height: self.height, } } fn layout( &self, _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout::atomic(limits, self.width, self.height) } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); let is_dragging = state.is_dragging; let current_value = self.value; let locate = |cursor_position: Point| -> Option { let bounds = layout.bounds(); let new_value = if cursor_position.y >= bounds.y + bounds.height { Some(*self.range.start()) } else if cursor_position.y <= bounds.y { Some(*self.range.end()) } else { let step = if state.keyboard_modifiers.shift() { self.shift_step.unwrap_or(self.step) } else { self.step } .into(); let start = (*self.range.start()).into(); let end = (*self.range.end()).into(); let percent = 1.0 - f64::from(cursor_position.y - bounds.y) / f64::from(bounds.height); let steps = (percent * (end - start) / step).round(); let value = steps * step + start; T::from_f64(value.min(end)) }; new_value }; let increment = |value: T| -> Option { let step = if state.keyboard_modifiers.shift() { self.shift_step.unwrap_or(self.step) } else { self.step } .into(); let steps = (value.into() / step).round(); let new_value = step * (steps + 1.0); if new_value > (*self.range.end()).into() { return Some(*self.range.end()); } T::from_f64(new_value) }; let decrement = |value: T| -> Option { let step = if state.keyboard_modifiers.shift() { self.shift_step.unwrap_or(self.step) } else { self.step } .into(); let steps = (value.into() / step).round(); let new_value = step * (steps - 1.0); if new_value < (*self.range.start()).into() { return Some(*self.range.start()); } T::from_f64(new_value) }; let change = |new_value: T| { if (self.value.into() - new_value.into()).abs() > f64::EPSILON { shell.publish((self.on_change)(new_value)); self.value = new_value; } }; match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = cursor.position_over(layout.bounds()) { if state.keyboard_modifiers.control() || state.keyboard_modifiers.command() { let _ = self.default.map(change); state.is_dragging = false; } else { let _ = locate(cursor_position).map(change); state.is_dragging = true; } shell.capture_event(); } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) | Event::Touch(touch::Event::FingerLost { .. }) => { if is_dragging { if let Some(on_release) = self.on_release.clone() { shell.publish(on_release); } state.is_dragging = false; shell.capture_event(); } } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { let _ = cursor.position().and_then(locate).map(change); shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { delta }) if state.keyboard_modifiers.control() => { if cursor.is_over(layout.bounds()) { let delta = match *delta { mouse::ScrollDelta::Lines { x: _, y } => y, mouse::ScrollDelta::Pixels { x: _, y } => y, }; if delta < 0.0 { let _ = decrement(current_value).map(change); } else { let _ = increment(current_value).map(change); } shell.capture_event(); } } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if cursor.is_over(layout.bounds()) { match key { Key::Named(key::Named::ArrowUp) => { let _ = increment(current_value).map(change); } Key::Named(key::Named::ArrowDown) => { let _ = decrement(current_value).map(change); } _ => (), } shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = *modifiers; } _ => {} } let current_status = if state.is_dragging { Status::Dragged } else if cursor.is_over(layout.bounds()) { Status::Hovered } else { Status::Active }; if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.status = Some(current_status); } else if self.status.is_some_and(|status| status != current_status) { shell.request_redraw(); } } fn draw( &self, _tree: &Tree, renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); let style = theme.style(&self.class, self.status.unwrap_or(Status::Active)); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { HandleShape::Circle { radius } => { (radius * 2.0, radius * 2.0, radius.into()) } HandleShape::Rectangle { width, border_radius, } => (f32::from(width), bounds.width, border_radius), }; let value = self.value.into() as f32; let (range_start, range_end) = { let (start, end) = self.range.clone().into_inner(); (start.into() as f32, end.into() as f32) }; let offset = if range_start >= range_end { 0.0 } else { (bounds.height - handle_width) * (value - range_end) / (range_start - range_end) }; let rail_x = bounds.x + bounds.width / 2.0; renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: rail_x - style.rail.width / 2.0, y: bounds.y, width: style.rail.width, height: offset + handle_width / 2.0, }, border: style.rail.border, ..renderer::Quad::default() }, style.rail.backgrounds.1, ); renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: rail_x - style.rail.width / 2.0, y: bounds.y + offset + handle_width / 2.0, width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, border: style.rail.border, ..renderer::Quad::default() }, style.rail.backgrounds.0, ); renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: rail_x - handle_height / 2.0, y: bounds.y + offset, width: handle_height, height: handle_width, }, border: Border { radius: handle_border_radius, width: style.handle.border_width, color: style.handle.border_color, }, ..renderer::Quad::default() }, style.handle.background, ); } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); let is_mouse_over = cursor.is_over(bounds); if state.is_dragging { mouse::Interaction::Grabbing } else if is_mouse_over { mouse::Interaction::Grab } else { mouse::Interaction::default() } } } impl<'a, T, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where T: Copy + Into + num_traits::FromPrimitive + 'a, Message: Clone + 'a, Theme: Catalog + 'a, Renderer: core::Renderer + 'a, { fn from( slider: VerticalSlider<'a, T, Message, Theme>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(slider) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] struct State { is_dragging: bool, keyboard_modifiers: keyboard::Modifiers, }