summaryrefslogtreecommitdiffstats
path: root/widget/src/slider.rs
diff options
context:
space:
mode:
Diffstat (limited to 'widget/src/slider.rs')
-rw-r--r--widget/src/slider.rs658
1 files changed, 347 insertions, 311 deletions
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index 65bc1772..f3ea9bfd 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -1,6 +1,5 @@
//! Display an interactive selector of a single value from a range of values.
-//!
-//! A [`Slider`] has some local [`State`].
+use crate::core::border;
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
@@ -10,16 +9,12 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
- Shell, Size, Widget,
+ Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
+ Rectangle, Shell, Size, Theme, Widget,
};
use std::ops::RangeInclusive;
-pub use iced_style::slider::{
- Appearance, Handle, HandleShape, Rail, StyleSheet,
-};
-
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
///
@@ -30,8 +25,7 @@ pub use iced_style::slider::{
///
/// # Example
/// ```no_run
-/// # type Slider<'a, T, Message> =
-/// # iced_widget::Slider<'a, Message, T, iced_widget::style::Theme>;
+/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
@@ -45,10 +39,7 @@ pub use iced_style::slider::{
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Slider<'a, T, Message, Theme = crate::Theme>
-where
- Theme: StyleSheet,
-{
+pub struct Slider<'a, T, Message, Theme = crate::Theme> {
range: RangeInclusive<T>,
step: T,
shift_step: Option<T>,
@@ -58,17 +49,16 @@ where
on_release: Option<Message>,
width: Length,
height: f32,
- style: Theme::Style,
+ style: Style<Theme>,
}
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
where
T: Copy + From<u8> + PartialOrd,
Message: Clone,
- Theme: StyleSheet,
{
/// The default height of a [`Slider`].
- pub const DEFAULT_HEIGHT: f32 = 22.0;
+ pub const DEFAULT_HEIGHT: f32 = 16.0;
/// Creates a new [`Slider`].
///
@@ -80,6 +70,7 @@ where
/// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
+ Theme: DefaultStyle,
F: 'a + Fn(T) -> Message,
{
let value = if value >= *range.start() {
@@ -104,7 +95,7 @@ where
on_release: None,
width: Length::Fill,
height: Self::DEFAULT_HEIGHT,
- style: Default::default(),
+ style: Theme::default_style(),
}
}
@@ -140,7 +131,7 @@ where
}
/// Sets the style of the [`Slider`].
- pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
+ pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into();
self
}
@@ -165,7 +156,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
- Theme: StyleSheet,
Renderer: crate::core::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -173,7 +163,7 @@ where
}
fn state(&self) -> tree::State {
- tree::State::new(State::new())
+ tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
@@ -203,20 +193,143 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- update(
- event,
- layout,
- cursor,
- shell,
- tree.state.downcast_mut::<State>(),
- &mut self.value,
- self.default,
- &self.range,
- self.step,
- self.shift_step,
- self.on_change.as_ref(),
- &self.on_release,
- )
+ let state = tree.state.downcast_mut::<State>();
+
+ let is_dragging = state.is_dragging;
+ let current_value = self.value;
+
+ let locate = |cursor_position: Point| -> Option<T> {
+ let bounds = layout.bounds();
+ let new_value = if cursor_position.x <= bounds.x {
+ Some(*self.range.start())
+ } 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
+ }
+ .into();
+
+ let start = (*self.range.start()).into();
+ let end = (*self.range.end()).into();
+
+ 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)
+ };
+
+ new_value
+ };
+
+ let increment = |value: T| -> Option<T> {
+ 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<T> {
+ 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.command() {
+ let _ = self.default.map(change);
+ state.is_dragging = false;
+ } else {
+ let _ = locate(cursor_position).map(change);
+ state.is_dragging = true;
+ }
+
+ return event::Status::Captured;
+ }
+ }
+ 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;
+
+ return event::Status::Captured;
+ }
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if is_dragging {
+ 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;
+ }
+ _ => {}
+ }
+
+ event::Status::Ignored
}
fn draw(
@@ -229,15 +342,92 @@ where
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- draw(
- renderer,
- layout,
- cursor,
- tree.state.downcast_ref::<State>(),
- self.value,
- &self.range,
+ let state = tree.state.downcast_ref::<State>();
+ let bounds = layout.bounds();
+ let is_mouse_over = cursor.is_over(bounds);
+
+ let style = (self.style)(
theme,
- &self.style,
+ if state.is_dragging {
+ Status::Dragged
+ } else if is_mouse_over {
+ Status::Hovered
+ } else {
+ 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.height, 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.width - handle_width) * (value - range_start)
+ / (range_end - range_start)
+ };
+
+ let rail_y = bounds.y + bounds.height / 2.0;
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: bounds.x,
+ y: rail_y - style.rail.width / 2.0,
+ width: offset + handle_width / 2.0,
+ height: style.rail.width,
+ },
+ border: Border::rounded(style.rail.border_radius),
+ ..renderer::Quad::default()
+ },
+ style.rail.colors.0,
+ );
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: bounds.x + offset + handle_width / 2.0,
+ y: rail_y - style.rail.width / 2.0,
+ width: bounds.width - offset - handle_width / 2.0,
+ height: style.rail.width,
+ },
+ border: Border::rounded(style.rail.border_radius),
+ ..renderer::Quad::default()
+ },
+ style.rail.colors.1,
+ );
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: bounds.x + offset,
+ y: rail_y - handle_height / 2.0,
+ width: handle_width,
+ height: handle_height,
+ },
+ border: Border {
+ radius: handle_border_radius,
+ width: style.handle.border_width,
+ color: style.handle.border_color,
+ },
+ ..renderer::Quad::default()
+ },
+ style.handle.color,
);
}
@@ -249,7 +439,17 @@ where
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>())
+ let state = tree.state.downcast_ref::<State>();
+ 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()
+ }
}
}
@@ -258,7 +458,7 @@ impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
where
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
Message: Clone + 'a,
- Theme: StyleSheet + 'a,
+ Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(
@@ -268,290 +468,126 @@ where
}
}
-/// Processes an [`Event`] and updates the [`State`] of a [`Slider`]
-/// accordingly.
-pub fn update<Message, T>(
- event: Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- shell: &mut Shell<'_, Message>,
- state: &mut State,
- value: &mut T,
- default: Option<T>,
- range: &RangeInclusive<T>,
- step: T,
- shift_step: Option<T>,
- on_change: &dyn Fn(T) -> Message,
- on_release: &Option<Message>,
-) -> event::Status
-where
- T: Copy + Into<f64> + num_traits::FromPrimitive,
- Message: Clone,
-{
- let is_dragging = state.is_dragging;
- let current_value = *value;
-
- let locate = |cursor_position: Point| -> Option<T> {
- let bounds = layout.bounds();
- let new_value = if cursor_position.x <= bounds.x {
- Some(*range.start())
- } else if cursor_position.x >= bounds.x + bounds.width {
- Some(*range.end())
- } else {
- 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();
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+struct State {
+ is_dragging: bool,
+ keyboard_modifiers: keyboard::Modifiers,
+}
- let percent = f64::from(cursor_position.x - bounds.x)
- / f64::from(bounds.width);
+/// The possible status of a [`Slider`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Status {
+ /// The [`Slider`] can be interacted with.
+ Active,
+ /// The [`Slider`] is being hovered.
+ Hovered,
+ /// The [`Slider`] is being dragged.
+ Dragged,
+}
- let steps = (percent * (end - start) / step).round();
- let value = steps * step + start;
+/// The appearance of a slider.
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The colors of the rail of the slider.
+ pub rail: Rail,
+ /// The appearance of the [`Handle`] of the slider.
+ pub handle: Handle,
+}
- T::from_f64(value)
+impl Appearance {
+ /// Changes the [`HandleShape`] of the [`Appearance`] to a circle
+ /// with the given radius.
+ pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
+ self.handle.shape = HandleShape::Circle {
+ radius: radius.into().0,
};
+ self
+ }
+}
- new_value
- };
-
- let increment = |value: T| -> Option<T> {
- 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<T> {
- 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));
-
- *value = new_value;
- }
- };
+/// The appearance of a slider rail
+#[derive(Debug, Clone, Copy)]
+pub struct Rail {
+ /// The colors of the rail of the slider.
+ pub colors: (Color, Color),
+ /// The width of the stroke of a slider rail.
+ pub width: f32,
+ /// The border radius of the corners of the rail.
+ pub border_radius: border::Radius,
+}
- 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.command() {
- let _ = default.map(change);
- state.is_dragging = false;
- } else {
- let _ = locate(cursor_position).map(change);
- state.is_dragging = true;
- }
+/// The appearance of the handle of a slider.
+#[derive(Debug, Clone, Copy)]
+pub struct Handle {
+ /// The shape of the handle.
+ pub shape: HandleShape,
+ /// The [`Color`] of the handle.
+ pub color: Color,
+ /// The border width of the handle.
+ pub border_width: f32,
+ /// The border [`Color`] of the handle.
+ pub border_color: Color,
+}
- return event::Status::Captured;
- }
- }
- 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) = on_release.clone() {
- shell.publish(on_release);
- }
- state.is_dragging = false;
+/// The shape of the handle of a slider.
+#[derive(Debug, Clone, Copy)]
+pub enum HandleShape {
+ /// A circular handle.
+ Circle {
+ /// The radius of the circle.
+ radius: f32,
+ },
+ /// A rectangular shape.
+ Rectangle {
+ /// The width of the rectangle.
+ width: u16,
+ /// The border radius of the corners of the rectangle.
+ border_radius: border::Radius,
+ },
+}
- return event::Status::Captured;
- }
- }
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- if is_dragging {
- let _ = cursor.position().and_then(locate).map(change);
+/// The style of a [`Slider`].
+pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
- 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);
- }
- _ => (),
- }
+/// The default style of a [`Slider`].
+pub trait DefaultStyle {
+ /// Returns the default style of a [`Slider`].
+ fn default_style() -> Style<Self>;
+}
- return event::Status::Captured;
- }
- }
- Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
- state.keyboard_modifiers = modifiers;
- }
- _ => {}
+impl DefaultStyle for Theme {
+ fn default_style() -> Style<Self> {
+ default
}
-
- event::Status::Ignored
}
-/// Draws a [`Slider`].
-pub fn draw<T, Theme, Renderer>(
- renderer: &mut Renderer,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- state: &State,
- value: T,
- range: &RangeInclusive<T>,
- theme: &Theme,
- style: &Theme::Style,
-) where
- T: Into<f64> + Copy,
- Theme: StyleSheet,
- Renderer: crate::core::Renderer,
-{
- let bounds = layout.bounds();
- let is_mouse_over = cursor.is_over(bounds);
-
- let style = if state.is_dragging {
- theme.dragging(style)
- } else if is_mouse_over {
- theme.hovered(style)
- } else {
- theme.active(style)
- };
-
- 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.height, border_radius),
- };
-
- let value = value.into() as f32;
- let (range_start, range_end) = {
- let (start, end) = range.clone().into_inner();
+impl DefaultStyle for Appearance {
+ fn default_style() -> Style<Self> {
+ |appearance, _status| *appearance
+ }
+}
- (start.into() as f32, end.into() as f32)
- };
+/// The default style of a [`Slider`].
+pub fn default(theme: &Theme, status: Status) -> Appearance {
+ let palette = theme.extended_palette();
- let offset = if range_start >= range_end {
- 0.0
- } else {
- (bounds.width - handle_width) * (value - range_start)
- / (range_end - range_start)
+ let color = match status {
+ Status::Active => palette.primary.strong.color,
+ Status::Hovered => palette.primary.base.color,
+ Status::Dragged => palette.primary.strong.color,
};
- let rail_y = bounds.y + bounds.height / 2.0;
-
- renderer.fill_quad(
- renderer::Quad {
- bounds: Rectangle {
- x: bounds.x,
- y: rail_y - style.rail.width / 2.0,
- width: offset + handle_width / 2.0,
- height: style.rail.width,
- },
- border: Border::with_radius(style.rail.border_radius),
- ..renderer::Quad::default()
+ Appearance {
+ rail: Rail {
+ colors: (color, palette.secondary.base.color),
+ width: 4.0,
+ border_radius: 2.0.into(),
},
- style.rail.colors.0,
- );
-
- renderer.fill_quad(
- renderer::Quad {
- bounds: Rectangle {
- x: bounds.x + offset + handle_width / 2.0,
- y: rail_y - style.rail.width / 2.0,
- width: bounds.width - offset - handle_width / 2.0,
- height: style.rail.width,
- },
- border: Border::with_radius(style.rail.border_radius),
- ..renderer::Quad::default()
- },
- style.rail.colors.1,
- );
-
- renderer.fill_quad(
- renderer::Quad {
- bounds: Rectangle {
- x: bounds.x + offset,
- y: rail_y - handle_height / 2.0,
- width: handle_width,
- height: handle_height,
- },
- border: Border {
- radius: handle_border_radius,
- width: style.handle.border_width,
- color: style.handle.border_color,
- },
- ..renderer::Quad::default()
+ handle: Handle {
+ shape: HandleShape::Circle { radius: 7.0 },
+ color,
+ border_color: Color::TRANSPARENT,
+ border_width: 0.0,
},
- style.handle.color,
- );
-}
-
-/// Computes the current [`mouse::Interaction`] of a [`Slider`].
-pub fn mouse_interaction(
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- state: &State,
-) -> mouse::Interaction {
- 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()
- }
-}
-
-/// The local state of a [`Slider`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct State {
- is_dragging: bool,
- keyboard_modifiers: keyboard::Modifiers,
-}
-
-impl State {
- /// Creates a new [`State`].
- pub fn new() -> State {
- State::default()
}
}