diff options
Diffstat (limited to 'native')
-rw-r--r-- | native/src/widget.rs | 3 | ||||
-rw-r--r-- | native/src/widget/helpers.rs | 17 | ||||
-rw-r--r-- | native/src/widget/slider.rs | 199 | ||||
-rw-r--r-- | native/src/widget/vertical_slider.rs | 470 |
4 files changed, 531 insertions, 158 deletions
diff --git a/native/src/widget.rs b/native/src/widget.rs index a4b46ed4..efe26fc7 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -33,6 +33,7 @@ pub mod text_input; pub mod toggler; pub mod tooltip; pub mod tree; +pub mod vertical_slider; mod action; mod id; @@ -79,6 +80,8 @@ pub use toggler::Toggler; pub use tooltip::Tooltip; #[doc(no_inline)] pub use tree::Tree; +#[doc(no_inline)] +pub use vertical_slider::VerticalSlider; pub use action::Action; pub use id::Id; diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index 0bde288f..8cc1ae82 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -198,6 +198,23 @@ where widget::Slider::new(range, value, on_change) } +/// Creates a new [`VerticalSlider`]. +/// +/// [`VerticalSlider`]: widget::VerticalSlider +pub fn vertical_slider<'a, T, Message, Renderer>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> widget::VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: widget::slider::StyleSheet, +{ + widget::VerticalSlider::new(range, value, on_change) +} + /// Creates a new [`PickList`]. /// /// [`PickList`]: widget::PickList diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index bf3383d9..87030a4d 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -16,9 +16,10 @@ use std::ops::RangeInclusive; pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; -/// A bar and a handle that selects a single value from a range of values. +/// An horizontal bar and a handle that selects a single value from a range of +/// values. /// -/// A [`Slider`] will try to fill the space of its container, based on its orientation. +/// 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. @@ -52,9 +53,8 @@ where value: T, on_change: Box<dyn Fn(T) -> Message + 'a>, on_release: Option<Message>, - width: Option<Length>, - height: Option<Length>, - orientation: Orientation, + width: Length, + height: u16, style: <Renderer::Theme as StyleSheet>::Style, } @@ -65,6 +65,9 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { + /// The default height of a [`Slider`]. + pub const DEFAULT_HEIGHT: u16 = 22; + /// Creates a new [`Slider`]. /// /// It expects: @@ -95,9 +98,8 @@ where step: T::from(1), on_change: Box::new(on_change), on_release: None, - width: None, - height: None, - orientation: Default::default(), + width: Length::Fill, + height: Self::DEFAULT_HEIGHT, style: Default::default(), } } @@ -115,13 +117,13 @@ where /// Sets the width of the [`Slider`]. pub fn width(mut self, width: Length) -> Self { - self.width = Some(width); + self.width = width; self } /// Sets the height of the [`Slider`]. - pub fn height(mut self, height: Length) -> Self { - self.height = Some(height); + pub fn height(mut self, height: u16) -> Self { + self.height = height; self } @@ -139,12 +141,6 @@ where self.step = step; self } - - /// Sets the orientation of the [`Slider`]. - pub fn orientation(mut self, orientation: Orientation) -> Self { - self.orientation = orientation; - self - } } impl<'a, T, Message, Renderer> Widget<Message, Renderer> @@ -164,17 +160,11 @@ where } fn width(&self) -> Length { - match self.orientation { - Orientation::Horizontal => self.width.unwrap_or(Length::Fill), - Orientation::Vertical => Length::Shrink, - } + self.width } fn height(&self) -> Length { - match self.orientation { - Orientation::Horizontal => Length::Shrink, - Orientation::Vertical => self.height.unwrap_or(Length::Fill), - } + Length::Shrink } fn layout( @@ -182,14 +172,9 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let width = self - .width - .unwrap_or_else(|| self.orientation.default_width()); - let height = self - .height - .unwrap_or_else(|| self.orientation.default_height()); - - let limits = limits.width(width).height(height); + let limits = + limits.width(self.width).height(Length::Units(self.height)); + let size = limits.resolve(Size::ZERO); layout::Node::new(size) @@ -216,7 +201,6 @@ where self.step, self.on_change.as_ref(), &self.on_release, - self.orientation, ) } @@ -239,7 +223,6 @@ where &self.range, theme, &self.style, - self.orientation, ) } @@ -287,7 +270,6 @@ pub fn update<Message, T>( step: T, on_change: &dyn Fn(T) -> Message, on_release: &Option<Message>, - orientation: Orientation, ) -> event::Status where T: Copy + Into<f64> + num_traits::FromPrimitive, @@ -297,40 +279,17 @@ where let mut change = || { let bounds = layout.bounds(); - - let cursor_below_bounds = match orientation { - Orientation::Horizontal => cursor_position.x <= bounds.x, - Orientation::Vertical => { - cursor_position.y >= bounds.y + bounds.height - } - }; - - let cursor_above_bounds = match orientation { - Orientation::Horizontal => { - cursor_position.x >= bounds.x + bounds.width - } - Orientation::Vertical => cursor_position.y <= bounds.y, - }; - - let new_value = if cursor_below_bounds { + let new_value = if cursor_position.x <= bounds.x { *range.start() - } else if cursor_above_bounds { + } 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 = match orientation { - Orientation::Horizontal => { - f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width) - } - Orientation::Vertical => { - 1.00 - (f64::from(cursor_position.y - bounds.y) - / f64::from(bounds.height)) - } - }; + 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; @@ -395,7 +354,6 @@ pub fn draw<T, R>( range: &RangeInclusive<T>, style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>, style: &<R::Theme as StyleSheet>::Style, - orientation: Orientation, ) where T: Into<f64> + Copy, R: crate::Renderer, @@ -412,26 +370,15 @@ pub fn draw<T, R>( style_sheet.active(style) }; - let rail = match orientation { - Orientation::Horizontal => bounds.y + (bounds.height / 2.0).round(), - Orientation::Vertical => bounds.x + (bounds.width / 2.0).round(), - }; + let rail_y = bounds.y + (bounds.height / 2.0).round(); renderer.fill_quad( renderer::Quad { - bounds: match orientation { - Orientation::Horizontal => Rectangle { - x: bounds.x, - y: rail - 1.0, - width: bounds.width, - height: 2.0, - }, - Orientation::Vertical => Rectangle { - x: rail - 1.0, - y: bounds.y, - width: 2.0, - height: bounds.height, - }, + bounds: Rectangle { + x: bounds.x, + y: rail_y - 1.0, + width: bounds.width, + height: 2.0, }, border_radius: 0.0.into(), border_width: 0.0, @@ -442,19 +389,11 @@ pub fn draw<T, R>( renderer.fill_quad( renderer::Quad { - bounds: match orientation { - Orientation::Horizontal => Rectangle { - x: bounds.x, - y: rail + 1.0, - width: bounds.width, - height: 2.0, - }, - Orientation::Vertical => Rectangle { - x: rail + 1.0, - y: bounds.y, - width: 2.0, - height: bounds.height, - }, + bounds: Rectangle { + x: bounds.x, + y: rail_y + 1.0, + width: bounds.width, + height: 2.0, }, border_radius: 0.0.into(), border_width: 0.0, @@ -471,14 +410,7 @@ pub fn draw<T, R>( HandleShape::Rectangle { width, border_radius, - } => { - let handle_height = match orientation { - Orientation::Horizontal => bounds.height, - Orientation::Vertical => bounds.width, - }; - - (f32::from(width), handle_height, border_radius) - } + } => (f32::from(width), bounds.height, border_radius), }; let value = value.into() as f32; @@ -491,33 +423,17 @@ pub fn draw<T, R>( let handle_offset = if range_start >= range_end { 0.0 } else { - match orientation { - Orientation::Horizontal => { - bounds.width * (value - range_start) / (range_end - range_start) - - handle_width / 2.0 - } - Orientation::Vertical => { - bounds.height * (value - range_end) / (range_start - range_end) - - handle_width / 2.0 - } - } + bounds.width * (value - range_start) / (range_end - range_start) + - handle_width / 2.0 }; renderer.fill_quad( renderer::Quad { - bounds: match orientation { - Orientation::Horizontal => Rectangle { - x: bounds.x + handle_offset.round(), - y: rail - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - Orientation::Vertical => Rectangle { - x: rail - (handle_height / 2.0), - y: bounds.y + handle_offset.round(), - width: handle_height, - height: handle_width, - }, + 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.into(), border_width: style.handle.border_width, @@ -557,36 +473,3 @@ impl State { State::default() } } - -/// The orientation of a [`Slider`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Orientation { - #[default] - /// Default orientation. - /// Will fill the horizontal space of its container. - Horizontal, - /// Vertical orientation. - /// Will fill the vertical space of its container. - Vertical, -} - -impl Orientation { - /// The default height of a [`Slider`] in horizontal orientation. - pub const DEFAULT_HEIGHT: Length = Length::Units(22); - /// The default width of a [`Slider`] in vertical orientation. - pub const DEFAULT_WIDTH: Length = Length::Units(22); - - fn default_height(&self) -> Length { - match self { - Orientation::Horizontal => Self::DEFAULT_HEIGHT, - Orientation::Vertical => Length::Fill, - } - } - - fn default_width(&self) -> Length { - match self { - Orientation::Horizontal => Length::Fill, - Orientation::Vertical => Self::DEFAULT_WIDTH, - } - } -} diff --git a/native/src/widget/vertical_slider.rs b/native/src/widget/vertical_slider.rs new file mode 100644 index 00000000..28e8405c --- /dev/null +++ b/native/src/widget/vertical_slider.rs @@ -0,0 +1,470 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`VerticalSlider`] has some local [`State`]. +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; + +use crate::event::{self, Event}; +use crate::widget::tree::{self, Tree}; +use crate::{ + layout, mouse, renderer, touch, Background, Clipboard, Color, Element, + Layout, Length, Point, Rectangle, Shell, Size, Widget, +}; + +/// An vertical bar and a handle that selects a single value from a range of +/// values. +/// +/// A [`VerticalSlider`] will try to fill the vertical space of its container. +/// +/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ``` +/// # use iced_native::widget::vertical_slider; +/// # use iced_native::renderer::Null; +/// # +/// # type VerticalSlider<'a, T, Message> = vertical_slider::VerticalSlider<'a, T, Message, Null>; +/// # +/// #[derive(Clone)] +/// pub enum Message { +/// SliderChanged(f32), +/// } +/// +/// let value = 50.0; +/// +/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); +/// ``` +#[allow(missing_debug_implementations)] +pub struct VerticalSlider<'a, T, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message + 'a>, + on_release: Option<Message>, + width: u16, + height: Length, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default width of a [`VerticalSlider`]. + pub const DEFAULT_WIDTH: u16 = 22; + + /// Creates a new [`VerticalSlider`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`VerticalSlider`] + /// * a function that will be called when the [`VerticalSlider`] is dragged. + /// It receives the new value of the [`VerticalSlider`] 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() + }; + + VerticalSlider { + value, + range, + step: T::from(1), + on_change: Box::new(on_change), + on_release: None, + width: Self::DEFAULT_WIDTH, + height: Length::Fill, + style: Default::default(), + } + } + + /// Sets the release message of the [`VerticalSlider`]. + /// 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 [`VerticalSlider`]. + pub fn width(mut self, width: u16) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`VerticalSlider`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the style of the [`VerticalSlider`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the step size of the [`VerticalSlider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = + limits.width(Length::Units(self.width)).height(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 { + update( + event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::<State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + &self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + ) + } +} + +impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + slider: VerticalSlider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} + +/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`] +/// 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.y >= bounds.y + bounds.height { + *range.start() + } else if cursor_position.y <= bounds.y { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); + + 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 [`VerticalSlider`]. +pub fn draw<T, R>( + renderer: &mut R, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>, + style: &<R::Theme as StyleSheet>::Style, +) where + T: Into<f64> + Copy, + R: crate::Renderer, + R::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_dragging { + style_sheet.dragging(style) + } else if is_mouse_over { + style_sheet.hovered(style) + } else { + style_sheet.active(style) + }; + + let rail_x = bounds.x + (bounds.width / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - 1.0, + y: bounds.y, + width: 2.0, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x + 1.0, + y: bounds.y, + width: 2.0, + height: bounds.height, + }, + border_radius: 0.0.into(), + 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), bounds.width, 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.height * (value - range_end) / (range_start - range_end) + - handle_width / 2.0 + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - (handle_height / 2.0), + y: bounds.y + handle_offset.round(), + width: handle_height, + height: handle_width, + }, + border_radius: handle_border_radius.into(), + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`]. +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() + } +} + +/// The local state of a [`VerticalSlider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_dragging: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } +} |