diff --git a/CHANGELOG.md b/CHANGELOG.md index 2149e4fc8d..c836051443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `theme::Custom::with_fn` to generate completely custom themes. [#2067](https://github.com/iced-rs/iced/pull/2067) - `style` attribute for `Font`. [#2041](https://github.com/iced-rs/iced/pull/2041) - Texture filtering options for `Image`. [#1894](https://github.com/iced-rs/iced/pull/1894) +- `default` and `shift_step` methods for `slider` widgets. [#2100](https://github.com/iced-rs/iced/pull/2100) ### Changed - Enable WebGPU backend in `wgpu` by default instead of WebGL. [#2068](https://github.com/iced-rs/iced/pull/2068) @@ -99,6 +100,7 @@ Many thanks to... - @jhff - @jim-ec - @joshuamegnauth54 +- @jpttrssn - @lufte - @matze - @MichalLebeda diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index e83804c223..f71dac01fa 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -11,14 +11,22 @@ pub enum Message { } pub struct Slider { - slider_value: u8, + value: u8, + default: u8, + step: u8, + shift_step: u8, } impl Sandbox for Slider { type Message = Message; fn new() -> Slider { - Slider { slider_value: 50 } + Slider { + value: 50, + default: 50, + step: 5, + shift_step: 1, + } } fn title(&self) -> String { @@ -28,23 +36,29 @@ impl Sandbox for Slider { fn update(&mut self, message: Message) { match message { Message::SliderChanged(value) => { - self.slider_value = value; + self.value = value; } } } fn view(&self) -> Element { - let value = self.slider_value; - - let h_slider = - container(slider(0..=100, value, Message::SliderChanged)) - .width(250); + let h_slider = container( + slider(0..=100, self.value, Message::SliderChanged) + .default(self.default) + .step(self.step) + .shift_step(self.shift_step), + ) + .width(250); - let v_slider = - container(vertical_slider(0..=100, value, Message::SliderChanged)) - .height(200); + let v_slider = container( + vertical_slider(0..=100, self.value, Message::SliderChanged) + .default(self.default) + .step(self.step) + .shift_step(self.shift_step), + ) + .height(200); - let text = text(format!("{value}")); + let text = text(self.value); container( column![ diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 5c3b63841e..65bc1772eb 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -2,6 +2,8 @@ //! //! A [`Slider`] has some local [`State`]. use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::keyboard::key::{self, Key}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -49,7 +51,9 @@ where { range: RangeInclusive, step: T, + shift_step: Option, value: T, + default: Option, on_change: Box Message + 'a>, on_release: Option, width: Length, @@ -59,7 +63,7 @@ where impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where - T: Copy + From + std::cmp::PartialOrd, + T: Copy + From + PartialOrd, Message: Clone, Theme: StyleSheet, { @@ -92,8 +96,10 @@ where Slider { value, + default: None, range, step: T::from(1), + shift_step: None, on_change: Box::new(on_change), on_release: None, width: Length::Fill, @@ -102,6 +108,14 @@ where } } + /// Sets the optional default value for the [`Slider`]. + /// + /// If set, the [`Slider`] 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 [`Slider`]. /// This is called when the mouse is released from the slider. /// @@ -136,6 +150,14 @@ where self.step = step.into(); 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 Into) -> Self { + self.shift_step = Some(shift_step.into()); + self + } } impl<'a, T, Message, Theme, Renderer> Widget @@ -188,8 +210,10 @@ where shell, tree.state.downcast_mut::(), &mut self.value, + self.default, &self.range, self.step, + self.shift_step, self.on_change.as_ref(), &self.on_release, ) @@ -253,8 +277,10 @@ pub fn update( shell: &mut Shell<'_, Message>, state: &mut State, value: &mut T, + default: Option, range: &RangeInclusive, step: T, + shift_step: Option, on_change: &dyn Fn(T) -> Message, on_release: &Option, ) -> event::Status @@ -263,15 +289,22 @@ where Message: Clone, { let is_dragging = state.is_dragging; + let current_value = *value; - let mut change = |cursor_position: Point| { + let locate = |cursor_position: Point| -> Option { let bounds = layout.bounds(); let new_value = if cursor_position.x <= bounds.x { - *range.start() + Some(*range.start()) } else if cursor_position.x >= bounds.x + bounds.width { - *range.end() + Some(*range.end()) } else { - let step = step.into(); + let step = if state.keyboard_modifiers.shift() { + shift_step.unwrap_or(step) + } else { + step + } + .into(); + let start = (*range.start()).into(); let end = (*range.end()).into(); @@ -281,13 +314,49 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } + T::from_f64(value) }; + new_value + }; + + let increment = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + shift_step.unwrap_or(step) + } else { + step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); + + if new_value > (*range.end()).into() { + return Some(*range.end()); + } + + T::from_f64(new_value) + }; + + let decrement = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + shift_step.unwrap_or(step) + } else { + step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); + + if new_value < (*range.start()).into() { + return Some(*range.start()); + } + + T::from_f64(new_value) + }; + + let change = |new_value: T| { if ((*value).into() - new_value.into()).abs() > f64::EPSILON { shell.publish((on_change)(new_value)); @@ -300,8 +369,13 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = cursor.position_over(layout.bounds()) { - change(cursor_position); - state.is_dragging = true; + if state.keyboard_modifiers.command() { + let _ = default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(change); + state.is_dragging = true; + } return event::Status::Captured; } @@ -321,11 +395,29 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { - let _ = cursor.position().map(change); + let _ = cursor.position().and_then(locate).map(change); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if cursor.position_over(layout.bounds()).is_some() { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } return event::Status::Captured; } } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } _ => {} } @@ -454,6 +546,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, + keyboard_modifiers: keyboard::Modifiers, } impl State { diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index d3086a81f5..8f7c88da72 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -7,6 +7,8 @@ pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet}; use crate::core; use crate::core::event::{self, Event}; +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; @@ -46,7 +48,9 @@ where { range: RangeInclusive, step: T, + shift_step: Option, value: T, + default: Option, on_change: Box Message + 'a>, on_release: Option, width: f32, @@ -89,8 +93,10 @@ where 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, @@ -99,6 +105,14 @@ where } } + /// 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. /// @@ -133,6 +147,14 @@ where 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 + } } impl<'a, T, Message, Theme, Renderer> Widget @@ -185,8 +207,10 @@ where shell, tree.state.downcast_mut::(), &mut self.value, + self.default, &self.range, self.step, + self.shift_step, self.on_change.as_ref(), &self.on_release, ) @@ -251,8 +275,10 @@ pub fn update( shell: &mut Shell<'_, Message>, state: &mut State, value: &mut T, + default: Option, range: &RangeInclusive, step: T, + shift_step: Option, on_change: &dyn Fn(T) -> Message, on_release: &Option, ) -> event::Status @@ -261,16 +287,23 @@ where Message: Clone, { let is_dragging = state.is_dragging; + let current_value = *value; - let mut change = |cursor_position: Point| { + let locate = |cursor_position: Point| -> Option { let bounds = layout.bounds(); let new_value = if cursor_position.y >= bounds.y + bounds.height { - *range.start() + Some(*range.start()) } else if cursor_position.y <= bounds.y { - *range.end() + Some(*range.end()) } else { - let step = step.into(); + let step = if state.keyboard_modifiers.shift() { + shift_step.unwrap_or(step) + } else { + step + } + .into(); + let start = (*range.start()).into(); let end = (*range.end()).into(); @@ -281,13 +314,49 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } + T::from_f64(value) }; + new_value + }; + + let increment = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + shift_step.unwrap_or(step) + } else { + step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); + + if new_value > (*range.end()).into() { + return Some(*range.end()); + } + + T::from_f64(new_value) + }; + + let decrement = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + shift_step.unwrap_or(step) + } else { + step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); + + if new_value < (*range.start()).into() { + return Some(*range.start()); + } + + T::from_f64(new_value) + }; + + let change = |new_value: T| { if ((*value).into() - new_value.into()).abs() > f64::EPSILON { shell.publish((on_change)(new_value)); @@ -300,8 +369,15 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = cursor.position_over(layout.bounds()) { - change(cursor_position); - state.is_dragging = true; + if state.keyboard_modifiers.control() + || state.keyboard_modifiers.command() + { + let _ = default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(change); + state.is_dragging = true; + } return event::Status::Captured; } @@ -321,11 +397,29 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { - let _ = cursor.position().map(change); + let _ = cursor.position().and_then(locate).map(change); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if cursor.position_over(layout.bounds()).is_some() { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } return event::Status::Captured; } } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } _ => {} } @@ -454,6 +548,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, + keyboard_modifiers: keyboard::Modifiers, } impl State {