Enhance Slider and VerticalSlider functionality

* Add optional default behavior
  * Add a `default` field
  * Add a `default()` method to set the `default` field
  * A double-click, ctrl-click or command-click will set the slider to the default value
* Add optional fine-grained control
  * Add an optional `step_fine` field
  * Add a `step_fine()` method to set the `step_fine` field
  * Use `step_fine` in place of `step` while shift is pressed
* Add increment/decrement via up/down keys
* Update `Slider` and `VerticalSlider` examples
This commit is contained in:
Jonatan Pettersson 2023-09-20 20:56:50 +02:00 committed by Héctor Ramón Jiménez
parent 66c8a804c6
commit 5e2b3d4a51
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
3 changed files with 324 additions and 40 deletions

View file

@ -12,13 +12,21 @@ pub enum Message {
pub struct Slider { pub struct Slider {
slider_value: u8, slider_value: u8,
slider_default: u8,
slider_step: u8,
slider_step_fine: u8,
} }
impl Sandbox for Slider { impl Sandbox for Slider {
type Message = Message; type Message = Message;
fn new() -> Slider { fn new() -> Slider {
Slider { slider_value: 50 } Slider {
slider_value: 50,
slider_default: 50,
slider_step: 5,
slider_step_fine: 1,
}
} }
fn title(&self) -> String { fn title(&self) -> String {
@ -35,14 +43,25 @@ impl Sandbox for Slider {
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let value = self.slider_value; let value = self.slider_value;
let default = self.slider_default;
let step = self.slider_step;
let step_fine = self.slider_step_fine;
let h_slider = let h_slider = container(
container(slider(0..=100, value, Message::SliderChanged)) slider(0..=100, value, Message::SliderChanged)
.width(250); .default(default)
.step(step)
.step_fine(step_fine),
)
.width(250);
let v_slider = let v_slider = container(
container(vertical_slider(0..=100, value, Message::SliderChanged)) vertical_slider(0..=100, value, Message::SliderChanged)
.height(200); .default(default)
.step(step)
.step_fine(step_fine),
)
.height(200);
let text = text(format!("{value}")); let text = text(format!("{value}"));

View file

@ -2,8 +2,10 @@
//! //!
//! A [`Slider`] has some local [`State`]. //! A [`Slider`] has some local [`State`].
use crate::core::event::{self, Event}; use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
use crate::core::layout; use crate::core::layout;
use crate::core::mouse; use crate::core::mouse::{self, click};
use crate::core::renderer; use crate::core::renderer;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
@ -49,7 +51,9 @@ where
{ {
range: RangeInclusive<T>, range: RangeInclusive<T>,
step: T, step: T,
step_fine: Option<T>,
value: T, value: T,
default: Option<T>,
on_change: Box<dyn Fn(T) -> Message + 'a>, on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>, on_release: Option<Message>,
width: Length, width: Length,
@ -92,8 +96,10 @@ where
Slider { Slider {
value, value,
default: None,
range, range,
step: T::from(1), step: T::from(1),
step_fine: None,
on_change: Box::new(on_change), on_change: Box::new(on_change),
on_release: None, on_release: None,
width: Length::Fill, width: Length::Fill,
@ -102,6 +108,13 @@ where
} }
} }
/// Sets the optional default value for the [`Slider`].
/// If set, [`Slider`] will reset to this value when doubled-clicked, ctrl-clicked, or command-clicked.
pub fn default(mut self, default: impl Into<T>) -> Self {
self.default = Some(default.into());
self
}
/// Sets the release message of the [`Slider`]. /// Sets the release message of the [`Slider`].
/// This is called when the mouse is released from the slider. /// This is called when the mouse is released from the slider.
/// ///
@ -136,6 +149,13 @@ where
self.step = step.into(); self.step = step.into();
self self
} }
/// Sets the optional fine-grained step size for the [`Slider`].
/// If set, this value is used as the step size while shift is pressed.
pub fn step_fine(mut self, step_fine: impl Into<T>) -> Self {
self.step_fine = Some(step_fine.into());
self
}
} }
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@ -188,8 +208,10 @@ where
shell, shell,
tree.state.downcast_mut::<State>(), tree.state.downcast_mut::<State>(),
&mut self.value, &mut self.value,
self.default,
&self.range, &self.range,
self.step, self.step,
self.step_fine,
self.on_change.as_ref(), self.on_change.as_ref(),
&self.on_release, &self.on_release,
) )
@ -253,8 +275,10 @@ pub fn update<Message, T>(
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
state: &mut State, state: &mut State,
value: &mut T, value: &mut T,
default: Option<T>,
range: &RangeInclusive<T>, range: &RangeInclusive<T>,
step: T, step: T,
step_fine: Option<T>,
on_change: &dyn Fn(T) -> Message, on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>, on_release: &Option<Message>,
) -> event::Status ) -> event::Status
@ -264,14 +288,19 @@ where
{ {
let is_dragging = state.is_dragging; let is_dragging = state.is_dragging;
let mut change = |cursor_position: Point| { let change_cursor_position = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds(); let bounds = layout.bounds();
let new_value = if cursor_position.x <= bounds.x { let new_value = if cursor_position.x <= bounds.x {
*range.start() Some(*range.start())
} else if cursor_position.x >= bounds.x + bounds.width { } else if cursor_position.x >= bounds.x + bounds.width {
*range.end() Some(*range.end())
} else { } else {
let step = step.into(); let step = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let start = (*range.start()).into(); let start = (*range.start()).into();
let end = (*range.end()).into(); let end = (*range.end()).into();
@ -281,17 +310,67 @@ where
let steps = (percent * (end - start) / step).round(); let steps = (percent * (end - start) / step).round();
let value = steps * step + start; let value = steps * step + start;
if let Some(value) = T::from_f64(value) { T::from_f64(value)
value
} else {
return;
}
}; };
if ((*value).into() - new_value.into()).abs() > f64::EPSILON { new_value
shell.publish((on_change)(new_value)); };
*value = new_value; let increment = |value: T| -> Option<T> {
let step = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let steps = (value.into() / step).round();
let new_value = step * (steps + f64::from(1));
if new_value > (*range.end()).into() {
return Some(*range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let steps = (value.into() / step).round();
let new_value = step * (steps - f64::from(1));
if new_value < (*range.start()).into() {
return Some(*range.start());
}
T::from_f64(new_value)
};
enum Change {
Default,
CursorPosition(Point),
Increment,
Decrement,
}
let mut change = |change: Change| {
if let Some(new_value) = match change {
Change::Default => default,
Change::CursorPosition(point) => change_cursor_position(point),
Change::Increment => increment(*value),
Change::Decrement => decrement(*value),
} {
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
*value = new_value;
}
} }
}; };
@ -300,8 +379,31 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) = cursor.position_over(layout.bounds()) if let Some(cursor_position) = cursor.position_over(layout.bounds())
{ {
change(cursor_position); let click =
state.is_dragging = true; mouse::Click::new(cursor_position, state.last_click);
match click.kind() {
click::Kind::Single => {
if state.keyboard_modifiers.control()
|| state.keyboard_modifiers.command()
{
change(Change::Default);
state.is_dragging = false;
} else {
change(Change::CursorPosition(cursor_position));
state.is_dragging = true;
}
}
click::Kind::Double => {
change(Change::Default);
state.is_dragging = false;
}
mouse::click::Kind::Triple => {
state.is_dragging = false;
}
}
state.last_click = Some(click);
return event::Status::Captured; return event::Status::Captured;
} }
@ -321,11 +423,31 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. }) Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => { | Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging { if is_dragging {
let _ = cursor.position().map(change); let _ = cursor
.position()
.map(|point| change(Change::CursorPosition(point)));
return event::Status::Captured; 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) => {
change(Change::Increment);
}
Key::Named(key::Named::ArrowDown) => {
change(Change::Decrement);
}
_ => (),
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {} _ => {}
} }
@ -451,9 +573,11 @@ pub fn mouse_interaction(
} }
/// The local state of a [`Slider`]. /// The local state of a [`Slider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct State { pub struct State {
is_dragging: bool, is_dragging: bool,
last_click: Option<mouse::Click>,
keyboard_modifiers: keyboard::Modifiers,
} }
impl State { impl State {
@ -462,3 +586,11 @@ impl State {
State::default() State::default()
} }
} }
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.is_dragging == other.is_dragging
}
}
impl Eq for State {}

View file

@ -7,8 +7,11 @@ pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet};
use crate::core; use crate::core;
use crate::core::event::{self, Event}; 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::layout::{self, Layout};
use crate::core::mouse; use crate::core::mouse;
use crate::core::mouse::click;
use crate::core::renderer; use crate::core::renderer;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
@ -46,7 +49,9 @@ where
{ {
range: RangeInclusive<T>, range: RangeInclusive<T>,
step: T, step: T,
step_fine: Option<T>,
value: T, value: T,
default: Option<T>,
on_change: Box<dyn Fn(T) -> Message + 'a>, on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>, on_release: Option<Message>,
width: f32, width: f32,
@ -89,8 +94,10 @@ where
VerticalSlider { VerticalSlider {
value, value,
default: None,
range, range,
step: T::from(1), step: T::from(1),
step_fine: None,
on_change: Box::new(on_change), on_change: Box::new(on_change),
on_release: None, on_release: None,
width: Self::DEFAULT_WIDTH, width: Self::DEFAULT_WIDTH,
@ -99,6 +106,13 @@ where
} }
} }
/// Sets the optional default value for the [`VerticalSlider`].
/// If set, [`VerticalSlider`] will reset to this value when doubled-clicked, ctrl-clicked, or command-clicked.
pub fn default(mut self, default: impl Into<T>) -> Self {
self.default = Some(default.into());
self
}
/// Sets the release message of the [`VerticalSlider`]. /// Sets the release message of the [`VerticalSlider`].
/// This is called when the mouse is released from the slider. /// This is called when the mouse is released from the slider.
/// ///
@ -133,6 +147,13 @@ where
self.step = step; self.step = step;
self self
} }
/// Sets the optional fine-grained step size for the [`VerticalSlider`].
/// If set, this value is used as the step size while shift is pressed.
pub fn step_fine(mut self, step_fine: impl Into<T>) -> Self {
self.step_fine = Some(step_fine.into());
self
}
} }
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@ -185,8 +206,10 @@ where
shell, shell,
tree.state.downcast_mut::<State>(), tree.state.downcast_mut::<State>(),
&mut self.value, &mut self.value,
self.default,
&self.range, &self.range,
self.step, self.step,
self.step_fine,
self.on_change.as_ref(), self.on_change.as_ref(),
&self.on_release, &self.on_release,
) )
@ -251,8 +274,10 @@ pub fn update<Message, T>(
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
state: &mut State, state: &mut State,
value: &mut T, value: &mut T,
default: Option<T>,
range: &RangeInclusive<T>, range: &RangeInclusive<T>,
step: T, step: T,
step_fine: Option<T>,
on_change: &dyn Fn(T) -> Message, on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>, on_release: &Option<Message>,
) -> event::Status ) -> event::Status
@ -262,15 +287,20 @@ where
{ {
let is_dragging = state.is_dragging; let is_dragging = state.is_dragging;
let mut change = |cursor_position: Point| { let change_cursor_position = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds(); let bounds = layout.bounds();
let new_value = if cursor_position.y >= bounds.y + bounds.height { let new_value = if cursor_position.y >= bounds.y + bounds.height {
*range.start() Some(*range.start())
} else if cursor_position.y <= bounds.y { } else if cursor_position.y <= bounds.y {
*range.end() Some(*range.end())
} else { } else {
let step = step.into(); let step = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let start = (*range.start()).into(); let start = (*range.start()).into();
let end = (*range.end()).into(); let end = (*range.end()).into();
@ -281,17 +311,67 @@ where
let steps = (percent * (end - start) / step).round(); let steps = (percent * (end - start) / step).round();
let value = steps * step + start; let value = steps * step + start;
if let Some(value) = T::from_f64(value) { T::from_f64(value)
value
} else {
return;
}
}; };
if ((*value).into() - new_value.into()).abs() > f64::EPSILON { new_value
shell.publish((on_change)(new_value)); };
*value = new_value; let increment = |value: T| -> Option<T> {
let step = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let steps = (value.into() / step).round();
let new_value = step * (steps + f64::from(1));
if new_value > (*range.end()).into() {
return Some(*range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let steps = (value.into() / step).round();
let new_value = step * (steps - f64::from(1));
if new_value < (*range.start()).into() {
return Some(*range.start());
}
T::from_f64(new_value)
};
enum Change {
Default,
CursorPosition(Point),
Increment,
Decrement,
}
let mut change = |change: Change| {
if let Some(new_value) = match change {
Change::Default => default,
Change::CursorPosition(point) => change_cursor_position(point),
Change::Increment => increment(*value),
Change::Decrement => decrement(*value),
} {
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
*value = new_value;
}
} }
}; };
@ -300,8 +380,31 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) = cursor.position_over(layout.bounds()) if let Some(cursor_position) = cursor.position_over(layout.bounds())
{ {
change(cursor_position); let click =
state.is_dragging = true; mouse::Click::new(cursor_position, state.last_click);
match click.kind() {
click::Kind::Single => {
if state.keyboard_modifiers.control()
|| state.keyboard_modifiers.command()
{
change(Change::Default);
state.is_dragging = false;
} else {
change(Change::CursorPosition(cursor_position));
state.is_dragging = true;
}
}
click::Kind::Double => {
change(Change::Default);
state.is_dragging = false;
}
mouse::click::Kind::Triple => {
state.is_dragging = false;
}
}
state.last_click = Some(click);
return event::Status::Captured; return event::Status::Captured;
} }
@ -321,11 +424,31 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. }) Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => { | Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging { if is_dragging {
let _ = cursor.position().map(change); let _ = cursor
.position()
.map(|point| change(Change::CursorPosition(point)));
return event::Status::Captured; 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) => {
change(Change::Increment);
}
Key::Named(key::Named::ArrowDown) => {
change(Change::Decrement);
}
_ => (),
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {} _ => {}
} }
@ -451,9 +574,11 @@ pub fn mouse_interaction(
} }
/// The local state of a [`VerticalSlider`]. /// The local state of a [`VerticalSlider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct State { pub struct State {
is_dragging: bool, is_dragging: bool,
last_click: Option<mouse::Click>,
keyboard_modifiers: keyboard::Modifiers,
} }
impl State { impl State {
@ -462,3 +587,11 @@ impl State {
State::default() State::default()
} }
} }
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.is_dragging == other.is_dragging
}
}
impl Eq for State {}