diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 799ff8ac37..6eb7edcd8d 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -2,7 +2,7 @@ use iced::alignment; use iced::mouse; use iced::theme; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; -use iced::widget::{Slider, column, row, text}; +use iced::widget::{Slider, column, row, slider, text}; use iced::{Center, Color, Element, Fill, Font, Pixels, Point, Rectangle, Renderer, Size, Vector}; use palette::{Darken, Hsl, Lighten, ShiftHue, convert::FromColor, rgb::Rgb}; @@ -304,7 +304,8 @@ impl ColorPicker { component: f32, update: impl Fn(f32) -> C + 'a, ) -> Slider<'a, f64, C> { - Slider::new(range, f64::from(component), move |v| update(v as f32)).step(0.01) + Slider::new(range, f64::from(component), move |v| update(v as f32)) + .scale(slider::continuous()) } row![ diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 3c9318f1f0..ea1ee9fe40 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -97,15 +97,20 @@ impl Example { self.snap, ), text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"), - slider(1.0..=200.0, top_left, Message::RadiusTopLeftChanged).step(0.01), - slider(1.0..=200.0, top_right, Message::RadiusTopRightChanged).step(0.01), - slider(1.0..=200.0, bottom_right, Message::RadiusBottomRightChanged).step(0.01), - slider(1.0..=200.0, bottom_left, Message::RadiusBottomLeftChanged).step(0.01), - slider(0.0..=10.0, self.border_width, Message::BorderWidthChanged).step(0.01), + slider(1.0..=200.0, top_left, Message::RadiusTopLeftChanged) + .scale(slider::discrete(0.01)), + slider(1.0..=200.0, top_right, Message::RadiusTopRightChanged) + .scale(slider::discrete(0.01)), + slider(1.0..=200.0, bottom_right, Message::RadiusBottomRightChanged) + .scale(slider::discrete(0.01)), + slider(1.0..=200.0, bottom_left, Message::RadiusBottomLeftChanged) + .scale(slider::discrete(0.01)), + slider(0.0..=10.0, self.border_width, Message::BorderWidthChanged) + .scale(slider::discrete(0.01)), text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"), - slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged).step(0.01), - slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged).step(0.01), - slider(0.0..=100.0, sr, Message::ShadowBlurRadiusChanged).step(0.01), + slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged).scale(slider::discrete(0.01)), + slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged).scale(slider::discrete(0.01)), + slider(0.0..=100.0, sr, Message::ShadowBlurRadiusChanged).scale(slider::discrete(0.01)), toggler(self.snap) .label("Snap to pixel grid") .on_toggle(Message::SnapToggled), diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 02630ff825..f829b2ed94 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -70,7 +70,7 @@ impl IcedCubes { control( "Size", slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged) - .step(0.01) + .scale(slider::continuous()) .width(100), ), checkbox(self.scene.show_depth_buffer) @@ -88,7 +88,7 @@ impl IcedCubes { ..self.scene.light_color }) }) - .step(0.01) + .scale(slider::continuous()) .width(100) ), control( @@ -99,7 +99,7 @@ impl IcedCubes { ..self.scene.light_color }) }) - .step(0.01) + .scale(slider::continuous()) .width(100) ), control( @@ -110,7 +110,7 @@ impl IcedCubes { ..self.scene.light_color }) }) - .step(0.01) + .scale(slider::continuous()) .width(100) ) ] diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index db2ceb74b4..1b70e4da5d 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -106,7 +106,7 @@ impl Example { let content = column![ circle(self.radius), text!("Radius: {:.2}", self.radius), - slider(1.0..=100.0, self.radius, Message::RadiusChanged).step(0.01), + slider(1.0..=100.0, self.radius, Message::RadiusChanged).scale(slider::discrete(0.01)), ] .padding(20) .spacing(20) diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 29da3f302a..765eca3a20 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -135,7 +135,8 @@ impl Image { format!("Width: {}px", self.width) ), with_value( - slider(0.0..=1.0, self.opacity, Message::OpacityChanged).step(0.01), + slider(0.0..=1.0, self.opacity, Message::OpacityChanged) + .scale(slider::discrete(0.01)), format!("Opacity: {:.2}", self.opacity) ), with_value( diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 624f53cb7d..3226513542 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -70,7 +70,7 @@ impl Gradient { let angle_picker = row![ text("Angle").width(64), - slider(Radians::RANGE, self.angle, Message::AngleChanged).step(0.01) + slider(Radians::RANGE, self.angle, Message::AngleChanged).scale(slider::continuous()) ] .spacing(8) .padding(8) @@ -114,10 +114,10 @@ impl Default for Gradient { fn color_picker(label: &str, color: Color) -> Element<'_, Color> { row![ text(label).width(64), - slider(0.0..=1.0, color.r, move |r| { Color { r, ..color } }).step(0.01), - slider(0.0..=1.0, color.g, move |g| { Color { g, ..color } }).step(0.01), - slider(0.0..=1.0, color.b, move |b| { Color { b, ..color } }).step(0.01), - slider(0.0..=1.0, color.a, move |a| { Color { a, ..color } }).step(0.01), + slider(0.0..=1.0, color.r, move |r| { Color { r, ..color } }).scale(slider::continuous()), + slider(0.0..=1.0, color.g, move |g| { Color { g, ..color } }).scale(slider::continuous()), + slider(0.0..=1.0, color.b, move |b| { Color { b, ..color } }).scale(slider::continuous()), + slider(0.0..=1.0, color.a, move |a| { Color { a, ..color } }).scale(slider::continuous()), ] .spacing(8) .padding(8) diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 6906daa161..98c918da4f 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -48,21 +48,21 @@ impl Controls { ..background_color }) }) - .step(0.01), + .scale(slider::continuous()), slider(0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color }) }) - .step(0.01), + .scale(slider::continuous()), slider(0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color }) }) - .step(0.01), + .scale(slider::continuous()), ] .width(500) .spacing(20); diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index f726ef1a9e..9e746c5c57 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -46,7 +46,8 @@ impl Progress { center( row![ bar, - vertical_slider(0.0..=100.0, self.value, Message::SliderChanged).step(0.01), + vertical_slider(0.0..=100.0, self.value, Message::SliderChanged) + .scale(slider::continuous()), ] .spacing(20), ) @@ -54,7 +55,8 @@ impl Progress { center( column![ bar, - slider(0.0..=100.0, self.value, Message::SliderChanged).step(0.01) + slider(0.0..=100.0, self.value, Message::SliderChanged) + .scale(slider::discrete(0.01)) ] .spacing(20), ) diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index eff453c6f9..aa362a6566 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -31,14 +31,14 @@ impl Slider { let h_slider = container( slider(1..=100, self.value, Message::SliderChanged) .default(50) - .shift_step(5), + .scale(slider::discrete(1).shift_step(5)), ) .width(250); let v_slider = container( vertical_slider(1..=100, self.value, Message::SliderChanged) .default(50) - .shift_step(5), + .scale(slider::discrete(1).shift_step(5)), ) .height(200); diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 57128a61f5..632cf7d40d 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -129,7 +129,7 @@ impl App { "Timeout", row![ text!("{:0>2} sec", self.timeout_secs), - slider(1.0..=30.0, self.timeout_secs as f64, Message::Timeout).step(1.0) + slider(1.0..=30.0, self.timeout_secs as f64, Message::Timeout) ] .spacing(5) .into() diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 2a0ff096c3..6ed5ca7e10 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -564,7 +564,6 @@ fn color_slider<'a>( slider(0.0..=1.0, f64::from(component), move |c| { Message::TextColorChanged(update(c as f32)) }) - .step(0.01) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 5832380fa0..2bf1712e38 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -50,7 +50,7 @@ impl VectorialText { let slider_with_label = |label, range, value, message: fn(f32) -> _| { column![ row![text(label), space::horizontal(), text!("{:.2}", value)], - slider(range, value, message).step(0.01) + slider(range, value, message).scale(slider::discrete(0.01)) ] .spacing(2) }; diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 1ce6635320..d9894d04b8 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -845,7 +845,7 @@ where stack![ container( slider(range, current, on_change) - .step(10.0) + .scale(slider::discrete(10)) .width(Fill) .height(24) .style(|theme: &core::Theme, status| { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 4bd7b2966e..c66a68f5a6 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1489,7 +1489,7 @@ pub fn slider<'a, T, Message, Theme>( on_change: impl Fn(T) -> Message + 'a, ) -> Slider<'a, T, Message, Theme> where - T: Copy + From + std::cmp::PartialOrd, + T: Copy + std::cmp::PartialOrd + num_traits::AsPrimitive + num_traits::FromPrimitive, Message: Clone, Theme: slider::Catalog + 'a, { @@ -1534,7 +1534,7 @@ pub fn vertical_slider<'a, T, Message, Theme>( on_change: impl Fn(T) -> Message + 'a, ) -> VerticalSlider<'a, T, Message, Theme> where - T: Copy + From + std::cmp::PartialOrd, + T: Copy + std::cmp::PartialOrd + num_traits::AsPrimitive + num_traits::FromPrimitive, Message: Clone, Theme: vertical_slider::Catalog + 'a, { diff --git a/widget/src/slider.rs b/widget/src/slider.rs index cf5f77e091..d087d7be82 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -44,7 +44,7 @@ use crate::core::{ use std::ops::RangeInclusive; -/// An horizontal bar and a handle that selects a single value from a range of +/// A 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. @@ -89,8 +89,7 @@ where Theme: Catalog, { range: RangeInclusive, - step: f64, - shift_step: Option, + scale: Box + 'a>, value: T, default: Option, on_change: Box Message + 'a>, @@ -101,6 +100,103 @@ where status: Option, } +/// A slider [`Scale`] +pub trait Scale { + /// Snap the value to the nearest value on the [`Scale`]. + fn snap(&self, value: f64, range: RangeInclusive) -> Option; + + /// Step the value up by one value on the [`Scale`]. + fn step_up( + &self, + _t: T, + _range: RangeInclusive, + _modifiers: keyboard::Modifiers, + ) -> Option { + None + } + + /// Step the value down by one value on the [`Scale`]. + fn step_down( + &self, + _t: T, + _range: RangeInclusive, + _modifiers: keyboard::Modifiers, + ) -> Option { + None + } +} + +/// Creates a slider scale that snaps to discrete steps. +pub fn discrete(step: impl num_traits::AsPrimitive) -> DiscreteScale { + DiscreteScale { + step: step.as_(), + shift_step: None, + } +} + +/// A slider scale that snaps to discrete steps. +pub struct DiscreteScale { + step: f64, + shift_step: Option, +} + +impl DiscreteScale { + /// Sets the optional "shift" step for the slider scale. + /// + /// If set, this value is used as the step while the shift key is pressed. + pub fn shift_step(self, shift_step: impl num_traits::AsPrimitive) -> Self { + Self { + step: self.step, + shift_step: Some(shift_step.as_()), + } + } +} + +impl + num_traits::FromPrimitive> Scale for DiscreteScale { + fn snap(&self, value: f64, range: RangeInclusive) -> Option { + let start = range.start().as_(); + let value = ((value - start) / self.step).round() * self.step + start; + T::from_f64(value.min(range.end().as_())) + } + + fn step_up(&self, t: T, range: RangeInclusive, modifiers: keyboard::Modifiers) -> Option { + let step = if modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + }; + T::from_f64((t.as_() + step).min(range.end().as_())) + } + + fn step_down( + &self, + t: T, + range: RangeInclusive, + modifiers: keyboard::Modifiers, + ) -> Option { + let step = if modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + }; + T::from_f64((t.as_() - step).max(range.start().as_())) + } +} + +/// Creates a continuous slider scale. +pub fn continuous() -> ContinuousScale { + ContinuousScale +} + +/// A continuous slider scale. +pub struct ContinuousScale; + +impl Scale for ContinuousScale { + fn snap(&self, value: f64, _: RangeInclusive) -> Option { + T::from_f64(value) + } +} + impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where T: Copy + PartialOrd, @@ -121,6 +217,7 @@ where pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, + T: num_traits::AsPrimitive + num_traits::FromPrimitive, { let value = if value >= *range.start() { value @@ -138,8 +235,7 @@ where value, default: None, range, - step: 1.0, - shift_step: None, + scale: Box::new(discrete(1)), on_change: Box::new(on_change), on_release: None, width: Length::Fill, @@ -180,17 +276,9 @@ where self } - /// Sets the step size of the [`Slider`]. - pub fn step(mut self, step: impl num_traits::AsPrimitive) -> Self { - self.step = step.as_(); - self - } - - /// Sets the optional "shift" step for the [`Slider`]. - /// - /// If set, this value is used as the step while the shift key is pressed. - pub fn shift_step(mut self, shift_step: impl num_traits::AsPrimitive) -> Self { - self.shift_step = Some(shift_step.as_()); + /// Sets the [`Scale`] of the [`Slider`]. + pub fn scale(mut self, scale: impl Scale + 'a) -> Self { + self.scale = Box::new(scale); self } @@ -267,56 +355,23 @@ where } else if cursor_position.x >= bounds.x + bounds.width { Some(*self.range.end()) } else { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - }; - - let start = (*self.range.start()).as_(); - let end = (*self.range.end()).as_(); - + let start = self.range.start().as_(); + let end = self.range.end().as_(); 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; - - T::from_f64(value.min(end)) + self.scale + .snap(start + percent * (end - start), self.range.clone()) } }; let increment = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - }; - - let steps = (value.as_() / step).round(); - let new_value = step * (steps + 1.0); - - if new_value > (*self.range.end()).as_() { - return Some(*self.range.end()); - } - - T::from_f64(new_value) + self.scale + .step_up(value, self.range.clone(), state.keyboard_modifiers) }; let decrement = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - }; - - let steps = (value.as_() / step).round(); - let new_value = step * (steps - 1.0); - - if new_value < (*self.range.start()).as_() { - return Some(*self.range.start()); - } - - T::from_f64(new_value) + self.scale + .step_down(value, self.range.clone(), state.keyboard_modifiers) }; let change = |new_value: T| { diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 06500ad277..e84484cc73 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -30,7 +30,10 @@ //! ``` use std::ops::RangeInclusive; -pub use crate::slider::{Catalog, Handle, HandleShape, Status, Style, StyleFn, default}; +pub use crate::slider::{ + Catalog, ContinuousScale, DiscreteScale, Handle, HandleShape, Scale, Status, Style, StyleFn, + continuous, default, discrete, +}; use crate::core::border::Border; use crate::core::keyboard; @@ -43,7 +46,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{self, Element, Event, Length, Pixels, Point, Rectangle, Shell, Size, Widget}; -/// An vertical bar and a handle that selects a single value from a range of +/// A 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. @@ -88,8 +91,7 @@ where Theme: Catalog, { range: RangeInclusive, - step: T, - shift_step: Option, + scale: Box + 'a>, value: T, default: Option, on_change: Box Message + 'a>, @@ -102,7 +104,7 @@ where impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> where - T: Copy + From + std::cmp::PartialOrd, + T: Copy + std::cmp::PartialOrd, Message: Clone, Theme: Catalog, { @@ -120,6 +122,7 @@ where pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, + T: num_traits::AsPrimitive + num_traits::FromPrimitive, { let value = if value >= *range.start() { value @@ -137,8 +140,7 @@ where value, default: None, range, - step: T::from(1), - shift_step: None, + scale: Box::new(discrete(1)), on_change: Box::new(on_change), on_release: None, width: Self::DEFAULT_WIDTH, @@ -179,17 +181,9 @@ where 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()); + /// Sets the [`Scale`] of the [`VerticalSlider`]. + pub fn scale(mut self, scale: impl Scale + 'a) -> Self { + self.scale = Box::new(scale); self } @@ -266,60 +260,24 @@ where } 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 - } - .as_(); - - let start = (*self.range.start()).as_(); - let end = (*self.range.end()).as_(); - + let start = self.range.start().as_(); + let end = self.range.end().as_(); 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)) + self.scale + .snap(start + percent * (end - start), self.range.clone()) } }; let increment = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - } - .as_(); - - let steps = (value.as_() / step).round(); - let new_value = step * (steps + 1.0); - - if new_value > (*self.range.end()).as_() { - return Some(*self.range.end()); - } - - T::from_f64(new_value) + self.scale + .step_up(value, self.range.clone(), state.keyboard_modifiers) }; let decrement = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - } - .as_(); - - let steps = (value.as_() / step).round(); - let new_value = step * (steps - 1.0); - - if new_value < (*self.range.start()).as_() { - return Some(*self.range.start()); - } - - T::from_f64(new_value) + self.scale + .step_down(value, self.range.clone(), state.keyboard_modifiers) }; let change = |new_value: T| {