From d91f4f6aa74d0693179a02167d626efa3ac4c20b Mon Sep 17 00:00:00 2001 From: Bingus Date: Sat, 19 Nov 2022 10:29:37 -0800 Subject: Add multidirectional scrolling capabilities to the existing Scrollable. --- native/src/widget/column.rs | 2 - native/src/widget/operation/scrollable.rs | 19 +- native/src/widget/scrollable.rs | 1116 +++++++++++++++++++---------- 3 files changed, 763 insertions(+), 374 deletions(-) (limited to 'native/src/widget') diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index f2ef132a..5ad4d858 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -10,8 +10,6 @@ use crate::{ Shell, Widget, }; -use std::u32; - /// A container that distributes its contents vertically. #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs index 2210137d..1e8b7543 100644 --- a/native/src/widget/operation/scrollable.rs +++ b/native/src/widget/operation/scrollable.rs @@ -1,27 +1,22 @@ //! Operate on widgets that can be scrolled. use crate::widget::{Id, Operation}; +use iced_core::Vector; /// The internal state of a widget that can be scrolled. pub trait Scrollable { /// Snaps the scroll of the widget to the given `percentage`. - fn snap_to(&mut self, percentage: f32); + fn snap_to(&mut self, percentage: Vector); } /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to /// the provided `percentage`. -pub fn snap_to(target: Id, percentage: f32) -> impl Operation { +pub fn snap_to(target: Id, percentage: Vector) -> impl Operation { struct SnapTo { target: Id, - percentage: f32, + percentage: Vector, } impl Operation for SnapTo { - fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { - if Some(&self.target) == id { - state.snap_to(self.percentage); - } - } - fn container( &mut self, _id: Option<&Id>, @@ -29,6 +24,12 @@ pub fn snap_to(target: Id, percentage: f32) -> impl Operation { ) { operate_on_children(self) } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.percentage); + } + } } SnapTo { target, percentage } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 20780f89..ec081343 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -13,8 +13,6 @@ use crate::{ Rectangle, Shell, Size, Vector, Widget, }; -use std::{f32, u32}; - pub use iced_style::scrollable::StyleSheet; pub mod style { @@ -37,8 +35,9 @@ where scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, + scroll_horizontal: Option, content: Element<'a, Message, Renderer>, - on_scroll: Option Message + 'a>>, + on_scroll: Option) -> Message + 'a>>, style: ::Style, } @@ -55,6 +54,7 @@ where scrollbar_width: 10, scrollbar_margin: 0, scroller_width: 10, + scroll_horizontal: None, content: content.into(), on_scroll: None, style: Default::default(), @@ -74,7 +74,7 @@ where } /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. + /// Silently enforces a minimum width of 1. pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { self.scrollbar_width = scrollbar_width.max(1); self @@ -88,17 +88,26 @@ where /// Sets the scroller width of the [`Scrollable`] . /// - /// It silently enforces a minimum value of 1. + /// It silently enforces a minimum width of 1. pub fn scroller_width(mut self, scroller_width: u16) -> Self { self.scroller_width = scroller_width.max(1); self } + /// Allow scrolling in a horizontal direction within the [`Scrollable`] . + pub fn horizontal_scroll(mut self, horizontal: Horizontal) -> Self { + self.scroll_horizontal = Some(horizontal); + self + } + /// Sets a function to call when the [`Scrollable`] is scrolled. /// - /// The function takes the new relative offset of the [`Scrollable`] - /// (e.g. `0` means top, while `1` means bottom). - pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self { + /// The function takes the new relative x & y offset of the [`Scrollable`] + /// (e.g. `0` means beginning, while `1` means end). + pub fn on_scroll( + mut self, + f: impl Fn(Vector) -> Message + 'a, + ) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -113,28 +122,57 @@ where } } -impl<'a, Message, Renderer> Widget - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() +/// Properties of a horizontal scrollbar within a [`Scrollable`]. +#[derive(Debug)] +pub struct Horizontal { + scrollbar_height: u16, + scrollbar_margin: u16, + scroller_height: u16, +} + +impl Default for Horizontal { + fn default() -> Self { + Self { + scrollbar_height: 10, + scrollbar_margin: 0, + scroller_height: 10, + } } +} - fn state(&self) -> tree::State { - tree::State::new(State::new()) +impl Horizontal { + /// Creates a new [`Horizontal`] for use in a [`Scrollable`]. + pub fn new() -> Self { + Self::default() } - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] + /// Sets the [`Horizontal`] scrollbar height of the [`Scrollable`] . + /// Silently enforces a minimum height of 1. + pub fn scrollbar_height(mut self, scrollbar_height: u16) -> Self { + self.scrollbar_height = scrollbar_height.max(1); + self } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + /// Sets the [`Horizontal`] scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self } + /// Sets the scroller height of the [`Horizontal`] scrollbar of the [`Scrollable`] . + /// Silently enforces a minimum height of 1. + pub fn scroller_height(mut self, scroller_height: u16) -> Self { + self.scroller_height = scroller_height.max(1); + self + } +} + +impl<'a, Message, Renderer> Widget + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ fn width(&self) -> Length { self.content.as_widget().width() } @@ -153,18 +191,64 @@ where limits, Widget::::width(self), self.height, - u32::MAX, + self.scroll_horizontal.is_some(), |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, ) } + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::(), + renderer, + theme, + layout, + cursor_position, + &self.style, + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn operate( &self, tree: &mut Tree, layout: Layout<'_>, - renderer: &Renderer, operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::(); @@ -175,7 +259,6 @@ where self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), - renderer, operation, ); }); @@ -201,6 +284,7 @@ where self.scrollbar_width, self.scrollbar_margin, self.scroller_width, + self.scroll_horizontal.as_ref(), &self.on_scroll, |event, layout, cursor_position, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -216,40 +300,6 @@ where ) } - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - tree.state.downcast_ref::(), - renderer, - theme, - layout, - cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - &self.style, - |renderer, layout, cursor_position, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - fn mouse_interaction( &self, tree: &Tree, @@ -262,9 +312,6 @@ where tree.state.downcast_ref::(), layout, cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, |layout, cursor_position, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -278,13 +325,13 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.content - .as_widget_mut() + .as_widget() .overlay( &mut tree.children[0], layout.children().next().unwrap(), @@ -294,12 +341,15 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = tree + let (offset_x, offset_y) = tree .state .downcast_ref::() .offset(bounds, content_bounds); - overlay.translate(Vector::new(0.0, -(offset as f32))) + overlay.translate(Vector::new( + -(offset_x as f32), + -(offset_y as f32), + )) }) } } @@ -344,7 +394,10 @@ impl From for widget::Id { /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage`. -pub fn snap_to(id: Id, percentage: f32) -> Command { +pub fn snap_to( + id: Id, + percentage: Vector, +) -> Command { Command::widget(operation::scrollable::snap_to(id.0, percentage)) } @@ -354,14 +407,29 @@ pub fn layout( limits: &layout::Limits, width: Length, height: Length, - max_height: u32, + horizontal_enabled: bool, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.max_height(max_height).width(width).height(height); + let limits = limits + .max_height(u32::MAX) + .max_width(if horizontal_enabled { + u32::MAX + } else { + limits.max().width as u32 + }) + .width(width) + .height(height); let child_limits = layout::Limits::new( Size::new(limits.min().width, 0.0), - Size::new(limits.max().width, f32::INFINITY), + Size::new( + if horizontal_enabled { + f32::INFINITY + } else { + limits.max().width + }, + f32::MAX, + ), ); let content = layout_content(renderer, &child_limits); @@ -382,7 +450,8 @@ pub fn update( scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, - on_scroll: &Option Message + '_>>, + horizontal: Option<&Horizontal>, + on_scroll: &Option) -> Message + '_>>, update_content: impl FnOnce( Event, Layout<'_>, @@ -392,36 +461,39 @@ pub fn update( ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let mouse_over_scrollable = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let scrollbar = scrollbar( - state, + state.create_scrollbars_maybe( + horizontal, scrollbar_width, scrollbar_margin, scroller_width, bounds, content_bounds, ); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); + + let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) = + state.mouse_over_scrollbars(cursor_position); let event_status = { - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + let (offset_x, offset_y) = state.offset(bounds, content_bounds); + Point::new( - cursor_position.x, - cursor_position.y + state.offset(bounds, content_bounds) as f32, + cursor_position.x + offset_x as f32, + cursor_position.y + offset_y as f32, ) } else { // TODO: Make `cursor_position` an `Option` so we can encode // cursor availability. // This will probably happen naturally once we add multi-window // support. - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; update_content( @@ -437,18 +509,18 @@ pub fn update( return event::Status::Captured; } - if is_mouse_over { + if mouse_over_scrollable { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) - state.scroll(y * 60.0, bounds, content_bounds); + let delta = match delta { + mouse::ScrollDelta::Lines { x, y } => { + // TODO: Configurable speed/friction (?) + Vector::new(x * 60.0, y * 60.0) } - mouse::ScrollDelta::Pixels { y, .. } => { - state.scroll(y, bounds, content_bounds); - } - } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; + + state.scroll(delta, bounds, content_bounds); notify_on_scroll( state, @@ -463,18 +535,21 @@ pub fn update( Event::Touch(event) => { match event { touch::Event::FingerPressed { .. } => { - state.scroll_box_touched_at = Some(cursor_position); + state.scroll_area_touched_at = Some(cursor_position); } touch::Event::FingerMoved { .. } => { if let Some(scroll_box_touched_at) = - state.scroll_box_touched_at + state.scroll_area_touched_at { - let delta = - cursor_position.y - scroll_box_touched_at.y; + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); state.scroll(delta, bounds, content_bounds); - state.scroll_box_touched_at = Some(cursor_position); + state.scroll_area_touched_at = + Some(cursor_position); notify_on_scroll( state, @@ -487,7 +562,7 @@ pub fn update( } touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. } => { - state.scroll_box_touched_at = None; + state.scroll_area_touched_at = None; } } @@ -497,21 +572,21 @@ pub fn update( } } - if state.is_scroller_grabbed() { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.scroller_grabbed_at = None; + if let Some(scrollbar) = &mut state.scrollbar_y { + if let Some(scroller_grabbed_at) = scrollbar.scroller.grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + scrollbar.scroller.grabbed_at = None; - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, state.scroller_grabbed_at) - { - state.scroll_to( + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + scrollbar.scroll_to( scrollbar.scroll_percentage( scroller_grabbed_at, cursor_position, @@ -530,18 +605,18 @@ pub fn update( return event::Status::Captured; } + _ => {} } - _ => {} - } - } else if is_mouse_over_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { + } else if scrollbar.is_mouse_over(cursor_position) { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) { - state.scroll_to( + scrollbar.scroll_to( scrollbar.scroll_percentage( scroller_grabbed_at, cursor_position, @@ -550,7 +625,8 @@ pub fn update( content_bounds, ); - state.scroller_grabbed_at = Some(scroller_grabbed_at); + scrollbar.scroller.grabbed_at = + Some(scroller_grabbed_at); notify_on_scroll( state, @@ -559,12 +635,84 @@ pub fn update( content_bounds, shell, ); + } - return event::Status::Captured; + return event::Status::Captured; + } + _ => {} + } + } + } + + if let Some(scrollbar) = &mut state.scrollbar_x { + if let Some(scroller_grabbed_at) = scrollbar.scroller.grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + scrollbar.scroller.grabbed_at = None; + + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + scrollbar.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + _ => {} + } + } else if scrollbar.is_mouse_over(cursor_position) { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) + { + scrollbar.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + scrollbar.scroller.grabbed_at = + Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } + + return event::Status::Captured; } + _ => {} } - _ => {} } } @@ -576,9 +724,6 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor_position: Point, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, content_interaction: impl FnOnce( Layout<'_>, Point, @@ -586,39 +731,38 @@ pub fn mouse_interaction( ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); + let mouse_over_scrollable = bounds.contains(cursor_position); + let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbar = scrollbar( - state, - scrollbar_width, - scrollbar_margin, - scroller_width, - bounds, - content_bounds, - ); - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); + let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) = + state.mouse_over_scrollbars(cursor_position); - if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) + || state.scrollers_grabbed() + { mouse::Interaction::Idle } else { - let offset = state.offset(bounds, content_bounds); + let (offset_x, offset_y) = state.offset(bounds, content_bounds); - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + Point::new( + cursor_position.x + offset_x as f32, + cursor_position.y + offset_y as f32, + ) } else { - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; content_interaction( content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + y: bounds.y + offset_y as f32, + x: bounds.x + offset_x as f32, ..bounds }, ) @@ -632,9 +776,6 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor_position: Point, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), ) where @@ -644,39 +785,38 @@ pub fn draw( let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = state.offset(bounds, content_bounds); - let scrollbar = scrollbar( - state, - scrollbar_width, - scrollbar_margin, - scroller_width, - bounds, - content_bounds, - ); - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); + let (offset_x, offset_y) = state.offset(bounds, content_bounds); - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) + let mouse_over_scrollable = bounds.contains(cursor_position); + + let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) = + state.mouse_over_scrollbars(cursor_position); + + let cursor_position = if mouse_over_scrollable + && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) + { + Point::new( + cursor_position.x + offset_x as f32, + cursor_position.y + offset_y as f32, + ) } else { - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; - if let Some(scrollbar) = scrollbar { + // Draw inner content + if state.scrollbar_y.is_some() || state.scrollbar_x.is_some() { renderer.with_layer(bounds, |renderer| { renderer.with_translation( - Vector::new(0.0, -(offset as f32)), + Vector::new(-(offset_x as f32), -(offset_y as f32)), |renderer| { draw_content( renderer, content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + y: bounds.y + offset_y as f32, + x: bounds.x + offset_x as f32, ..bounds }, ); @@ -684,16 +824,65 @@ pub fn draw( ); }); - let style = if state.is_scroller_grabbed() { - theme.dragging(style) - } else if is_mouse_over_scrollbar { - theme.hovered(style) - } else { - theme.active(style) - }; + let draw_scrollbar = + |renderer: &mut Renderer, scrollbar: Option<&Scrollbar>| { + if let Some(scrollbar) = scrollbar { + let style = match scrollbar.direction { + Direction::Vertical => { + if scrollbar.scroller.grabbed_at.is_some() { + theme.dragging(style) + } else if mouse_over_y_scrollbar { + theme.hovered(style) + } else { + theme.active(style) + } + } + Direction::Horizontal => { + if scrollbar.scroller.grabbed_at.is_some() { + theme.dragging_horizontal(style) + } else if mouse_over_x_scrollbar { + theme.hovered_horizontal(style) + } else { + theme.active_horizontal(style) + } + } + }; - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0.0; + //track + if style.background.is_some() + || (style.border_color != Color::TRANSPARENT + && style.border_width > 0.0) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background.unwrap_or(Background::Color( + Color::TRANSPARENT, + )), + ); + } + + //thumb + if style.scroller.color != Color::TRANSPARENT + || (style.scroller.border_color != Color::TRANSPARENT + && style.scroller.border_width > 0.0) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); + } + } + }; renderer.with_layer( Rectangle { @@ -702,33 +891,8 @@ pub fn draw( ..bounds }, |renderer| { - if is_scrollbar_visible { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius.into(), - border_width: style.border_width, - border_color: style.border_color, - }, - style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - if (is_mouse_over || state.is_scroller_grabbed()) - && is_scrollbar_visible - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius.into(), - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } + draw_scrollbar(renderer, state.scrollbar_y.as_ref()); + draw_scrollbar(renderer, state.scrollbar_x.as_ref()); }, ); } else { @@ -737,226 +901,403 @@ pub fn draw( content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + x: bounds.x + offset_x as f32, + y: bounds.y + offset_y as f32, ..bounds }, ); } } -fn scrollbar( - state: &State, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - bounds: Rectangle, - content_bounds: Rectangle, -) -> Option { - let offset = state.offset(bounds, content_bounds); - - if content_bounds.height > bounds.height { - let outer_width = - scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; - - let outer_bounds = Rectangle { - x: bounds.x + bounds.width - outer_width as f32, - y: bounds.y, - width: outer_width as f32, - height: bounds.height, - }; - - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + scrollbar_width / 2), - y: bounds.y, - width: scrollbar_width as f32, - height: bounds.height, - }; - - let ratio = bounds.height / content_bounds.height; - let scroller_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + scroller_width / 2), - y: scrollbar_bounds.y + y_offset, - width: scroller_width as f32, - height: scroller_height, - }; - - Some(Scrollbar { - outer_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - } -} - fn notify_on_scroll( state: &State, - on_scroll: &Option Message + '_>>, + on_scroll: &Option) -> Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, ) { - if content_bounds.height <= bounds.height { - return; - } - if let Some(on_scroll) = on_scroll { - shell.publish(on_scroll( - state.offset.absolute(bounds, content_bounds) - / (content_bounds.height - bounds.height), - )); - } -} + let delta_x = if content_bounds.width <= bounds.width { + 0.0 + } else { + state.scrollbar_x.map_or(0.0, |scrollbar| { + scrollbar.offset.absolute( + Direction::Horizontal, + bounds, + content_bounds, + ) / (content_bounds.width - bounds.width) + }) + }; -/// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy)] -pub struct State { - scroller_grabbed_at: Option, - scroll_box_touched_at: Option, - offset: Offset, -} + let delta_y = if content_bounds.height <= bounds.height { + 0.0 + } else { + state.scrollbar_y.map_or(0.0, |scrollbar| { + scrollbar.offset.absolute( + Direction::Vertical, + bounds, + content_bounds, + ) / (content_bounds.height - bounds.height) + }) + }; -impl Default for State { - fn default() -> Self { - Self { - scroller_grabbed_at: None, - scroll_box_touched_at: None, - offset: Offset::Absolute(0.0), - } + shell.publish(on_scroll(Vector::new(delta_x, delta_y))) } } -impl operation::Scrollable for State { - fn snap_to(&mut self, percentage: f32) { - State::snap_to(self, percentage); - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The direction of the [`Scrollable`]. +pub enum Direction { + /// X or horizontal + Horizontal, + /// Y or vertical + Vertical, } /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy)] -enum Offset { - Absolute(f32), - Relative(f32), +#[derive(Debug, Clone, Copy, Default)] +pub struct State { + scroll_area_touched_at: Option, + scrollbar_x: Option, + scrollbar_y: Option, } -impl Offset { - fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { - match self { - Self::Absolute(absolute) => { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0); - - absolute.min(hidden_content) - } - Self::Relative(percentage) => { - ((content_bounds.height - bounds.height) * percentage).max(0.0) - } +impl operation::Scrollable for State { + fn snap_to(&mut self, percentage: Vector) { + if let Some(scrollbar) = &mut self.scrollbar_y { + scrollbar.snap_to(percentage.y) + } + if let Some(scrollbar) = &mut self.scrollbar_x { + scrollbar.snap_to(percentage.x) } } } impl State { - /// Creates a new [`State`] with the scrollbar located at the top. + /// Creates a new [`State`]. pub fn new() -> Self { State::default() } - /// Apply a scrolling offset to the current [`State`], given the bounds of - /// the [`Scrollable`] and its contents. - pub fn scroll( + /// Create y or x scrollbars if content is overflowing the [`Scrollable`] bounds. + pub fn create_scrollbars_maybe( &mut self, - delta_y: f32, + horizontal: Option<&Horizontal>, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, bounds: Rectangle, content_bounds: Rectangle, ) { - if bounds.height >= content_bounds.height { - return; - } + let show_scrollbar_x = horizontal.and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); - self.offset = Offset::Absolute( - (self.offset.absolute(bounds, content_bounds) - delta_y) - .clamp(0.0, content_bounds.height - bounds.height), - ); - } + self.scrollbar_y = if content_bounds.height > bounds.height { + let (offset_y, scroller_grabbed) = + if let Some(scrollbar) = &self.scrollbar_y { + ( + scrollbar.offset.absolute( + scrollbar.direction, + bounds, + content_bounds, + ), + scrollbar.scroller.grabbed_at, + ) + } else { + (0.0, None) + }; + + // Need to adjust the height of the vertical scrollbar if the horizontal scrollbar + // is present + let scrollbar_x_height = show_scrollbar_x.map_or(0.0, |h| { + (h.scrollbar_height.max(h.scroller_height) + h.scrollbar_margin) + as f32 + }); + + let total_scrollbar_width = + scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width - total_scrollbar_width as f32, + y: bounds.y, + width: total_scrollbar_width as f32, + height: (bounds.height - scrollbar_x_height).max(0.0), + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from( + total_scrollbar_width / 2 + scrollbar_width / 2, + ), + y: bounds.y, + width: scrollbar_width as f32, + height: (bounds.height - scrollbar_x_height).max(0.0), + }; + + let ratio = bounds.height / content_bounds.height; + // min height for easier grabbing with super tall content + let scroller_height = (bounds.height * ratio).max(2.0); + let scroller_offset = offset_y as f32 * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(total_scrollbar_width / 2 + scroller_width / 2), + y: (scrollbar_bounds.y + scroller_offset - scrollbar_x_height) + .max(0.0), + width: scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + direction: Direction::Vertical, + scroller: Scroller { + bounds: scroller_bounds, + grabbed_at: scroller_grabbed, + }, + offset: Offset::Absolute(offset_y), + }) + } else { + None + }; - /// Scrolls the [`Scrollable`] to a relative amount. - /// - /// `0` represents scrollbar at the top, while `1` represents scrollbar at - /// the bottom. - pub fn scroll_to( - &mut self, - percentage: f32, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - self.snap_to(percentage); - self.unsnap(bounds, content_bounds); + self.scrollbar_x = if let Some(horizontal) = show_scrollbar_x { + let (offset_x, scroller_grabbed) = + if let Some(scrollbar) = &self.scrollbar_x { + ( + scrollbar.offset.absolute( + scrollbar.direction, + bounds, + content_bounds, + ), + scrollbar.scroller.grabbed_at, + ) + } else { + (0.0, None) + }; + + // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar + // is present + let scrollbar_y_width = self.scrollbar_y.map_or(0.0, |_| { + (scrollbar_width.max(scroller_width) + scrollbar_margin) as f32 + }); + + let total_scrollbar_height = + horizontal.scrollbar_height.max(horizontal.scroller_height) + + 2 * horizontal.scrollbar_margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height - total_scrollbar_height as f32, + width: (bounds.width - scrollbar_y_width).max(0.0), + height: total_scrollbar_height as f32, + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height + - f32::from( + total_scrollbar_height / 2 + + horizontal.scrollbar_height / 2, + ), + width: (bounds.width - scrollbar_y_width).max(0.0), + height: horizontal.scrollbar_height as f32, + }; + + let ratio = bounds.width / content_bounds.width; + // min width for easier grabbing with extra wide content + let scroller_width = (bounds.width * ratio).max(2.0); + let scroller_offset = offset_x as f32 * ratio; + + let scroller_bounds = Rectangle { + x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) + .max(0.0), + y: bounds.y + bounds.height + - f32::from( + total_scrollbar_height / 2 + + horizontal.scroller_height / 2, + ), + width: scroller_width, + height: horizontal.scroller_height as f32, + }; + + Some(Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + direction: Direction::Horizontal, + scroller: Scroller { + bounds: scroller_bounds, + grabbed_at: scroller_grabbed, + }, + offset: Offset::Absolute(offset_x), + }) + } else { + None + }; } - /// Snaps the scroll position to a relative amount. - /// - /// `0` represents scrollbar at the top, while `1` represents scrollbar at - /// the bottom. - pub fn snap_to(&mut self, percentage: f32) { - self.offset = Offset::Relative(percentage.clamp(0.0, 1.0)); + /// Returns whether the mouse is within the bounds of each scrollbar. + fn mouse_over_scrollbars(&self, cursor_position: Point) -> (bool, bool) { + ( + self.scrollbar_x.map_or(false, |scrollbar| { + scrollbar.is_mouse_over(cursor_position) + }), + self.scrollbar_y.map_or(false, |scrollbar| { + scrollbar.is_mouse_over(cursor_position) + }), + ) } - /// Unsnaps the current scroll position, if snapped, given the bounds of the - /// [`Scrollable`] and its contents. - pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { - self.offset = - Offset::Absolute(self.offset.absolute(bounds, content_bounds)); + /// Returns whether the scroller for either scrollbar is currently grabbed. + fn scrollers_grabbed(&self) -> bool { + self.scrollbar_x + .map_or(false, |scrollbar| scrollbar.scroller.grabbed_at.is_some()) + || self.scrollbar_y.map_or(false, |scrollbar| { + scrollbar.scroller.grabbed_at.is_some() + }) } - /// Returns the current scrolling offset of the [`State`], given the bounds - /// of the [`Scrollable`] and its contents. - pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { - self.offset.absolute(bounds, content_bounds) as u32 - } + /// Apply a scrolling offset to the current [`State`], given the bounds of + /// the [`Scrollable`] and its contents. + pub fn scroll( + &mut self, + delta: Vector, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + if delta.x != 0.0 && bounds.width < content_bounds.width { + if let Some(scrollbar) = &mut self.scrollbar_x { + scrollbar.offset = Offset::Absolute( + (scrollbar.offset.absolute( + Direction::Horizontal, + bounds, + content_bounds, + ) - delta.x) + .max(0.0) + .min((content_bounds.width - bounds.width) as f32), + ); + } + } - /// Returns whether the scroller is currently grabbed or not. - pub fn is_scroller_grabbed(&self) -> bool { - self.scroller_grabbed_at.is_some() + if delta.y != 0.0 && bounds.height < content_bounds.height { + if let Some(scrollbar) = &mut self.scrollbar_y { + scrollbar.offset = Offset::Absolute( + (scrollbar.offset.absolute( + Direction::Vertical, + bounds, + content_bounds, + ) - delta.y) + .max(0.0) + .min((content_bounds.height - bounds.height) as f32), + ) + } + } } - /// Returns whether the scroll box is currently touched or not. - pub fn is_scroll_box_touched(&self) -> bool { - self.scroll_box_touched_at.is_some() + /// Returns the current x & y scrolling offset of the [`State`], given the bounds + /// of the [`Scrollable`] and its contents. + pub fn offset( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> (f32, f32) { + ( + self.scrollbar_x.map_or(0.0, |scrollbar| { + scrollbar.offset.absolute( + Direction::Horizontal, + bounds, + content_bounds, + ) + }), + self.scrollbar_y.map_or(0.0, |scrollbar| { + scrollbar.offset.absolute( + Direction::Vertical, + bounds, + content_bounds, + ) + }), + ) } } /// The scrollbar of a [`Scrollable`]. -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] struct Scrollbar { - /// The outer bounds of the scrollable, including the [`Scrollbar`] and - /// [`Scroller`]. - outer_bounds: Rectangle, + /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, + /// and the scrollbar margin. + total_bounds: Rectangle, - /// The bounds of the [`Scrollbar`]. + /// The bounds of just the [`Scrollbar`]. bounds: Rectangle, - /// The bounds of the [`Scroller`]. + /// The direction of the [`Scrollbar`]. + direction: Direction, + + /// The state of this scrollbar's [`Scroller`]. scroller: Scroller, + + /// The current offset of the [`Scrollbar`]. + offset: Offset, } impl Scrollbar { + /// Snaps the scroll position to a relative amount. + /// + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn snap_to(&mut self, percentage: f32) { + self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); + } + + /// Unsnaps the current scroll position if snapped, given the bounds of the [`Scrollable`] + /// and its contents. + pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { + self.offset = Offset::Absolute(self.offset.absolute( + self.direction, + bounds, + content_bounds, + )); + } + + /// Scrolls the [`Scrollbar`] to a certain percentage. + fn scroll_to( + &mut self, + percentage: f32, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.snap_to(percentage); + self.unsnap(bounds, content_bounds); + } + + /// Returns whether the mouse is over the scrollbar or not. fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.outer_bounds.contains(cursor_position) + self.total_bounds.contains(cursor_position) } fn grab_scroller(&self, cursor_position: Point) -> Option { - if self.outer_bounds.contains(cursor_position) { + if self.total_bounds.contains(cursor_position) { Some(if self.scroller.bounds.contains(cursor_position) { - (cursor_position.y - self.scroller.bounds.y) - / self.scroller.bounds.height + match self.direction { + Direction::Vertical => { + (cursor_position.y - self.scroller.bounds.y) + / self.scroller.bounds.height + } + Direction::Horizontal => { + (cursor_position.x - self.scroller.bounds.x) + / self.scroller.bounds.width + } + } } else { 0.5 }) @@ -970,10 +1311,56 @@ impl Scrollbar { grabbed_at: f32, cursor_position: Point, ) -> f32 { - (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height) + match self.direction { + Direction::Vertical => { + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) + } + Direction::Horizontal => { + (cursor_position.x + - self.bounds.x + - self.scroller.bounds.width * grabbed_at) + / (self.bounds.width - self.scroller.bounds.width) + } + } + } +} + +/// The directional offset of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute( + self, + direction: Direction, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> f32 { + match self { + Self::Absolute(absolute) => match direction { + Direction::Horizontal => { + absolute.min((content_bounds.width - bounds.width).max(0.0)) + } + Direction::Vertical => absolute + .min((content_bounds.height - bounds.height).max(0.0)), + }, + Self::Relative(percentage) => match direction { + Direction::Horizontal => { + ((content_bounds.width - bounds.width) * percentage) + .max(0.0) + } + Direction::Vertical => { + ((content_bounds.height - bounds.height) * percentage) + .max(0.0) + } + }, + } } } @@ -982,4 +1369,7 @@ impl Scrollbar { struct Scroller { /// The bounds of the [`Scroller`]. bounds: Rectangle, + + /// Whether or not the scroller is currently grabbed. + grabbed_at: Option, } -- cgit From 9f85e0c721927f1e3bd195a998ec7a80ec0e7455 Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Sat, 24 Dec 2022 21:27:44 -0800 Subject: Reworked Scrollable to account for lack of widget order guarantees. Fixed thumb "snapping" bug on scrollable when cursor is out of bounds. --- native/src/widget/operation/scrollable.rs | 10 +- native/src/widget/scrollable.rs | 1133 ++++++++++++++--------------- 2 files changed, 537 insertions(+), 606 deletions(-) (limited to 'native/src/widget') diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs index 1e8b7543..6981b1f4 100644 --- a/native/src/widget/operation/scrollable.rs +++ b/native/src/widget/operation/scrollable.rs @@ -1,19 +1,19 @@ //! Operate on widgets that can be scrolled. use crate::widget::{Id, Operation}; -use iced_core::Vector; +use iced_core::Point; /// The internal state of a widget that can be scrolled. pub trait Scrollable { - /// Snaps the scroll of the widget to the given `percentage`. - fn snap_to(&mut self, percentage: Vector); + /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. + fn snap_to(&mut self, percentage: Point); } /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to /// the provided `percentage`. -pub fn snap_to(target: Id, percentage: Vector) -> impl Operation { +pub fn snap_to(target: Id, percentage: Point) -> impl Operation { struct SnapTo { target: Id, - percentage: Vector, + percentage: Point, } impl Operation for SnapTo { diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index ec081343..39f970d7 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -32,12 +32,10 @@ where { id: Option, height: Length, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - scroll_horizontal: Option, + vertical: Properties, + horizontal: Option, content: Element<'a, Message, Renderer>, - on_scroll: Option) -> Message + 'a>>, + on_scroll: Option Message + 'a>>, style: ::Style, } @@ -51,10 +49,8 @@ where Scrollable { id: None, height: Length::Shrink, - scrollbar_width: 10, - scrollbar_margin: 0, - scroller_width: 10, - scroll_horizontal: None, + vertical: Properties::default(), + horizontal: None, content: content.into(), on_scroll: None, style: Default::default(), @@ -73,30 +69,15 @@ where self } - /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. - pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { - self.scrollbar_width = scrollbar_width.max(1); - self - } - - /// Sets the scrollbar margin of the [`Scrollable`] . - pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { - self.scrollbar_margin = scrollbar_margin; - self - } - - /// Sets the scroller width of the [`Scrollable`] . - /// - /// It silently enforces a minimum width of 1. - pub fn scroller_width(mut self, scroller_width: u16) -> Self { - self.scroller_width = scroller_width.max(1); + /// Configures the vertical scrollbar of the [`Scrollable`] . + pub fn vertical_scroll(mut self, properties: Properties) -> Self { + self.vertical = properties; self } - /// Allow scrolling in a horizontal direction within the [`Scrollable`] . - pub fn horizontal_scroll(mut self, horizontal: Horizontal) -> Self { - self.scroll_horizontal = Some(horizontal); + /// Configures the horizontal scrollbar of the [`Scrollable`] . + pub fn horizontal_scroll(mut self, properties: Properties) -> Self { + self.horizontal = Some(properties); self } @@ -104,10 +85,7 @@ where /// /// The function takes the new relative x & y offset of the [`Scrollable`] /// (e.g. `0` means beginning, while `1` means end). - pub fn on_scroll( - mut self, - f: impl Fn(Vector) -> Message + 'a, - ) -> Self { + pub fn on_scroll(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -122,47 +100,47 @@ where } } -/// Properties of a horizontal scrollbar within a [`Scrollable`]. +/// Properties of a scrollbar within a [`Scrollable`]. #[derive(Debug)] -pub struct Horizontal { - scrollbar_height: u16, - scrollbar_margin: u16, - scroller_height: u16, +pub struct Properties { + width: u16, + margin: u16, + scroller_width: u16, } -impl Default for Horizontal { +impl Default for Properties { fn default() -> Self { Self { - scrollbar_height: 10, - scrollbar_margin: 0, - scroller_height: 10, + width: 10, + margin: 0, + scroller_width: 10, } } } -impl Horizontal { - /// Creates a new [`Horizontal`] for use in a [`Scrollable`]. +impl Properties { + /// Creates new [`Properties`] for use in a [`Scrollable`]. pub fn new() -> Self { Self::default() } - /// Sets the [`Horizontal`] scrollbar height of the [`Scrollable`] . - /// Silently enforces a minimum height of 1. - pub fn scrollbar_height(mut self, scrollbar_height: u16) -> Self { - self.scrollbar_height = scrollbar_height.max(1); + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum width of 1. + pub fn width(mut self, width: u16) -> Self { + self.width = width.max(1); self } - /// Sets the [`Horizontal`] scrollbar margin of the [`Scrollable`] . - pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { - self.scrollbar_margin = scrollbar_margin; + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn margin(mut self, margin: u16) -> Self { + self.margin = margin; self } - /// Sets the scroller height of the [`Horizontal`] scrollbar of the [`Scrollable`] . - /// Silently enforces a minimum height of 1. - pub fn scroller_height(mut self, scroller_height: u16) -> Self { - self.scroller_height = scroller_height.max(1); + /// Sets the scroller width of the [`Scrollable`] . + /// Silently enforces a minimum width of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); self } } @@ -173,6 +151,22 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn width(&self) -> Length { self.content.as_widget().width() } @@ -191,64 +185,18 @@ where limits, Widget::::width(self), self.height, - self.scroll_horizontal.is_some(), + self.horizontal.is_some(), |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, ) } - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - tree.state.downcast_ref::(), - renderer, - theme, - layout, - cursor_position, - &self.style, - |renderer, layout, cursor_position, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - fn operate( &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::(); @@ -259,6 +207,7 @@ where self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), + renderer, operation, ); }); @@ -281,10 +230,8 @@ where cursor_position, clipboard, shell, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - self.scroll_horizontal.as_ref(), + &self.vertical, + self.horizontal.as_ref(), &self.on_scroll, |event, layout, cursor_position, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -300,6 +247,39 @@ where ) } + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::(), + renderer, + theme, + layout, + cursor_position, + &self.vertical, + self.horizontal.as_ref(), + &self.style, + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + fn mouse_interaction( &self, tree: &Tree, @@ -312,6 +292,8 @@ where tree.state.downcast_ref::(), layout, cursor_position, + &self.vertical, + self.horizontal.as_ref(), |layout, cursor_position, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -325,13 +307,13 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.content - .as_widget() + .as_widget_mut() .overlay( &mut tree.children[0], layout.children().next().unwrap(), @@ -341,15 +323,12 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let (offset_x, offset_y) = tree + let offset = tree .state .downcast_ref::() .offset(bounds, content_bounds); - overlay.translate(Vector::new( - -(offset_x as f32), - -(offset_y as f32), - )) + overlay.translate(Vector::new(-offset.x, -offset.y)) }) } } @@ -393,10 +372,10 @@ impl From for widget::Id { } /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage`. +/// to the provided `percentage` along the x & y axis. pub fn snap_to( id: Id, - percentage: Vector, + percentage: Point, ) -> Command { Command::widget(operation::scrollable::snap_to(id.0, percentage)) } @@ -447,11 +426,9 @@ pub fn update( cursor_position: Point, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - horizontal: Option<&Horizontal>, - on_scroll: &Option) -> Message + '_>>, + vertical: &Properties, + horizontal: Option<&Properties>, + on_scroll: &Option Message + '_>>, update_content: impl FnOnce( Event, Layout<'_>, @@ -466,28 +443,17 @@ pub fn update( let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - state.create_scrollbars_maybe( - horizontal, - scrollbar_width, - scrollbar_margin, - scroller_width, - bounds, - content_bounds, - ); + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) = - state.mouse_over_scrollbars(cursor_position); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); let event_status = { let cursor_position = if mouse_over_scrollable && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) { - let (offset_x, offset_y) = state.offset(bounds, content_bounds); - - Point::new( - cursor_position.x + offset_x as f32, - cursor_position.y + offset_y as f32, - ) + cursor_position + state.offset(bounds, content_bounds) } else { // TODO: Make `cursor_position` an `Option` so we can encode // cursor availability. @@ -572,22 +538,20 @@ pub fn update( } } - if let Some(scrollbar) = &mut state.scrollbar_y { - if let Some(scroller_grabbed_at) = scrollbar.scroller.grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - scrollbar.scroller.grabbed_at = None; + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.y_scroller_grabbed_at = None; - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - scrollbar.scroll_to( - scrollbar.scroll_percentage( + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + state.scroll_y_to( + scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, ), @@ -605,61 +569,56 @@ pub fn update( return event::Status::Captured; } - _ => {} } - } else if scrollbar.is_mouse_over(cursor_position) { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - scrollbar.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - scrollbar.scroller.grabbed_at = - Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let (Some(scroller_grabbed_at), Some(scrollbar)) = + (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) + { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); - return event::Status::Captured; + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } - _ => {} + + return event::Status::Captured; } + _ => {} } } - if let Some(scrollbar) = &mut state.scrollbar_x { - if let Some(scroller_grabbed_at) = scrollbar.scroller.grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - scrollbar.scroller.grabbed_at = None; + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.x_scroller_grabbed_at = None; - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - scrollbar.scroll_to( - scrollbar.scroll_percentage( + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, ), @@ -674,45 +633,42 @@ pub fn update( content_bounds, shell, ); - - return event::Status::Captured; } - _ => {} + + return event::Status::Captured; } - } else if scrollbar.is_mouse_over(cursor_position) { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - scrollbar.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - scrollbar.scroller.grabbed_at = - Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let (Some(scroller_grabbed_at), Some(scrollbar)) = + (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) + { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); return event::Status::Captured; } - _ => {} } + _ => {} } } @@ -724,6 +680,8 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor_position: Point, + vertical: &Properties, + horizontal: Option<&Properties>, content_interaction: impl FnOnce( Layout<'_>, Point, @@ -736,23 +694,23 @@ pub fn mouse_interaction( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) = - state.mouse_over_scrollbars(cursor_position); + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() { mouse::Interaction::Idle } else { - let (offset_x, offset_y) = state.offset(bounds, content_bounds); + let offset = state.offset(bounds, content_bounds); let cursor_position = if mouse_over_scrollable && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) { - Point::new( - cursor_position.x + offset_x as f32, - cursor_position.y + offset_y as f32, - ) + cursor_position + offset } else { Point::new(-1.0, -1.0) }; @@ -761,8 +719,8 @@ pub fn mouse_interaction( content_layout, cursor_position, &Rectangle { - y: bounds.y + offset_y as f32, - x: bounds.x + offset_x as f32, + y: bounds.y + offset.y, + x: bounds.x + offset.x, ..bounds }, ) @@ -776,6 +734,8 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor_position: Point, + vertical: &Properties, + horizontal: Option<&Properties>, style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), ) where @@ -786,37 +746,36 @@ pub fn draw( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let (offset_x, offset_y) = state.offset(bounds, content_bounds); + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let mouse_over_scrollable = bounds.contains(cursor_position); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); - let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) = - state.mouse_over_scrollbars(cursor_position); + let offset = state.offset(bounds, content_bounds); let cursor_position = if mouse_over_scrollable && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) { - Point::new( - cursor_position.x + offset_x as f32, - cursor_position.y + offset_y as f32, - ) + cursor_position + offset } else { Point::new(-1.0, -1.0) }; // Draw inner content - if state.scrollbar_y.is_some() || state.scrollbar_x.is_some() { + if scrollbars.active() { renderer.with_layer(bounds, |renderer| { renderer.with_translation( - Vector::new(-(offset_x as f32), -(offset_y as f32)), + Vector::new(-offset.x, -offset.y), |renderer| { draw_content( renderer, content_layout, cursor_position, &Rectangle { - y: bounds.y + offset_y as f32, - x: bounds.x + offset_x as f32, + y: bounds.y + offset.y, + x: bounds.x + offset.x, ..bounds }, ); @@ -825,62 +784,41 @@ pub fn draw( }); let draw_scrollbar = - |renderer: &mut Renderer, scrollbar: Option<&Scrollbar>| { - if let Some(scrollbar) = scrollbar { - let style = match scrollbar.direction { - Direction::Vertical => { - if scrollbar.scroller.grabbed_at.is_some() { - theme.dragging(style) - } else if mouse_over_y_scrollbar { - theme.hovered(style) - } else { - theme.active(style) - } - } - Direction::Horizontal => { - if scrollbar.scroller.grabbed_at.is_some() { - theme.dragging_horizontal(style) - } else if mouse_over_x_scrollbar { - theme.hovered_horizontal(style) - } else { - theme.active_horizontal(style) - } - } - }; - - //track - if style.background.is_some() - || (style.border_color != Color::TRANSPARENT - && style.border_width > 0.0) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background.unwrap_or(Background::Color( - Color::TRANSPARENT, - )), - ); - } + |renderer: &mut Renderer, + style: style::Scrollbar, + scrollbar: &Scrollbar| { + //track + if style.background.is_some() + || (style.border_color != Color::TRANSPARENT + && style.border_width > 0.0) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius.into(), + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } - //thumb - if style.scroller.color != Color::TRANSPARENT - || (style.scroller.border_color != Color::TRANSPARENT - && style.scroller.border_width > 0.0) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius, - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } + //thumb + if style.scroller.color != Color::TRANSPARENT + || (style.scroller.border_color != Color::TRANSPARENT + && style.scroller.border_width > 0.0) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius.into(), + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); } }; @@ -891,8 +829,31 @@ pub fn draw( ..bounds }, |renderer| { - draw_scrollbar(renderer, state.scrollbar_y.as_ref()); - draw_scrollbar(renderer, state.scrollbar_x.as_ref()); + //draw y scrollbar + if let Some(scrollbar) = scrollbars.y { + let style = if state.y_scroller_grabbed_at.is_some() { + theme.dragging(style) + } else if mouse_over_y_scrollbar { + theme.hovered(style) + } else { + theme.active(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } + + //draw x scrollbar + if let Some(scrollbar) = scrollbars.x { + let style = if state.x_scroller_grabbed_at.is_some() { + theme.dragging_horizontal(style) + } else if mouse_over_x_scrollbar { + theme.hovered_horizontal(style) + } else { + theme.active_horizontal(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } }, ); } else { @@ -901,8 +862,8 @@ pub fn draw( content_layout, cursor_position, &Rectangle { - x: bounds.x + offset_x as f32, - y: bounds.y + offset_y as f32, + x: bounds.x + offset.x, + y: bounds.y + offset.y, ..bounds }, ); @@ -911,84 +872,198 @@ pub fn draw( fn notify_on_scroll( state: &State, - on_scroll: &Option) -> Message + '_>>, + on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, ) { if let Some(on_scroll) = on_scroll { - let delta_x = if content_bounds.width <= bounds.width { - 0.0 - } else { - state.scrollbar_x.map_or(0.0, |scrollbar| { - scrollbar.offset.absolute( - Direction::Horizontal, - bounds, - content_bounds, - ) / (content_bounds.width - bounds.width) - }) - }; + if content_bounds <= bounds { + return; + } - let delta_y = if content_bounds.height <= bounds.height { - 0.0 - } else { - state.scrollbar_y.map_or(0.0, |scrollbar| { - scrollbar.offset.absolute( - Direction::Vertical, - bounds, - content_bounds, - ) / (content_bounds.height - bounds.height) - }) - }; + let x_offset = state.offset_x.absolute_x(bounds, content_bounds) + / (content_bounds.width - bounds.width); - shell.publish(on_scroll(Vector::new(delta_x, delta_y))) - } -} + let y_offset = state.offset_y.absolute_y(bounds, content_bounds) + / (content_bounds.height - bounds.height); -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// The direction of the [`Scrollable`]. -pub enum Direction { - /// X or horizontal - Horizontal, - /// Y or vertical - Vertical, + shell.publish(on_scroll(Point::new(x_offset, y_offset))) + } } /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct State { scroll_area_touched_at: Option, - scrollbar_x: Option, - scrollbar_y: Option, + offset_y: Offset, + y_scroller_grabbed_at: Option, + offset_x: Offset, + x_scroller_grabbed_at: Option, +} + +impl Default for State { + fn default() -> Self { + Self { + scroll_area_touched_at: None, + offset_y: Offset::Absolute(0.0), + y_scroller_grabbed_at: None, + offset_x: Offset::Absolute(0.0), + x_scroller_grabbed_at: None, + } + } } impl operation::Scrollable for State { - fn snap_to(&mut self, percentage: Vector) { - if let Some(scrollbar) = &mut self.scrollbar_y { - scrollbar.snap_to(percentage.y) + fn snap_to(&mut self, percentage: Point) { + State::snap_to(self, percentage); + } +} + +/// The offset of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute_x(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + match self { + Offset::Absolute(absolute) => { + absolute.min((content_bounds.width - bounds.width).max(0.0)) + } + Offset::Relative(percentage) => { + ((content_bounds.width - bounds.width) * percentage).max(0.0) + } } - if let Some(scrollbar) = &mut self.scrollbar_x { - scrollbar.snap_to(percentage.x) + } + + fn absolute_y(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + match self { + Offset::Absolute(absolute) => { + absolute.min((content_bounds.height - bounds.height).max(0.0)) + } + Offset::Relative(percentage) => { + ((content_bounds.height - bounds.height) * percentage).max(0.0) + } } } } impl State { - /// Creates a new [`State`]. + /// Creates a new [`State`] with the scrollbar(s) at the beginning. pub fn new() -> Self { State::default() } - /// Create y or x scrollbars if content is overflowing the [`Scrollable`] bounds. - pub fn create_scrollbars_maybe( + /// Apply a scrolling offset to the current [`State`], given the bounds of + /// the [`Scrollable`] and its contents. + pub fn scroll( + &mut self, + delta: Vector, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + if bounds.height < content_bounds.height { + self.offset_y = Offset::Absolute( + (self.offset_y.absolute_y(bounds, content_bounds) - delta.y) + .clamp(0.0, content_bounds.height - bounds.height), + ) + } + + if bounds.width < content_bounds.width { + self.offset_x = Offset::Absolute( + (self.offset_x.absolute_x(bounds, content_bounds) - delta.x) + .clamp(0.0, content_bounds.width - bounds.width), + ); + } + } + + /// Scrolls the [`Scrollable`] to a relative amount along the y axis. + /// + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_y_to( + &mut self, + percentage: f32, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)); + self.unsnap(bounds, content_bounds); + } + + /// Scrolls the [`Scrollable`] to a relative amount along the x axis. + /// + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_x_to( &mut self, - horizontal: Option<&Horizontal>, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + percentage: f32, bounds: Rectangle, content_bounds: Rectangle, ) { + self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)); + self.unsnap(bounds, content_bounds); + } + + /// Snaps the scroll position to a relative amount. + /// + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn snap_to(&mut self, percentage: Point) { + self.offset_x = Offset::Relative(percentage.x.clamp(0.0, 1.0)); + self.offset_y = Offset::Relative(percentage.y.clamp(0.0, 1.0)); + } + + /// Unsnaps the current scroll position, if snapped, given the bounds of the + /// [`Scrollable`] and its contents. + pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { + self.offset_x = + Offset::Absolute(self.offset_x.absolute_x(bounds, content_bounds)); + self.offset_y = + Offset::Absolute(self.offset_y.absolute_y(bounds, content_bounds)); + } + + /// Returns the current x & y scrolling offset of the [`State`], given the bounds + /// of the [`Scrollable`] and its contents. + pub fn offset( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Point { + Point::new( + self.offset_x.absolute_x(bounds, content_bounds), + self.offset_y.absolute_y(bounds, content_bounds), + ) + } + + /// Returns whether any scroller is currently grabbed or not. + pub fn scrollers_grabbed(&self) -> bool { + self.x_scroller_grabbed_at.is_some() + || self.y_scroller_grabbed_at.is_some() + } +} + +#[derive(Debug)] +/// State of both [`Scrollbar`]s. +struct Scrollbars { + y: Option, + x: Option, +} + +impl Scrollbars { + /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. + fn new( + state: &State, + vertical: &Properties, + horizontal: Option<&Properties>, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Self { + let offset = state.offset(bounds, content_bounds); + let show_scrollbar_x = horizontal.and_then(|h| { if content_bounds.width > bounds.width { Some(h) @@ -997,59 +1072,47 @@ impl State { } }); - self.scrollbar_y = if content_bounds.height > bounds.height { - let (offset_y, scroller_grabbed) = - if let Some(scrollbar) = &self.scrollbar_y { - ( - scrollbar.offset.absolute( - scrollbar.direction, - bounds, - content_bounds, - ), - scrollbar.scroller.grabbed_at, - ) - } else { - (0.0, None) - }; + let y_scrollbar = if content_bounds.height > bounds.height { + let Properties { + width, + margin, + scroller_width, + } = *vertical; - // Need to adjust the height of the vertical scrollbar if the horizontal scrollbar + // Adjust the height of the vertical scrollbar if the horizontal scrollbar // is present - let scrollbar_x_height = show_scrollbar_x.map_or(0.0, |h| { - (h.scrollbar_height.max(h.scroller_height) + h.scrollbar_margin) - as f32 + let x_scrollbar_height = show_scrollbar_x.map_or(0.0, |h| { + (h.width.max(h.scroller_width) + h.margin) as f32 }); - let total_scrollbar_width = - scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + let total_scrollbar_width = width.max(scroller_width) + 2 * margin; // Total bounds of the scrollbar + margin + scroller width let total_scrollbar_bounds = Rectangle { x: bounds.x + bounds.width - total_scrollbar_width as f32, y: bounds.y, width: total_scrollbar_width as f32, - height: (bounds.height - scrollbar_x_height).max(0.0), + height: (bounds.height - x_scrollbar_height).max(0.0), }; // Bounds of just the scrollbar let scrollbar_bounds = Rectangle { x: bounds.x + bounds.width - - f32::from( - total_scrollbar_width / 2 + scrollbar_width / 2, - ), + - f32::from(total_scrollbar_width / 2 + width / 2), y: bounds.y, - width: scrollbar_width as f32, - height: (bounds.height - scrollbar_x_height).max(0.0), + width: width as f32, + height: (bounds.height - x_scrollbar_height).max(0.0), }; let ratio = bounds.height / content_bounds.height; // min height for easier grabbing with super tall content let scroller_height = (bounds.height * ratio).max(2.0); - let scroller_offset = offset_y as f32 * ratio; + let scroller_offset = offset.y * ratio; let scroller_bounds = Rectangle { x: bounds.x + bounds.width - f32::from(total_scrollbar_width / 2 + scroller_width / 2), - y: (scrollbar_bounds.y + scroller_offset - scrollbar_x_height) + y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height) .max(0.0), width: scroller_width as f32, height: scroller_height, @@ -1058,41 +1121,29 @@ impl State { Some(Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - direction: Direction::Vertical, scroller: Scroller { bounds: scroller_bounds, - grabbed_at: scroller_grabbed, }, - offset: Offset::Absolute(offset_y), }) } else { None }; - self.scrollbar_x = if let Some(horizontal) = show_scrollbar_x { - let (offset_x, scroller_grabbed) = - if let Some(scrollbar) = &self.scrollbar_x { - ( - scrollbar.offset.absolute( - scrollbar.direction, - bounds, - content_bounds, - ), - scrollbar.scroller.grabbed_at, - ) - } else { - (0.0, None) - }; + let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { + let Properties { + width, + margin, + scroller_width, + } = *horizontal; // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar // is present - let scrollbar_y_width = self.scrollbar_y.map_or(0.0, |_| { - (scrollbar_width.max(scroller_width) + scrollbar_margin) as f32 + let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| { + (vertical.width.max(vertical.scroller_width) + vertical.margin) + as f32 }); - let total_scrollbar_height = - horizontal.scrollbar_height.max(horizontal.scroller_height) - + 2 * horizontal.scrollbar_margin; + let total_scrollbar_height = width.max(scroller_width) + 2 * margin; // Total bounds of the scrollbar + margin + scroller width let total_scrollbar_bounds = Rectangle { @@ -1106,127 +1157,89 @@ impl State { let scrollbar_bounds = Rectangle { x: bounds.x, y: bounds.y + bounds.height - - f32::from( - total_scrollbar_height / 2 - + horizontal.scrollbar_height / 2, - ), + - f32::from(total_scrollbar_height / 2 + width / 2), width: (bounds.width - scrollbar_y_width).max(0.0), - height: horizontal.scrollbar_height as f32, + height: width as f32, }; let ratio = bounds.width / content_bounds.width; // min width for easier grabbing with extra wide content - let scroller_width = (bounds.width * ratio).max(2.0); - let scroller_offset = offset_x as f32 * ratio; + let scroller_length = (bounds.width * ratio).max(2.0); + let scroller_offset = offset.x * ratio; let scroller_bounds = Rectangle { x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) .max(0.0), y: bounds.y + bounds.height - f32::from( - total_scrollbar_height / 2 - + horizontal.scroller_height / 2, + total_scrollbar_height / 2 + scroller_width / 2, ), - width: scroller_width, - height: horizontal.scroller_height as f32, + width: scroller_length, + height: scroller_width as f32, }; Some(Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - direction: Direction::Horizontal, scroller: Scroller { bounds: scroller_bounds, - grabbed_at: scroller_grabbed, }, - offset: Offset::Absolute(offset_x), }) } else { None }; + + Self { + y: y_scrollbar, + x: x_scrollbar, + } } - /// Returns whether the mouse is within the bounds of each scrollbar. - fn mouse_over_scrollbars(&self, cursor_position: Point) -> (bool, bool) { + fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { ( - self.scrollbar_x.map_or(false, |scrollbar| { - scrollbar.is_mouse_over(cursor_position) - }), - self.scrollbar_y.map_or(false, |scrollbar| { - scrollbar.is_mouse_over(cursor_position) - }), + self.y + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false), + self.x + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false), ) } - /// Returns whether the scroller for either scrollbar is currently grabbed. - fn scrollers_grabbed(&self) -> bool { - self.scrollbar_x - .map_or(false, |scrollbar| scrollbar.scroller.grabbed_at.is_some()) - || self.scrollbar_y.map_or(false, |scrollbar| { - scrollbar.scroller.grabbed_at.is_some() - }) - } - - /// Apply a scrolling offset to the current [`State`], given the bounds of - /// the [`Scrollable`] and its contents. - pub fn scroll( - &mut self, - delta: Vector, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - if delta.x != 0.0 && bounds.width < content_bounds.width { - if let Some(scrollbar) = &mut self.scrollbar_x { - scrollbar.offset = Offset::Absolute( - (scrollbar.offset.absolute( - Direction::Horizontal, - bounds, - content_bounds, - ) - delta.x) - .max(0.0) - .min((content_bounds.width - bounds.width) as f32), - ); + fn grab_y_scroller(&self, cursor_position: Point) -> Option { + self.y.and_then(|scrollbar| { + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scrollbar.scroller.bounds.contains(cursor_position) { + (cursor_position.y - scrollbar.scroller.bounds.y) + / scrollbar.scroller.bounds.height + } else { + 0.5 + }) + } else { + None } - } + }) + } - if delta.y != 0.0 && bounds.height < content_bounds.height { - if let Some(scrollbar) = &mut self.scrollbar_y { - scrollbar.offset = Offset::Absolute( - (scrollbar.offset.absolute( - Direction::Vertical, - bounds, - content_bounds, - ) - delta.y) - .max(0.0) - .min((content_bounds.height - bounds.height) as f32), - ) + fn grab_x_scroller(&self, cursor_position: Point) -> Option { + self.x.and_then(|scrollbar| { + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scrollbar.scroller.bounds.contains(cursor_position) { + (cursor_position.x - scrollbar.scroller.bounds.x) + / scrollbar.scroller.bounds.width + } else { + 0.5 + }) + } else { + None } - } + }) } - /// Returns the current x & y scrolling offset of the [`State`], given the bounds - /// of the [`Scrollable`] and its contents. - pub fn offset( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> (f32, f32) { - ( - self.scrollbar_x.map_or(0.0, |scrollbar| { - scrollbar.offset.absolute( - Direction::Horizontal, - bounds, - content_bounds, - ) - }), - self.scrollbar_y.map_or(0.0, |scrollbar| { - scrollbar.offset.absolute( - Direction::Vertical, - bounds, - content_bounds, - ) - }), - ) + fn active(&self) -> bool { + self.y.is_some() || self.x.is_some() } } @@ -1240,126 +1253,47 @@ struct Scrollbar { /// The bounds of just the [`Scrollbar`]. bounds: Rectangle, - /// The direction of the [`Scrollbar`]. - direction: Direction, - /// The state of this scrollbar's [`Scroller`]. scroller: Scroller, - - /// The current offset of the [`Scrollbar`]. - offset: Offset, } impl Scrollbar { - /// Snaps the scroll position to a relative amount. - /// - /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at - /// the end. - pub fn snap_to(&mut self, percentage: f32) { - self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); - } - - /// Unsnaps the current scroll position if snapped, given the bounds of the [`Scrollable`] - /// and its contents. - pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { - self.offset = Offset::Absolute(self.offset.absolute( - self.direction, - bounds, - content_bounds, - )); - } - - /// Scrolls the [`Scrollbar`] to a certain percentage. - fn scroll_to( - &mut self, - percentage: f32, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - self.snap_to(percentage); - self.unsnap(bounds, content_bounds); - } - /// Returns whether the mouse is over the scrollbar or not. fn is_mouse_over(&self, cursor_position: Point) -> bool { self.total_bounds.contains(cursor_position) } - fn grab_scroller(&self, cursor_position: Point) -> Option { - if self.total_bounds.contains(cursor_position) { - Some(if self.scroller.bounds.contains(cursor_position) { - match self.direction { - Direction::Vertical => { - (cursor_position.y - self.scroller.bounds.y) - / self.scroller.bounds.height - } - Direction::Horizontal => { - (cursor_position.x - self.scroller.bounds.x) - / self.scroller.bounds.width - } - } - } else { - 0.5 - }) - } else { - None - } - } - - fn scroll_percentage( + /// Returns the y-axis scrolled percentage from the cursor position. + fn scroll_percentage_y( &self, grabbed_at: f32, cursor_position: Point, ) -> f32 { - match self.direction { - Direction::Vertical => { - (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height) - } - Direction::Horizontal => { - (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width) - } + if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + // cursor position is unavailable! Set to either end or beginning of scrollbar depending + // on where the thumb currently is in the track + (self.scroller.bounds.y / self.total_bounds.height).round() + } else { + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) } } -} -/// The directional offset of a [`Scrollable`]. -#[derive(Debug, Clone, Copy)] -enum Offset { - Absolute(f32), - Relative(f32), -} - -impl Offset { - fn absolute( - self, - direction: Direction, - bounds: Rectangle, - content_bounds: Rectangle, + /// Returns the x-axis scrolled percentage from the cursor position. + fn scroll_percentage_x( + &self, + grabbed_at: f32, + cursor_position: Point, ) -> f32 { - match self { - Self::Absolute(absolute) => match direction { - Direction::Horizontal => { - absolute.min((content_bounds.width - bounds.width).max(0.0)) - } - Direction::Vertical => absolute - .min((content_bounds.height - bounds.height).max(0.0)), - }, - Self::Relative(percentage) => match direction { - Direction::Horizontal => { - ((content_bounds.width - bounds.width) * percentage) - .max(0.0) - } - Direction::Vertical => { - ((content_bounds.height - bounds.height) * percentage) - .max(0.0) - } - }, + if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + (self.scroller.bounds.x / self.total_bounds.width).round() + } else { + (cursor_position.x + - self.bounds.x + - self.scroller.bounds.width * grabbed_at) + / (self.bounds.width - self.scroller.bounds.width) } } } @@ -1369,7 +1303,4 @@ impl Offset { struct Scroller { /// The bounds of the [`Scroller`]. bounds: Rectangle, - - /// Whether or not the scroller is currently grabbed. - grabbed_at: Option, } -- cgit From 624a4ada7981eb05c0b50cafa7e9545ad8347cb5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 8 Jan 2023 20:07:11 +0100 Subject: Introduce `RelativeOffset` type in `scrollable` --- native/src/widget/operation/scrollable.rs | 30 ++++++++--- native/src/widget/scrollable.rs | 85 +++++++++++++++---------------- 2 files changed, 64 insertions(+), 51 deletions(-) (limited to 'native/src/widget') diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs index 6981b1f4..3b20631f 100644 --- a/native/src/widget/operation/scrollable.rs +++ b/native/src/widget/operation/scrollable.rs @@ -1,19 +1,18 @@ //! Operate on widgets that can be scrolled. use crate::widget::{Id, Operation}; -use iced_core::Point; /// The internal state of a widget that can be scrolled. pub trait Scrollable { /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. - fn snap_to(&mut self, percentage: Point); + fn snap_to(&mut self, offset: RelativeOffset); } /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to /// the provided `percentage`. -pub fn snap_to(target: Id, percentage: Point) -> impl Operation { +pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { struct SnapTo { target: Id, - percentage: Point, + offset: RelativeOffset, } impl Operation for SnapTo { @@ -27,10 +26,29 @@ pub fn snap_to(target: Id, percentage: Point) -> impl Operation { fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { if Some(&self.target) == id { - state.snap_to(self.percentage); + state.snap_to(self.offset); } } } - SnapTo { target, percentage } + SnapTo { target, offset } +} + +/// The amount of offset in each direction of a [`Scrollable`]. +/// +/// A value of `0.0` means start, while `1.0` means end. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct RelativeOffset { + /// The amount of horizontal offset + pub x: f32, + /// The amount of vertical offset + pub y: f32, +} + +impl RelativeOffset { + /// A relative offset that points to the top-left of a [`Scrollable`]. + pub const START: Self = Self { x: 0.0, y: 0.0 }; + + /// A relative offset that points to the bottom-right of a [`Scrollable`]. + pub const END: Self = Self { x: 1.0, y: 1.0 }; } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 39f970d7..63e7312f 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -14,6 +14,7 @@ use crate::{ }; pub use iced_style::scrollable::StyleSheet; +pub use operation::scrollable::RelativeOffset; pub mod style { //! The styles of a [`Scrollable`]. @@ -35,7 +36,7 @@ where vertical: Properties, horizontal: Option, content: Element<'a, Message, Renderer>, - on_scroll: Option Message + 'a>>, + on_scroll: Option Message + 'a>>, style: ::Style, } @@ -85,7 +86,10 @@ where /// /// The function takes the new relative x & y offset of the [`Scrollable`] /// (e.g. `0` means beginning, while `1` means end). - pub fn on_scroll(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + pub fn on_scroll( + mut self, + f: impl Fn(RelativeOffset) -> Message + 'a, + ) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -375,9 +379,9 @@ impl From for widget::Id { /// to the provided `percentage` along the x & y axis. pub fn snap_to( id: Id, - percentage: Point, + offset: RelativeOffset, ) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, percentage)) + Command::widget(operation::scrollable::snap_to(id.0, offset)) } /// Computes the layout of a [`Scrollable`]. @@ -428,7 +432,7 @@ pub fn update( shell: &mut Shell<'_, Message>, vertical: &Properties, horizontal: Option<&Properties>, - on_scroll: &Option Message + '_>>, + on_scroll: &Option Message + '_>>, update_content: impl FnOnce( Event, Layout<'_>, @@ -872,7 +876,7 @@ pub fn draw( fn notify_on_scroll( state: &State, - on_scroll: &Option Message + '_>>, + on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, @@ -882,13 +886,15 @@ fn notify_on_scroll( return; } - let x_offset = state.offset_x.absolute_x(bounds, content_bounds) + let x = state.offset_x.absolute(bounds.width, content_bounds.width) / (content_bounds.width - bounds.width); - let y_offset = state.offset_y.absolute_y(bounds, content_bounds) + let y = state + .offset_y + .absolute(bounds.height, content_bounds.height) / (content_bounds.height - bounds.height); - shell.publish(on_scroll(Point::new(x_offset, y_offset))) + shell.publish(on_scroll(RelativeOffset { x, y })) } } @@ -915,12 +921,11 @@ impl Default for State { } impl operation::Scrollable for State { - fn snap_to(&mut self, percentage: Point) { - State::snap_to(self, percentage); + fn snap_to(&mut self, offset: RelativeOffset) { + State::snap_to(self, offset); } } -/// The offset of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { Absolute(f32), @@ -928,24 +933,13 @@ enum Offset { } impl Offset { - fn absolute_x(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { - match self { - Offset::Absolute(absolute) => { - absolute.min((content_bounds.width - bounds.width).max(0.0)) - } - Offset::Relative(percentage) => { - ((content_bounds.width - bounds.width) * percentage).max(0.0) - } - } - } - - fn absolute_y(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + fn absolute(self, window: f32, content: f32) -> f32 { match self { Offset::Absolute(absolute) => { - absolute.min((content_bounds.height - bounds.height).max(0.0)) + absolute.min((content - window).max(0.0)) } Offset::Relative(percentage) => { - ((content_bounds.height - bounds.height) * percentage).max(0.0) + ((content - window) * percentage).max(0.0) } } } @@ -967,14 +961,16 @@ impl State { ) { if bounds.height < content_bounds.height { self.offset_y = Offset::Absolute( - (self.offset_y.absolute_y(bounds, content_bounds) - delta.y) + (self.offset_y.absolute(bounds.height, content_bounds.height) + - delta.y) .clamp(0.0, content_bounds.height - bounds.height), ) } if bounds.width < content_bounds.width { self.offset_x = Offset::Absolute( - (self.offset_x.absolute_x(bounds, content_bounds) - delta.x) + (self.offset_x.absolute(bounds.width, content_bounds.width) + - delta.x) .clamp(0.0, content_bounds.width - bounds.width), ); } @@ -1008,34 +1004,33 @@ impl State { self.unsnap(bounds, content_bounds); } - /// Snaps the scroll position to a relative amount. - /// - /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at - /// the end. - pub fn snap_to(&mut self, percentage: Point) { - self.offset_x = Offset::Relative(percentage.x.clamp(0.0, 1.0)); - self.offset_y = Offset::Relative(percentage.y.clamp(0.0, 1.0)); + /// Snaps the scroll position to a [`RelativeOffset`]. + pub fn snap_to(&mut self, offset: RelativeOffset) { + self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0)); + self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0)); } /// Unsnaps the current scroll position, if snapped, given the bounds of the /// [`Scrollable`] and its contents. pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { - self.offset_x = - Offset::Absolute(self.offset_x.absolute_x(bounds, content_bounds)); - self.offset_y = - Offset::Absolute(self.offset_y.absolute_y(bounds, content_bounds)); + self.offset_x = Offset::Absolute( + self.offset_x.absolute(bounds.width, content_bounds.width), + ); + self.offset_y = Offset::Absolute( + self.offset_y.absolute(bounds.height, content_bounds.height), + ); } - /// Returns the current x & y scrolling offset of the [`State`], given the bounds - /// of the [`Scrollable`] and its contents. + /// Returns the scrolling offset of the [`State`], given the bounds of the + /// [`Scrollable`] and its contents. pub fn offset( &self, bounds: Rectangle, content_bounds: Rectangle, - ) -> Point { - Point::new( - self.offset_x.absolute_x(bounds, content_bounds), - self.offset_y.absolute_y(bounds, content_bounds), + ) -> Vector { + Vector::new( + self.offset_x.absolute(bounds.width, content_bounds.width), + self.offset_y.absolute(bounds.height, content_bounds.height), ) } -- cgit From 19f4373863beb4a0d307dcd6f9632c1736b2355a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 8 Jan 2023 20:17:11 +0100 Subject: Enable horizontal scrolling with `Shift+MouseWheel` --- native/src/widget/scrollable.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'native/src/widget') diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 63e7312f..efd66d1b 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,5 +1,6 @@ //! Navigate an endless amount of content with a scrollbar. use crate::event::{self, Event}; +use crate::keyboard; use crate::layout; use crate::mouse; use crate::overlay; @@ -479,13 +480,26 @@ pub fn update( return event::Status::Captured; } + if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event + { + state.keyboard_modifiers = modifiers; + + return event::Status::Ignored; + } + if mouse_over_scrollable { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { // TODO: Configurable speed/friction (?) - Vector::new(x * 60.0, y * 60.0) + let movement = if state.keyboard_modifiers.shift() { + Vector::new(y, x) + } else { + Vector::new(x, y) + }; + + movement * 60.0 } mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; @@ -906,6 +920,7 @@ pub struct State { y_scroller_grabbed_at: Option, offset_x: Offset, x_scroller_grabbed_at: Option, + keyboard_modifiers: keyboard::Modifiers, } impl Default for State { @@ -916,6 +931,7 @@ impl Default for State { y_scroller_grabbed_at: None, offset_x: Offset::Absolute(0.0), x_scroller_grabbed_at: None, + keyboard_modifiers: keyboard::Modifiers::default(), } } } -- cgit From f64e95e2469bd0f8012742a585c41c770634030e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 8 Jan 2023 20:19:36 +0100 Subject: Remove `PartialOrd` implementation for `Rectangle` A `PartialOrd` implementation is unclear for this type, since it has a position besides its dimensions. --- native/src/widget/scrollable.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'native/src/widget') diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index efd66d1b..46b757b9 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -896,7 +896,9 @@ fn notify_on_scroll( shell: &mut Shell<'_, Message>, ) { if let Some(on_scroll) = on_scroll { - if content_bounds <= bounds { + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { return; } -- cgit From 2d007474dd002dfc936f75f7379f1bbae72c5ad4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 8 Jan 2023 20:27:15 +0100 Subject: Avoid dragging scroll area when touching scrollbars for `Scrollable` --- native/src/widget/scrollable.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'native/src/widget') diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 46b757b9..82286036 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -516,7 +516,10 @@ pub fn update( return event::Status::Captured; } - Event::Touch(event) => { + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => + { match event { touch::Event::FingerPressed { .. } => { state.scroll_area_touched_at = Some(cursor_position); -- cgit