Skip to content

Commit

Permalink
Enhance Slider and VerticalSlider functionality
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jpttrssn authored and hecrj committed Jan 31, 2024
1 parent 66c8a80 commit 5e2b3d4
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 38 deletions.
33 changes: 26 additions & 7 deletions examples/slider/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ pub enum Message {

pub struct Slider {
slider_value: u8,
slider_default: u8,
slider_step: u8,
slider_step_fine: u8,
}

impl Sandbox for Slider {
type Message = Message;

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 {
Expand All @@ -35,14 +43,25 @@ impl Sandbox for Slider {

fn view(&self) -> Element<Message> {
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 =
container(slider(0..=100, value, Message::SliderChanged))
.width(250);
let h_slider = container(
slider(0..=100, value, Message::SliderChanged)
.default(default)
.step(step)
.step_fine(step_fine),
)
.width(250);

let v_slider =
container(vertical_slider(0..=100, value, Message::SliderChanged))
.height(200);
let v_slider = container(
vertical_slider(0..=100, value, Message::SliderChanged)
.default(default)
.step(step)
.step_fine(step_fine),
)
.height(200);

let text = text(format!("{value}"));

Expand Down
164 changes: 148 additions & 16 deletions widget/src/slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
//!
//! 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::mouse::{self, click};
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
Expand Down Expand Up @@ -49,7 +51,9 @@ where
{
range: RangeInclusive<T>,
step: T,
step_fine: Option<T>,
value: T,
default: Option<T>,
on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>,
width: Length,
Expand Down Expand Up @@ -92,8 +96,10 @@ where

Slider {
value,
default: None,
range,
step: T::from(1),
step_fine: None,
on_change: Box::new(on_change),
on_release: None,
width: Length::Fill,
Expand All @@ -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`].
/// This is called when the mouse is released from the slider.
///
Expand Down Expand Up @@ -136,6 +149,13 @@ where
self.step = step.into();
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>
Expand Down Expand Up @@ -188,8 +208,10 @@ where
shell,
tree.state.downcast_mut::<State>(),
&mut self.value,
self.default,
&self.range,
self.step,
self.step_fine,
self.on_change.as_ref(),
&self.on_release,
)
Expand Down Expand Up @@ -253,8 +275,10 @@ pub fn update<Message, T>(
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
default: Option<T>,
range: &RangeInclusive<T>,
step: T,
step_fine: Option<T>,
on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>,
) -> event::Status
Expand All @@ -264,14 +288,19 @@ where
{
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 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 = match step_fine {
Some(step_fine) if state.keyboard_modifiers.shift() => {
step_fine.into()
}
_ => step.into(),
};
let start = (*range.start()).into();
let end = (*range.end()).into();

Expand All @@ -281,17 +310,67 @@ 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<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(),
};

if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
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,
}

*value = new_value;
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;
}
}
};

Expand All @@ -300,8 +379,31 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) = cursor.position_over(layout.bounds())
{
change(cursor_position);
state.is_dragging = true;
let click =
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;
}
Expand All @@ -321,11 +423,31 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ = cursor.position().map(change);
let _ = cursor
.position()
.map(|point| change(Change::CursorPosition(point)));

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;
}
_ => {}
}

Expand Down Expand Up @@ -451,9 +573,11 @@ pub fn mouse_interaction(
}

/// The local state of a [`Slider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[derive(Debug, Clone, Copy, Default)]
pub struct State {
is_dragging: bool,
last_click: Option<mouse::Click>,
keyboard_modifiers: keyboard::Modifiers,
}

impl State {
Expand All @@ -462,3 +586,11 @@ impl State {
State::default()
}
}

impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.is_dragging == other.is_dragging
}
}

impl Eq for State {}
Loading

0 comments on commit 5e2b3d4

Please sign in to comment.