diff options
Diffstat (limited to '')
-rw-r--r-- | native/src/widget/slider.rs | 397 | ||||
-rw-r--r-- | pure/Cargo.toml | 1 | ||||
-rw-r--r-- | pure/src/widget.rs | 19 | ||||
-rw-r--r-- | pure/src/widget/slider.rs | 253 |
4 files changed, 503 insertions, 167 deletions
diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 917bfc14..e41e895d 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -143,6 +143,215 @@ where } } +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update<Message, T>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive<T>, + step: 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 mut change = || { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + *range.start() + } else if cursor_position.x >= bounds.x + bounds.width { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*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; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + 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) = 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 { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T>( + renderer: &mut impl crate::Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet, +) where + T: Into<f64> + Copy, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), f32::from(bounds.height), border_radius), + }; + + let value = value.into() as f32; + let (range_start, range_end) = { + let (start, end) = range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`Slider`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } +} + +/// Hashes the layout of a [`Slider`]. +pub fn hash_layout(state: &mut Hasher, width: Length) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + width.hash(state); +} + /// The local state of a [`Slider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { @@ -193,73 +402,18 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let is_dragging = self.state.is_dragging; - - let mut change = || { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - *self.range.start() - } else if cursor_position.x >= bounds.x + bounds.width { - *self.range.end() - } else { - let step = 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; - - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } - }; - - 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 layout.bounds().contains(cursor_position) { - change(); - self.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); - } - self.state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - change(); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored + update( + event, + layout, + cursor_position, + shell, + &mut self.state, + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) } fn draw( @@ -270,90 +424,15 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let style = if self.state.is_dragging { - self.style_sheet.dragging() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let rail_y = bounds.y + (bounds.height / 2.0).round(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail_colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y + 2.0, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(style.rail_colors.1), - ); - - let (handle_width, handle_height, handle_border_radius) = match style - .handle - .shape - { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), f32::from(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 handle_offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) - }; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - border_radius: handle_border_radius, - border_width: style.handle.border_width, - border_color: style.handle.border_color, - }, - style.handle.color, - ); + draw( + renderer, + layout, + cursor_position, + &self.state, + self.value, + &self.range, + self.style_sheet.as_ref(), + ) } fn mouse_interaction( @@ -363,23 +442,11 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if self.state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position, &self.state) } fn hash_layout(&self, state: &mut Hasher) { - struct Marker; - std::any::TypeId::of::<Marker>().hash(state); - - self.width.hash(state); + hash_layout(state, self.width) } } diff --git a/pure/Cargo.toml b/pure/Cargo.toml index bdfe43ae..317dccdf 100644 --- a/pure/Cargo.toml +++ b/pure/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] iced_native = { version = "0.4", path = "../native" } iced_style = { version = "0.3", path = "../style" } +num-traits = "0.2" diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 93298c61..009741a8 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -7,6 +7,7 @@ mod container; mod element; mod row; mod scrollable; +mod slider; mod text; mod text_input; mod tree; @@ -19,6 +20,7 @@ pub use element::Element; pub use image::Image; pub use row::Row; pub use scrollable::Scrollable; +pub use slider::Slider; pub use text::Text; pub use text_input::TextInput; pub use tree::Tree; @@ -36,8 +38,6 @@ pub trait Widget<Message, Renderer> { fn state(&self) -> Box<dyn Any>; - fn diff(&self, tree: &mut Tree); - fn children_state(&self) -> Vec<Tree>; fn width(&self) -> Length; @@ -62,6 +62,8 @@ pub trait Widget<Message, Renderer> { viewport: &Rectangle, ); + fn diff(&self, _tree: &mut Tree) {} + fn mouse_interaction( &self, _state: &Tree, @@ -149,6 +151,19 @@ where TextInput::new(placeholder, value, on_change) } +pub fn slider<'a, Message, Renderer, T>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> Slider<'a, T, Message> +where + Message: Clone, + Renderer: iced_native::Renderer, + T: Copy + From<u8> + std::cmp::PartialOrd, +{ + Slider::new(range, value, on_change) +} + pub fn image<Handle>(handle: Handle) -> Image<Handle> { Image::new(handle) } diff --git a/pure/src/widget/slider.rs b/pure/src/widget/slider.rs new file mode 100644 index 00000000..f659c2ed --- /dev/null +++ b/pure/src/widget/slider.rs @@ -0,0 +1,253 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::slider; +use iced_native::{ + Clipboard, Hasher, Layout, Length, Point, Rectangle, Shell, Size, +}; + +use std::any::{self, Any}; +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// An 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. +/// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ``` +/// # use iced_native::widget::slider::{self, Slider}; +/// # +/// #[derive(Clone)] +/// pub enum Message { +/// SliderChanged(f32), +/// } +/// +/// let state = &mut slider::State::new(); +/// let value = 50.0; +/// +/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct Slider<'a, T, Message> { + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message + 'a>, + on_release: Option<Message>, + width: Length, + height: u16, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, T, Message> Slider<'a, T, Message> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, +{ + /// The default height of a [`Slider`]. + pub const DEFAULT_HEIGHT: u16 = 22; + + /// Creates a new [`Slider`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`Slider`] + /// * a function that will be called when the [`Slider`] is dragged. + /// It receives the new value of the [`Slider`] and must produce a + /// `Message`. + pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + + Slider { + value, + range, + step: T::from(1), + on_change: Box::new(on_change), + on_release: None, + width: Length::Fill, + height: Self::DEFAULT_HEIGHT, + style_sheet: Default::default(), + } + } + + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. + /// + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`Slider`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Slider`]. + pub fn height(mut self, height: u16) -> Self { + self.height = height; + self + } + + /// Sets the style of the [`Slider`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message> +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<slider::State>() + } + + fn state(&self) -> Box<dyn Any> { + Box::new(slider::State::new()) + } + + fn children_state(&self) -> Vec<Tree> { + Vec::new() + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = + limits.width(self.width).height(Length::Units(self.height)); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + slider::update( + event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::<slider::State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + slider::draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<slider::State>(), + self.value, + &self.range, + self.style_sheet.as_ref(), + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + slider::mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::<slider::State>(), + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + slider::hash_layout(state, self.width) + } +} + +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + iced_native::Renderer, +{ + fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} |