diff options
Diffstat (limited to 'native/src/widget')
-rw-r--r-- | native/src/widget/column.rs | 2 | ||||
-rw-r--r-- | native/src/widget/operation/scrollable.rs | 41 | ||||
-rw-r--r-- | native/src/widget/scrollable.rs | 961 |
3 files changed, 679 insertions, 325 deletions
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..3b20631f 100644 --- a/native/src/widget/operation/scrollable.rs +++ b/native/src/widget/operation/scrollable.rs @@ -3,25 +3,19 @@ use crate::widget::{Id, Operation}; /// 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); + /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. + 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<T>(target: Id, percentage: f32) -> impl Operation<T> { +pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> { struct SnapTo { target: Id, - percentage: f32, + offset: RelativeOffset, } impl<T> Operation<T> 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,7 +23,32 @@ pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> { ) { operate_on_children(self) } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + 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 20780f89..82286036 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; @@ -13,9 +14,8 @@ use crate::{ Rectangle, Shell, Size, Vector, Widget, }; -use std::{f32, u32}; - pub use iced_style::scrollable::StyleSheet; +pub use operation::scrollable::RelativeOffset; pub mod style { //! The styles of a [`Scrollable`]. @@ -34,11 +34,10 @@ where { id: Option<Id>, height: Length, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + vertical: Properties, + horizontal: Option<Properties>, content: Element<'a, Message, Renderer>, - on_scroll: Option<Box<dyn Fn(f32) -> Message + 'a>>, + on_scroll: Option<Box<dyn Fn(RelativeOffset) -> Message + 'a>>, style: <Renderer::Theme as StyleSheet>::Style, } @@ -52,9 +51,8 @@ where Scrollable { id: None, height: Length::Shrink, - scrollbar_width: 10, - scrollbar_margin: 0, - scroller_width: 10, + vertical: Properties::default(), + horizontal: None, content: content.into(), on_scroll: None, style: Default::default(), @@ -73,32 +71,26 @@ where self } - /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. - pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { - self.scrollbar_width = scrollbar_width.max(1); + /// Configures the vertical scrollbar of the [`Scrollable`] . + pub fn vertical_scroll(mut self, properties: Properties) -> Self { + self.vertical = properties; 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 value of 1. - pub fn scroller_width(mut self, scroller_width: u16) -> Self { - self.scroller_width = scroller_width.max(1); + /// Configures the horizontal scrollbar of the [`Scrollable`] . + pub fn horizontal_scroll(mut self, properties: Properties) -> Self { + self.horizontal = Some(properties); 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(RelativeOffset) -> Message + 'a, + ) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -113,6 +105,51 @@ where } } +/// Properties of a scrollbar within a [`Scrollable`]. +#[derive(Debug)] +pub struct Properties { + width: u16, + margin: u16, + scroller_width: u16, +} + +impl Default for Properties { + fn default() -> Self { + Self { + width: 10, + margin: 0, + scroller_width: 10, + } + } +} + +impl Properties { + /// Creates new [`Properties`] for use in a [`Scrollable`]. + pub fn new() -> Self { + Self::default() + } + + /// 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 scrollbar margin of the [`Scrollable`] . + pub fn margin(mut self, margin: u16) -> Self { + self.margin = margin; + self + } + + /// 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 + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where @@ -153,7 +190,7 @@ where limits, Widget::<Message, Renderer>::width(self), self.height, - u32::MAX, + self.horizontal.is_some(), |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -198,9 +235,8 @@ where cursor_position, clipboard, shell, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, + &self.vertical, + self.horizontal.as_ref(), &self.on_scroll, |event, layout, cursor_position, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -232,9 +268,8 @@ where theme, layout, cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, + &self.vertical, + self.horizontal.as_ref(), &self.style, |renderer, layout, cursor_position, viewport| { self.content.as_widget().draw( @@ -262,9 +297,8 @@ where tree.state.downcast_ref::<State>(), layout, cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, + &self.vertical, + self.horizontal.as_ref(), |layout, cursor_position, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -299,7 +333,7 @@ where .downcast_ref::<State>() .offset(bounds, content_bounds); - overlay.translate(Vector::new(0.0, -(offset as f32))) + overlay.translate(Vector::new(-offset.x, -offset.y)) }) } } @@ -343,9 +377,12 @@ impl From<Id> for widget::Id { } /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage`. -pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> { - Command::widget(operation::scrollable::snap_to(id.0, percentage)) +/// to the provided `percentage` along the x & y axis. +pub fn snap_to<Message: 'static>( + id: Id, + offset: RelativeOffset, +) -> Command<Message> { + Command::widget(operation::scrollable::snap_to(id.0, offset)) } /// Computes the layout of a [`Scrollable`]. @@ -354,14 +391,29 @@ pub fn layout<Renderer>( 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); @@ -379,10 +431,9 @@ pub fn update<Message>( cursor_position: Point, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>, + vertical: &Properties, + horizontal: Option<&Properties>, + on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>, update_content: impl FnOnce( Event, Layout<'_>, @@ -392,36 +443,28 @@ pub fn update<Message>( ) -> 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, - 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 scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); let event_status = { - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y + state.offset(bounds, content_bounds) as f32, - ) + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + cursor_position + state.offset(bounds, content_bounds) } else { // TODO: Make `cursor_position` an `Option<Point>` 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 +480,31 @@ pub fn update<Message>( return event::Status::Captured; } - if is_mouse_over { + 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 }) => { - 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 (?) + let movement = if state.keyboard_modifiers.shift() { + Vector::new(y, x) + } else { + Vector::new(x, y) + }; + + movement * 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, @@ -460,21 +516,27 @@ pub fn update<Message>( 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_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 +549,7 @@ pub fn update<Message>( } touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. } => { - state.scroll_box_touched_at = None; + state.scroll_area_touched_at = None; } } @@ -497,22 +559,20 @@ pub fn update<Message>( } } - if state.is_scroller_grabbed() { + 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.scroller_grabbed_at = None; + state.y_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( - scrollbar.scroll_percentage( + if let Some(scrollbar) = scrollbars.y { + state.scroll_y_to( + scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, ), @@ -533,35 +593,100 @@ pub fn update<Message>( } _ => {} } - } else if is_mouse_over_scrollbar { + } else if mouse_over_y_scrollbar { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } + 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, + ); + + 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(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 { .. }) => { + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } 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; } } _ => {} @@ -576,9 +701,8 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor_position: Point, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + vertical: &Properties, + horizontal: Option<&Properties>, content_interaction: impl FnOnce( Layout<'_>, Point, @@ -586,39 +710,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 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 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 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) + { + cursor_position + offset } 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, + x: bounds.x + offset.x, ..bounds }, ) @@ -632,9 +755,8 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, layout: Layout<'_>, cursor_position: Point, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + vertical: &Properties, + horizontal: Option<&Properties>, style: &<Renderer::Theme as StyleSheet>::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), ) where @@ -644,39 +766,37 @@ pub fn draw<Renderer>( 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 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 offset = 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_x_scrollbar || mouse_over_y_scrollbar) + { + cursor_position + offset } else { - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; - if let Some(scrollbar) = scrollbar { + // Draw inner content + if scrollbars.active() { renderer.with_layer(bounds, |renderer| { renderer.with_translation( - Vector::new(0.0, -(offset as f32)), + Vector::new(-offset.x, -offset.y), |renderer| { draw_content( renderer, content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + y: bounds.y + offset.y, + x: bounds.x + offset.x, ..bounds }, ); @@ -684,25 +804,15 @@ pub fn draw<Renderer>( ); }); - let style = if state.is_scroller_grabbed() { - theme.dragging(style) - } else if is_mouse_over_scrollbar { - theme.hovered(style) - } else { - theme.active(style) - }; - - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0.0; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - if is_scrollbar_visible { + let draw_scrollbar = + |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, @@ -716,8 +826,10 @@ pub fn draw<Renderer>( ); } - if (is_mouse_over || state.is_scroller_grabbed()) - && is_scrollbar_visible + //thumb + if style.scroller.color != Color::TRANSPARENT + || (style.scroller.border_color != Color::TRANSPARENT + && style.scroller.border_width > 0.0) { renderer.fill_quad( renderer::Quad { @@ -729,6 +841,40 @@ pub fn draw<Renderer>( style.scroller.color, ); } + }; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + //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 { @@ -737,110 +883,70 @@ pub fn draw<Renderer>( content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + x: bounds.x + offset.x, + y: bounds.y + offset.y, ..bounds }, ); } } -fn scrollbar( - state: &State, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - bounds: Rectangle, - content_bounds: Rectangle, -) -> Option<Scrollbar> { - 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<Message>( state: &State, - on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>, + on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> 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), - )); + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return; + } + + let x = state.offset_x.absolute(bounds.width, content_bounds.width) + / (content_bounds.width - bounds.width); + + let y = state + .offset_y + .absolute(bounds.height, content_bounds.height) + / (content_bounds.height - bounds.height); + + shell.publish(on_scroll(RelativeOffset { x, y })) } } /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] pub struct State { - scroller_grabbed_at: Option<f32>, - scroll_box_touched_at: Option<Point>, - offset: Offset, + scroll_area_touched_at: Option<Point>, + offset_y: Offset, + y_scroller_grabbed_at: Option<f32>, + offset_x: Offset, + x_scroller_grabbed_at: Option<f32>, + keyboard_modifiers: keyboard::Modifiers, } impl Default for State { fn default() -> Self { Self { - scroller_grabbed_at: None, - scroll_box_touched_at: None, - offset: Offset::Absolute(0.0), + 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, + keyboard_modifiers: keyboard::Modifiers::default(), } } } impl operation::Scrollable for State { - fn snap_to(&mut self, percentage: f32) { - State::snap_to(self, percentage); + fn snap_to(&mut self, offset: RelativeOffset) { + State::snap_to(self, offset); } } -/// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { Absolute(f32), @@ -848,23 +954,20 @@ enum Offset { } impl Offset { - fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + fn absolute(self, window: f32, content: f32) -> f32 { match self { - Self::Absolute(absolute) => { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0); - - absolute.min(hidden_content) + Offset::Absolute(absolute) => { + absolute.min((content - window).max(0.0)) } - Self::Relative(percentage) => { - ((content_bounds.height - bounds.height) * percentage).max(0.0) + Offset::Relative(percentage) => { + ((content - window) * percentage).max(0.0) } } } } impl State { - /// Creates a new [`State`] with the scrollbar located at the top. + /// Creates a new [`State`] with the scrollbar(s) at the beginning. pub fn new() -> Self { State::default() } @@ -873,107 +976,341 @@ impl State { /// the [`Scrollable`] and its contents. pub fn scroll( &mut self, - delta_y: f32, + delta: Vector<f32>, bounds: Rectangle, content_bounds: Rectangle, ) { - if bounds.height >= content_bounds.height { - return; + if bounds.height < content_bounds.height { + self.offset_y = Offset::Absolute( + (self.offset_y.absolute(bounds.height, content_bounds.height) + - delta.y) + .clamp(0.0, content_bounds.height - bounds.height), + ) } - self.offset = Offset::Absolute( - (self.offset.absolute(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(bounds.width, content_bounds.width) + - delta.x) + .clamp(0.0, content_bounds.width - bounds.width), + ); + } } - /// Scrolls the [`Scrollable`] to a relative amount. + /// Scrolls the [`Scrollable`] to a relative amount along the y axis. /// - /// `0` represents scrollbar at the top, while `1` represents scrollbar at - /// the bottom. - pub fn scroll_to( + /// `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.snap_to(percentage); + self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)); self.unsnap(bounds, content_bounds); } - /// Snaps the scroll position to a relative amount. + /// Scrolls the [`Scrollable`] to a relative amount along the x axis. /// - /// `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)); + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_x_to( + &mut self, + 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 [`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 = - Offset::Absolute(self.offset.absolute(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 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 + /// 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, + ) -> Vector { + Vector::new( + self.offset_x.absolute(bounds.width, content_bounds.width), + self.offset_y.absolute(bounds.height, content_bounds.height), + ) } - /// Returns whether the scroller is currently grabbed or not. - pub fn is_scroller_grabbed(&self) -> bool { - self.scroller_grabbed_at.is_some() + /// 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<Scrollbar>, + x: Option<Scrollbar>, +} - /// 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() +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) + } else { + None + } + }); + + let y_scrollbar = if content_bounds.height > bounds.height { + let Properties { + width, + margin, + scroller_width, + } = *vertical; + + // Adjust the height of the vertical scrollbar if the horizontal scrollbar + // is present + 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 = 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 - 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 + width / 2), + y: bounds.y, + 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 * 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 - x_scrollbar_height) + .max(0.0), + width: scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + 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 = y_scrollbar.map_or(0.0, |_| { + (vertical.width.max(vertical.scroller_width) + vertical.margin) + as f32 + }); + + let total_scrollbar_height = width.max(scroller_width) + 2 * 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 + width / 2), + width: (bounds.width - scrollbar_y_width).max(0.0), + height: width as f32, + }; + + let ratio = bounds.width / content_bounds.width; + // min width for easier grabbing with extra wide content + 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 + scroller_width / 2, + ), + width: scroller_length, + height: scroller_width as f32, + }; + + Some(Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + }; + + Self { + y: y_scrollbar, + x: x_scrollbar, + } + } + + fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { + ( + 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), + ) + } + + fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> { + 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 + } + }) + } + + fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> { + 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 + } + }) + } + + fn active(&self) -> bool { + self.y.is_some() || self.x.is_some() } } /// 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 state of this scrollbar's [`Scroller`]. scroller: Scroller, } impl Scrollbar { + /// 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<f32> { - if self.outer_bounds.contains(cursor_position) { - Some(if self.scroller.bounds.contains(cursor_position) { - (cursor_position.y - self.scroller.bounds.y) - / self.scroller.bounds.height - } else { - 0.5 - }) + /// Returns the y-axis scrolled percentage from the cursor position. + fn scroll_percentage_y( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + 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 { - None + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) } } - fn scroll_percentage( + /// Returns the x-axis scrolled percentage from the cursor position. + fn scroll_percentage_x( &self, 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) + 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) + } } } |