From 3a0d34c0240f4421737a6a08761f99d6f8140d02 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Mar 2023 05:37:11 +0100 Subject: Create `iced_widget` subcrate and re-organize the whole codebase --- widget/src/scrollable.rs | 1325 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1325 insertions(+) create mode 100644 widget/src/scrollable.rs (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs new file mode 100644 index 00000000..49c780de --- /dev/null +++ b/widget/src/scrollable.rs @@ -0,0 +1,1325 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget; +use crate::core::widget::operation::{self, Operation}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Vector, Widget, +}; +use crate::native::Command; + +pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; +pub use operation::scrollable::RelativeOffset; + +/// A widget that can vertically display an infinite amount of content with a +/// scrollbar. +#[allow(missing_debug_implementations)] +pub struct Scrollable<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Option, + height: Length, + vertical: Properties, + horizontal: Option, + content: Element<'a, Message, Renderer>, + on_scroll: Option Message + 'a>>, + style: ::Style, +} + +impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into>) -> Self { + Scrollable { + id: None, + height: Length::Shrink, + vertical: Properties::default(), + horizontal: None, + content: content.into(), + on_scroll: None, + style: Default::default(), + } + } + + /// Sets the [`Id`] of the [`Scrollable`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Sets the height of the [`Scrollable`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Configures the vertical scrollbar of the [`Scrollable`] . + pub fn vertical_scroll(mut self, properties: Properties) -> Self { + self.vertical = properties; + self + } + + /// 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 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 + } + + /// Sets the style of the [`Scrollable`] . + pub fn style( + mut self, + style: impl Into<::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// Properties of a scrollbar within a [`Scrollable`]. +#[derive(Debug)] +pub struct Properties { + width: f32, + margin: f32, + scroller_width: f32, +} + +impl Default for Properties { + fn default() -> Self { + Self { + width: 10.0, + margin: 0.0, + scroller_width: 10.0, + } + } +} + +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: impl Into) -> Self { + self.width = width.into().0.max(1.0); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn margin(mut self, margin: impl Into) -> Self { + self.margin = margin.into().0; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + /// Silently enforces a minimum width of 1. + pub fn scroller_width(mut self, scroller_width: impl Into) -> Self { + self.scroller_width = scroller_width.into().0.max(1.0); + self + } +} + +impl<'a, Message, Renderer> Widget + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::core::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() + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::::width(self), + self.height, + self.horizontal.is_some(), + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + let state = tree.state.downcast_mut::(); + + operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); + + operation.container( + self.id.as_ref().map(|id| &id.0), + &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + tree.state.downcast_mut::(), + event, + layout, + cursor_position, + clipboard, + shell, + &self.vertical, + self.horizontal.as_ref(), + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + 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, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + 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], + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = tree + .state + .downcast_ref::() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(-offset.x, -offset.y)) + }) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: Scrollable<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`Scrollable`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +impl From for widget::Id { + fn from(id: Id) -> Self { + id.0 + } +} + +/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided `percentage` along the x & y axis. +pub fn snap_to( + id: Id, + offset: RelativeOffset, +) -> Command { + Command::widget(operation::scrollable::snap_to(id.0, offset)) +} + +/// Computes the layout of a [`Scrollable`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + horizontal_enabled: bool, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .max_height(f32::INFINITY) + .max_width(if horizontal_enabled { + f32::INFINITY + } else { + limits.max().width + }) + .width(width) + .height(height); + + let child_limits = layout::Limits::new( + Size::new(limits.min().width, 0.0), + Size::new( + if horizontal_enabled { + f32::INFINITY + } else { + limits.max().width + }, + f32::MAX, + ), + ); + + let content = layout_content(renderer, &child_limits); + let size = limits.resolve(content.size()); + + layout::Node::with_children(size, vec![content]) +} + +/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] +/// accordingly. +pub fn update( + state: &mut State, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + vertical: &Properties, + horizontal: Option<&Properties>, + on_scroll: &Option Message + '_>>, + update_content: impl FnOnce( + Event, + Layout<'_>, + Point, + &mut dyn Clipboard, + &mut Shell<'_, Message>, + ) -> event::Status, +) -> event::Status { + let bounds = layout.bounds(); + let mouse_over_scrollable = bounds.contains(cursor_position); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + 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 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` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(-1.0, -1.0) + }; + + update_content( + event.clone(), + content, + cursor_position, + clipboard, + shell, + ) + }; + + if let event::Status::Captured = event_status { + 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 (?) + 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), + }; + + state.scroll(delta, bounds, content_bounds); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + 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); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + 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_area_touched_at = + Some(cursor_position); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_area_touched_at = None; + } + } + + return event::Status::Captured; + } + _ => {} + } + } + + 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 { .. }) => { + if let Some(scrollbar) = scrollbars.y { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + 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_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, + ); + + 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; + } + } + _ => {} + } + } + + event::Status::Ignored +} + +/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. +pub fn mouse_interaction( + state: &State, + layout: Layout<'_>, + cursor_position: Point, + vertical: &Properties, + horizontal: Option<&Properties>, + content_interaction: impl FnOnce( + Layout<'_>, + Point, + &Rectangle, + ) -> 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 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 = state.offset(bounds, content_bounds); + + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + cursor_position + offset + } else { + Point::new(-1.0, -1.0) + }; + + content_interaction( + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset.y, + x: bounds.x + offset.x, + ..bounds + }, + ) + } +} + +/// Draws a [`Scrollable`]. +pub fn draw( + state: &State, + renderer: &mut Renderer, + 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 + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.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 offset = state.offset(bounds, content_bounds); + + let cursor_position = if mouse_over_scrollable + && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) + { + cursor_position + offset + } else { + Point::new(-1.0, -1.0) + }; + + // Draw inner content + if scrollbars.active() { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(-offset.x, -offset.y), + |renderer| { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset.y, + x: bounds.x + offset.x, + ..bounds + }, + ); + }, + ); + }); + + let draw_scrollbar = + |renderer: &mut Renderer, + style: Scrollbar, + scrollbar: &internals::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.into(), + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + 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 { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + x: bounds.x + offset.x, + y: bounds.y + offset.y, + ..bounds + }, + ); + } +} + +fn notify_on_scroll( + state: &State, + on_scroll: &Option Message + '_>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) { + if let Some(on_scroll) = on_scroll { + 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 { + scroll_area_touched_at: Option, + offset_y: Offset, + y_scroller_grabbed_at: Option, + offset_x: Offset, + x_scroller_grabbed_at: Option, + keyboard_modifiers: keyboard::Modifiers, +} + +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, + keyboard_modifiers: keyboard::Modifiers::default(), + } + } +} + +impl operation::Scrollable for State { + fn snap_to(&mut self, offset: RelativeOffset) { + State::snap_to(self, offset); + } +} + +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, window: f32, content: f32) -> f32 { + match self { + Offset::Absolute(absolute) => { + absolute.min((content - window).max(0.0)) + } + Offset::Relative(percentage) => { + ((content - window) * percentage).max(0.0) + } + } + } +} + +impl State { + /// Creates a new [`State`] with the scrollbar(s) at the beginning. + 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( + &mut self, + delta: Vector, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + 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), + ) + } + + 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 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, + 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_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 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 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) + } 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); + + let total_scrollbar_width = + width.max(scroller_width) + 2.0 * margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width - total_scrollbar_width, + y: bounds.y, + width: total_scrollbar_width, + height: (bounds.height - x_scrollbar_height).max(0.0), + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - total_scrollbar_width / 2.0 + - width / 2.0, + y: bounds.y, + width, + 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 + - total_scrollbar_width / 2.0 + - scroller_width / 2.0, + y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height) + .max(0.0), + width: scroller_width, + height: scroller_height, + }; + + Some(internals::Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: internals::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 + }); + + let total_scrollbar_height = + width.max(scroller_width) + 2.0 * 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, + width: (bounds.width - scrollbar_y_width).max(0.0), + height: total_scrollbar_height, + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height + - total_scrollbar_height / 2.0 + - width / 2.0, + width: (bounds.width - scrollbar_y_width).max(0.0), + height: width, + }; + + 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 + - total_scrollbar_height / 2.0 + - scroller_width / 2.0, + width: scroller_length, + height: scroller_width, + }; + + Some(internals::Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: internals::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 { + 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 { + 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() + } +} + +pub(super) mod internals { + use crate::core::{Point, Rectangle}; + + /// The scrollbar of a [`Scrollable`]. + #[derive(Debug, Copy, Clone)] + pub struct Scrollbar { + /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, + /// and the scrollbar margin. + pub total_bounds: Rectangle, + + /// The bounds of just the [`Scrollbar`]. + pub bounds: Rectangle, + + /// The state of this scrollbar's [`Scroller`]. + pub scroller: Scroller, + } + + impl Scrollbar { + /// Returns whether the mouse is over the scrollbar or not. + pub fn is_mouse_over(&self, cursor_position: Point) -> bool { + self.total_bounds.contains(cursor_position) + } + + /// Returns the y-axis scrolled percentage from the cursor position. + pub 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 { + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) + } + } + + /// Returns the x-axis scrolled percentage from the cursor position. + pub fn scroll_percentage_x( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + 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) + } + } + } + + /// The handle of a [`Scrollbar`]. + #[derive(Debug, Clone, Copy)] + pub struct Scroller { + /// The bounds of the [`Scroller`]. + pub bounds: Rectangle, + } +} -- cgit From 99e0a71504456976ba88040f5d1d3bbc347694ea Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Mar 2023 06:35:20 +0100 Subject: Rename `iced_native` to `iced_runtime` --- widget/src/scrollable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 49c780de..5a7481f7 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -13,7 +13,7 @@ use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; -use crate::native::Command; +use crate::runtime::Command; pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; pub use operation::scrollable::RelativeOffset; -- cgit From 12a57fae5c4a8cf24976c551a64753b5da32ee30 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Wed, 10 May 2023 17:09:29 -0700 Subject: Remove min width 1 from scrollbar & scroller --- widget/src/scrollable.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index fd51a6a8..c3478493 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -128,9 +128,8 @@ impl Properties { } /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into().0.max(1.0); + self.width = width.into().0.max(0.0); self } @@ -141,9 +140,8 @@ impl Properties { } /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. pub fn scroller_width(mut self, scroller_width: impl Into) -> Self { - self.scroller_width = scroller_width.into().0.max(1.0); + self.scroller_width = scroller_width.into().0.max(0.0); self } } -- cgit From 29326952b420c22c34a5a1315e8f8a1ce77311ef Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 23 May 2023 04:39:41 +0200 Subject: Avoid drawing empty quads in `widget::scrollable` --- widget/src/scrollable.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c3478493..af5424c0 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -809,9 +809,11 @@ pub fn draw( style: Scrollbar, scrollbar: &internals::Scrollbar| { //track - if style.background.is_some() - || (style.border_color != Color::TRANSPARENT - && style.border_width > 0.0) + if scrollbar.bounds.width > 0.0 + && scrollbar.bounds.height > 0.0 + && (style.background.is_some() + || (style.border_color != Color::TRANSPARENT + && style.border_width > 0.0)) { renderer.fill_quad( renderer::Quad { @@ -827,9 +829,11 @@ pub fn draw( } //thumb - if style.scroller.color != Color::TRANSPARENT - || (style.scroller.border_color != Color::TRANSPARENT - && style.scroller.border_width > 0.0) + if scrollbar.scroller.bounds.width > 0.0 + && scrollbar.scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border_color != Color::TRANSPARENT + && style.scroller.border_width > 0.0)) { renderer.fill_quad( renderer::Quad { -- cgit From 1234d528121265698f9f426ca89fc687dc95dc01 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Tue, 23 May 2023 15:28:45 +0200 Subject: clippy --- widget/src/scrollable.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index af5424c0..12e544c5 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -818,7 +818,7 @@ pub fn draw( renderer.fill_quad( renderer::Quad { bounds: scrollbar.bounds, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, border_width: style.border_width, border_color: style.border_color, }, @@ -838,7 +838,7 @@ pub fn draw( renderer.fill_quad( renderer::Quad { bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius.into(), + border_radius: style.scroller.border_radius, border_width: style.scroller.border_width, border_color: style.scroller.border_color, }, -- cgit From 34451bff185d8875f55747ee97ed746828e30f40 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 8 Jun 2023 20:11:59 +0200 Subject: Implement basic cursor availability --- widget/src/scrollable.rs | 256 +++++++++++++++++++++++------------------------ 1 file changed, 125 insertions(+), 131 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 12e544c5..e5cda4df 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -222,7 +222,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -231,18 +231,18 @@ where tree.state.downcast_mut::(), event, layout, - cursor_position, + cursor, clipboard, shell, &self.vertical, self.horizontal.as_ref(), &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { + |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -258,7 +258,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( @@ -266,18 +266,18 @@ where renderer, theme, layout, - cursor_position, + cursor, &self.vertical, self.horizontal.as_ref(), &self.style, - |renderer, layout, cursor_position, viewport| { + |renderer, layout, cursor, viewport| { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, - cursor_position, + cursor, viewport, ) }, @@ -288,21 +288,21 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { mouse_interaction( tree.state.downcast_ref::(), layout, - cursor_position, + cursor, &self.vertical, self.horizontal.as_ref(), - |layout, cursor_position, viewport| { + |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -428,7 +428,7 @@ pub fn update( state: &mut State, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, vertical: &Properties, @@ -437,13 +437,13 @@ pub fn update( update_content: impl FnOnce( Event, Layout<'_>, - Point, + mouse::Cursor, &mut dyn Clipboard, &mut Shell<'_, Message>, ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(&bounds); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); @@ -452,28 +452,21 @@ pub fn update( Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); let event_status = { - 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` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + state.offset(bounds, content_bounds), + ) + } + _ => mouse::Cursor::Unavailable, }; - update_content( - event.clone(), - content, - cursor_position, - clipboard, - shell, - ) + update_content(event.clone(), content, cursor, clipboard, shell) }; if let event::Status::Captured = event_status { @@ -487,76 +480,71 @@ pub fn update( 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 (?) - 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), - }; + let Some(cursor_position) = cursor_over_scrollable else { + return event::Status::Ignored + }; + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + 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) + }; - state.scroll(delta, bounds, content_bounds); + movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); + state.scroll(delta, bounds, content_bounds); + + notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); + + return event::Status::Captured; + } + 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); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + 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); - return event::Status::Captured; - } - 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); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - 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_area_touched_at = - Some(cursor_position); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - state.scroll_area_touched_at = None; + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } } - - return event::Status::Captured; + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_area_touched_at = None; + } } - _ => {} + + return event::Status::Captured; } + _ => {} } if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { @@ -700,17 +688,17 @@ pub fn update( pub fn mouse_interaction( state: &State, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, vertical: &Properties, horizontal: Option<&Properties>, content_interaction: impl FnOnce( Layout<'_>, - Point, + mouse::Cursor, &Rectangle, ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(&bounds); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); @@ -719,7 +707,7 @@ pub fn mouse_interaction( Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() @@ -728,17 +716,18 @@ pub fn mouse_interaction( } else { let offset = state.offset(bounds, content_bounds); - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + offset) + } + _ => mouse::Cursor::Unavailable, }; content_interaction( content_layout, - cursor_position, + cursor, &Rectangle { y: bounds.y + offset.y, x: bounds.x + offset.x, @@ -754,11 +743,11 @@ pub fn draw( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, vertical: &Properties, horizontal: Option<&Properties>, style: &::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, @@ -770,18 +759,19 @@ pub fn draw( let scrollbars = Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(&bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); let offset = state.offset(bounds, content_bounds); - let cursor_position = if mouse_over_scrollable - && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + offset) + } + _ => mouse::Cursor::Unavailable, }; // Draw inner content @@ -793,7 +783,7 @@ pub fn draw( draw_content( renderer, content_layout, - cursor_position, + cursor, &Rectangle { y: bounds.y + offset.y, x: bounds.x + offset.x, @@ -858,7 +848,7 @@ pub fn draw( if let Some(scrollbar) = scrollbars.y { let style = if state.y_scroller_grabbed_at.is_some() { theme.dragging(style) - } else if mouse_over_scrollable { + } else if cursor_over_scrollable.is_some() { theme.hovered(style, mouse_over_y_scrollbar) } else { theme.active(style) @@ -871,7 +861,7 @@ pub fn draw( if let Some(scrollbar) = scrollbars.x { let style = if state.x_scroller_grabbed_at.is_some() { theme.dragging_horizontal(style) - } else if mouse_over_scrollable { + } else if cursor_over_scrollable.is_some() { theme.hovered_horizontal(style, mouse_over_x_scrollbar) } else { theme.active_horizontal(style) @@ -885,7 +875,7 @@ pub fn draw( draw_content( renderer, content_layout, - cursor_position, + cursor, &Rectangle { x: bounds.x + offset.x, y: bounds.y + offset.y, @@ -1283,17 +1273,21 @@ impl Scrollbars { } } - 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 is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) { + if let Some(cursor_position) = 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), + ) + } else { + (false, false) + } } fn grab_y_scroller(&self, cursor_position: Point) -> Option { -- cgit From 5c8cfb411ed0a9a6e55bd1193cd7e97252e63d28 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 8 Jun 2023 20:16:46 +0200 Subject: Take `Rectangle` by value in `Cursor` API --- widget/src/scrollable.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e5cda4df..d90aca2b 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -443,7 +443,7 @@ pub fn update( ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(&bounds); + let cursor_over_scrollable = cursor.position_over(bounds); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); @@ -698,7 +698,7 @@ pub fn mouse_interaction( ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(&bounds); + let cursor_over_scrollable = cursor.position_over(bounds); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); @@ -759,7 +759,7 @@ pub fn draw( let scrollbars = Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let cursor_over_scrollable = cursor.position_over(&bounds); + let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); -- cgit From 27639c4ce6161fa07986c2f1d472a8a259ae2129 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 8 Jun 2023 21:03:24 +0200 Subject: Fix mouse interactions in `Scrollable` --- widget/src/scrollable.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index d90aca2b..5c00ee20 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -480,10 +480,6 @@ pub fn update( return event::Status::Ignored; } - let Some(cursor_position) = cursor_over_scrollable else { - return event::Status::Ignored - }; - match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { let delta = match delta { @@ -512,12 +508,20 @@ pub fn update( { match event { touch::Event::FingerPressed { .. } => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + state.scroll_area_touched_at = Some(cursor_position); } touch::Event::FingerMoved { .. } => { if let Some(scroll_box_touched_at) = state.scroll_area_touched_at { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + let delta = Vector::new( cursor_position.x - scroll_box_touched_at.x, cursor_position.y - scroll_box_touched_at.y, @@ -559,6 +563,10 @@ pub fn update( Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + state.scroll_y_to( scrollbar.scroll_percentage_y( scroller_grabbed_at, @@ -585,6 +593,10 @@ pub fn update( match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + if let (Some(scroller_grabbed_at), Some(scrollbar)) = (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) { @@ -625,6 +637,10 @@ pub fn update( } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + if let Some(scrollbar) = scrollbars.x { state.scroll_x_to( scrollbar.scroll_percentage_x( @@ -652,6 +668,10 @@ pub fn update( match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + if let (Some(scroller_grabbed_at), Some(scrollbar)) = (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) { -- cgit From 38da9535831d4371cac0bf21282d4c1a6a82c209 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Mon, 12 Jun 2023 13:15:11 -0700 Subject: Only scroll w/ wheel if over scrollable --- widget/src/scrollable.rs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5c00ee20..010befac 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -482,6 +482,10 @@ pub fn update( match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return event::Status::Ignored; + } + let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { // TODO: Configurable speed/friction (?) -- cgit From fa04f40524ab7a4ee23bcbc09bc4960c05b192db Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Fri, 26 May 2023 20:27:17 -0400 Subject: Make vertical scroll properties optional Co-Authored-By: Austin M. Reppert --- widget/src/scrollable.rs | 130 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 40 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 010befac..1a326848 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -29,8 +29,7 @@ where id: Option, width: Length, height: Length, - vertical: Properties, - horizontal: Option, + scrollbar_properties: ScrollbarProperties, content: Element<'a, Message, Renderer>, on_scroll: Option Message + 'a>>, style: ::Style, @@ -47,8 +46,7 @@ where id: None, width: Length::Shrink, height: Length::Shrink, - vertical: Properties::default(), - horizontal: None, + scrollbar_properties: Default::default(), content: content.into(), on_scroll: None, style: Default::default(), @@ -73,15 +71,12 @@ where self } - /// Configures the vertical scrollbar of the [`Scrollable`] . - pub fn vertical_scroll(mut self, properties: Properties) -> Self { - self.vertical = properties; - self - } - - /// Configures the horizontal scrollbar of the [`Scrollable`] . - pub fn horizontal_scroll(mut self, properties: Properties) -> Self { - self.horizontal = Some(properties); + /// Configures the scrollbar(s) of the [`Scrollable`] . + pub fn scrollbar_properties( + mut self, + scrollbar_properties: ScrollbarProperties, + ) -> Self { + self.scrollbar_properties = scrollbar_properties; self } @@ -103,6 +98,43 @@ where } } +/// Properties of the scrollbar(s) within a [`Scrollable`]. +#[derive(Debug)] +pub enum ScrollbarProperties { + /// Vertical Scrollbar. + Vertical(Properties), + /// Horizontal Scrollbar. + Horizontal(Properties), + /// Both Vertical and Horizontal Scrollbars. + Both(Properties, Properties), +} + +impl ScrollbarProperties { + /// Returns the horizontal [`Properties`] of the [`ScrollbarProperties`]. + pub fn horizontal(&self) -> Option<&Properties> { + match self { + Self::Horizontal(properties) => Some(properties), + Self::Both(_, properties) => Some(properties), + _ => None, + } + } + + /// Returns the vertical [`Properties`] of the [`ScrollbarProperties`]. + pub fn vertical(&self) -> Option<&Properties> { + match self { + Self::Vertical(properties) => Some(properties), + Self::Both(properties, _) => Some(properties), + _ => None, + } + } +} + +impl Default for ScrollbarProperties { + fn default() -> Self { + Self::Vertical(Properties::default()) + } +} + /// Properties of a scrollbar within a [`Scrollable`]. #[derive(Debug)] pub struct Properties { @@ -186,7 +218,8 @@ where limits, self.width, self.height, - self.horizontal.is_some(), + self.scrollbar_properties.horizontal().is_some(), + self.scrollbar_properties.vertical().is_some(), |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -234,8 +267,7 @@ where cursor, clipboard, shell, - &self.vertical, - self.horizontal.as_ref(), + &self.scrollbar_properties, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -267,8 +299,7 @@ where theme, layout, cursor, - &self.vertical, - self.horizontal.as_ref(), + &self.scrollbar_properties, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -296,8 +327,7 @@ where tree.state.downcast_ref::(), layout, cursor, - &self.vertical, - self.horizontal.as_ref(), + &self.scrollbar_properties, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -400,19 +430,24 @@ pub fn layout( width: Length, height: Length, horizontal_enabled: bool, + vertical_enabled: bool, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), + Size::new(limits.min().width, limits.min().height), Size::new( if horizontal_enabled { f32::INFINITY } else { limits.max().width }, - f32::MAX, + if vertical_enabled { + f32::MAX + } else { + limits.max().height + }, ), ); @@ -431,8 +466,7 @@ pub fn update( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, on_scroll: &Option Message + '_>>, update_content: impl FnOnce( Event, @@ -449,7 +483,7 @@ pub fn update( let content_bounds = content.bounds(); let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + Scrollbars::new(state, &scrollbar_properties, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -531,6 +565,17 @@ pub fn update( cursor_position.y - scroll_box_touched_at.y, ); + let delta = Vector::new( + delta.x + * scrollbar_properties + .horizontal() + .map_or(0.0, |_| 1.0), + delta.y + * scrollbar_properties + .vertical() + .map_or(0.0, |_| 1.0), + ); + state.scroll(delta, bounds, content_bounds); state.scroll_area_touched_at = Some(cursor_position); @@ -713,8 +758,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -728,7 +772,7 @@ pub fn mouse_interaction( let content_bounds = content_layout.bounds(); let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + Scrollbars::new(state, scrollbar_properties, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -768,8 +812,7 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -781,7 +824,7 @@ pub fn draw( let content_bounds = content_layout.bounds(); let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + Scrollbars::new(state, &scrollbar_properties, bounds, content_bounds); let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = @@ -1157,22 +1200,30 @@ 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>, + scrollbar_properties: &ScrollbarProperties, 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) + let show_scrollbar_x = + scrollbar_properties.horizontal().and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); + + let show_scrollbar_y = scrollbar_properties.vertical().and_then(|v| { + if content_bounds.height > bounds.height { + Some(v) } else { None } }); - let y_scrollbar = if content_bounds.height > bounds.height { + let y_scrollbar = if let Some(vertical) = show_scrollbar_y { let Properties { width, margin, @@ -1240,9 +1291,8 @@ impl Scrollbars { // 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 - }); + let scrollbar_y_width = show_scrollbar_y + .map_or(0.0, |v| v.width.max(v.scroller_width) + v.margin); let total_scrollbar_height = width.max(scroller_width) + 2.0 * margin; -- cgit From 493571695a8853ee91309a92d04b8dbea29bab8d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 27 Jun 2023 22:30:54 +0200 Subject: Rename `ScrollbarProperties` to `Direction` in `scrollable` --- widget/src/scrollable.rs | 105 ++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 57 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 1a326848..82f71dff 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -29,7 +29,7 @@ where id: Option, width: Length, height: Length, - scrollbar_properties: ScrollbarProperties, + direction: Direction, content: Element<'a, Message, Renderer>, on_scroll: Option Message + 'a>>, style: ::Style, @@ -46,7 +46,7 @@ where id: None, width: Length::Shrink, height: Length::Shrink, - scrollbar_properties: Default::default(), + direction: Default::default(), content: content.into(), on_scroll: None, style: Default::default(), @@ -71,12 +71,9 @@ where self } - /// Configures the scrollbar(s) of the [`Scrollable`] . - pub fn scrollbar_properties( - mut self, - scrollbar_properties: ScrollbarProperties, - ) -> Self { - self.scrollbar_properties = scrollbar_properties; + /// Sets the [`Direction`] of the [`Scrollable`] . + pub fn direction(mut self, direction: Direction) -> Self { + self.direction = direction; self } @@ -98,45 +95,50 @@ where } } -/// Properties of the scrollbar(s) within a [`Scrollable`]. -#[derive(Debug)] -pub enum ScrollbarProperties { - /// Vertical Scrollbar. +/// The direction of [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Direction { + /// Vertical scrolling Vertical(Properties), - /// Horizontal Scrollbar. + /// Horizontal scrolling Horizontal(Properties), - /// Both Vertical and Horizontal Scrollbars. - Both(Properties, Properties), + /// Both vertical and horizontal scrolling + Both { + /// The properties of the vertical scrollbar. + vertical: Properties, + /// The properties of the horizontal scrollbar. + horizontal: Properties, + }, } -impl ScrollbarProperties { - /// Returns the horizontal [`Properties`] of the [`ScrollbarProperties`]. +impl Direction { + /// Returns the [`Properties`] of the horizontal scrollbar, if any. pub fn horizontal(&self) -> Option<&Properties> { match self { Self::Horizontal(properties) => Some(properties), - Self::Both(_, properties) => Some(properties), + Self::Both { horizontal, .. } => Some(horizontal), _ => None, } } - /// Returns the vertical [`Properties`] of the [`ScrollbarProperties`]. + /// Returns the [`Properties`] of the vertical scrollbar, if any. pub fn vertical(&self) -> Option<&Properties> { match self { Self::Vertical(properties) => Some(properties), - Self::Both(properties, _) => Some(properties), + Self::Both { vertical, .. } => Some(vertical), _ => None, } } } -impl Default for ScrollbarProperties { +impl Default for Direction { fn default() -> Self { Self::Vertical(Properties::default()) } } /// Properties of a scrollbar within a [`Scrollable`]. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Properties { width: f32, margin: f32, @@ -218,8 +220,7 @@ where limits, self.width, self.height, - self.scrollbar_properties.horizontal().is_some(), - self.scrollbar_properties.vertical().is_some(), + &self.direction, |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -267,7 +268,7 @@ where cursor, clipboard, shell, - &self.scrollbar_properties, + &self.direction, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -299,7 +300,7 @@ where theme, layout, cursor, - &self.scrollbar_properties, + &self.direction, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -327,7 +328,7 @@ where tree.state.downcast_ref::(), layout, cursor, - &self.scrollbar_properties, + &self.direction, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -429,8 +430,7 @@ pub fn layout( limits: &layout::Limits, width: Length, height: Length, - horizontal_enabled: bool, - vertical_enabled: bool, + direction: &Direction, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); @@ -438,12 +438,12 @@ pub fn layout( let child_limits = layout::Limits::new( Size::new(limits.min().width, limits.min().height), Size::new( - if horizontal_enabled { + if direction.horizontal().is_some() { f32::INFINITY } else { limits.max().width }, - if vertical_enabled { + if direction.vertical().is_some() { f32::MAX } else { limits.max().height @@ -466,7 +466,7 @@ pub fn update( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, on_scroll: &Option Message + '_>>, update_content: impl FnOnce( Event, @@ -482,8 +482,7 @@ pub fn update( let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let scrollbars = - Scrollbars::new(state, &scrollbar_properties, bounds, content_bounds); + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -567,13 +566,8 @@ pub fn update( let delta = Vector::new( delta.x - * scrollbar_properties - .horizontal() - .map_or(0.0, |_| 1.0), - delta.y - * scrollbar_properties - .vertical() - .map_or(0.0, |_| 1.0), + * direction.horizontal().map_or(0.0, |_| 1.0), + delta.y * direction.vertical().map_or(0.0, |_| 1.0), ); state.scroll(delta, bounds, content_bounds); @@ -758,7 +752,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -771,8 +765,7 @@ pub fn mouse_interaction( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbars = - Scrollbars::new(state, scrollbar_properties, bounds, content_bounds); + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -812,7 +805,7 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -823,8 +816,7 @@ pub fn draw( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbars = - Scrollbars::new(state, &scrollbar_properties, bounds, content_bounds); + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = @@ -1200,22 +1192,21 @@ impl Scrollbars { /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. fn new( state: &State, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Self { let offset = state.offset(bounds, content_bounds); - let show_scrollbar_x = - scrollbar_properties.horizontal().and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); + let show_scrollbar_x = direction.horizontal().and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); - let show_scrollbar_y = scrollbar_properties.vertical().and_then(|v| { + let show_scrollbar_y = direction.vertical().and_then(|v| { if content_bounds.height > bounds.height { Some(v) } else { -- cgit From 412e15b170a61f7d7369122d7d0b089491e1b0ea Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 27 Jun 2023 22:41:16 +0200 Subject: Require a `Direction` when computing `State::offset` in `scrollable` --- widget/src/scrollable.rs | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 82f71dff..5bc6914c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -358,10 +358,11 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = tree - .state - .downcast_ref::() - .offset(bounds, content_bounds); + let offset = tree.state.downcast_ref::().offset( + &self.direction, + bounds, + content_bounds, + ); overlay.translate(Vector::new(-offset.x, -offset.y)) }) @@ -493,7 +494,8 @@ pub fn update( if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => { mouse::Cursor::Available( - cursor_position + state.offset(bounds, content_bounds), + cursor_position + + state.offset(direction, bounds, content_bounds), ) } _ => mouse::Cursor::Unavailable, @@ -564,12 +566,6 @@ pub fn update( cursor_position.y - scroll_box_touched_at.y, ); - let delta = Vector::new( - delta.x - * direction.horizontal().map_or(0.0, |_| 1.0), - delta.y * direction.vertical().map_or(0.0, |_| 1.0), - ); - state.scroll(delta, bounds, content_bounds); state.scroll_area_touched_at = Some(cursor_position); @@ -775,7 +771,7 @@ pub fn mouse_interaction( { mouse::Interaction::Idle } else { - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -822,7 +818,7 @@ pub fn draw( let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -1161,16 +1157,25 @@ impl State { ); } - /// Returns the scrolling offset of the [`State`], given the bounds of the - /// [`Scrollable`] and its contents. + /// Returns the scrolling offset of the [`State`], given a [`Direction`], + /// the bounds of the [`Scrollable`] and its contents. pub fn offset( &self, + direction: &Direction, 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), + if direction.horizontal().is_some() { + self.offset_x.absolute(bounds.width, content_bounds.width) + } else { + 0.0 + }, + if direction.vertical().is_some() { + self.offset_y.absolute(bounds.height, content_bounds.height) + } else { + 0.0 + }, ) } @@ -1196,7 +1201,7 @@ impl Scrollbars { bounds: Rectangle, content_bounds: Rectangle, ) -> Self { - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let show_scrollbar_x = direction.horizontal().and_then(|h| { if content_bounds.width > bounds.width { -- cgit From 1c26440f0bd8f7a002946524dd4d522ba9fb7f29 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 27 Jun 2023 22:46:04 +0200 Subject: Use `Option::filter` instead of `and_then` in `scrollable` --- widget/src/scrollable.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5bc6914c..473124ca 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1203,21 +1203,13 @@ impl Scrollbars { ) -> Self { let offset = state.offset(direction, bounds, content_bounds); - let show_scrollbar_x = direction.horizontal().and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); + let show_scrollbar_x = direction + .horizontal() + .filter(|_| content_bounds.width > bounds.width); - let show_scrollbar_y = direction.vertical().and_then(|v| { - if content_bounds.height > bounds.height { - Some(v) - } else { - None - } - }); + let show_scrollbar_y = direction + .vertical() + .filter(|_| content_bounds.height > bounds.height); let y_scrollbar = if let Some(vertical) = show_scrollbar_y { let Properties { -- cgit From 4f066b516bd7c5a8a3a55f01d09d650e10567839 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Mon, 12 Jun 2023 21:04:43 -0700 Subject: Add scrollable alignment option --- widget/src/scrollable.rs | 109 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 8 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 473124ca..b6111975 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -143,6 +143,7 @@ pub struct Properties { width: f32, margin: f32, scroller_width: f32, + alignment: Alignment, } impl Default for Properties { @@ -151,6 +152,7 @@ impl Default for Properties { width: 10.0, margin: 0.0, scroller_width: 10.0, + alignment: Alignment::Start, } } } @@ -178,6 +180,31 @@ impl Properties { self.scroller_width = scroller_width.into().0.max(0.0); self } + + /// Sets the alignment of the [`Scrollable`] . + pub fn alignment(mut self, alignment: Alignment) -> Self { + self.alignment = alignment; + self + } +} + +/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum Alignment { + /// Content is aligned to the start of the [`Viewport`]. + #[default] + Start, + /// Content is aligned to the end of the [`Viewport`] + End, +} + +impl Alignment { + fn aligned(self, offset: f32, viewport: f32, content: f32) -> f32 { + match self { + Alignment::Start => offset, + Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + } + } } impl<'a, Message, Renderer> Widget @@ -485,6 +512,15 @@ pub fn update( let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); + let horizontal_alignment = direction + .horizontal() + .map(|p| p.alignment) + .unwrap_or_default(); + let vertical_alignment = direction + .vertical() + .map(|p| p.alignment) + .unwrap_or_default(); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -535,7 +571,11 @@ pub fn update( mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; - state.scroll(delta, bounds, content_bounds); + state.scroll( + aligned_delta(delta, vertical_alignment, horizontal_alignment), + bounds, + content_bounds, + ); notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); @@ -566,7 +606,15 @@ pub fn update( cursor_position.y - scroll_box_touched_at.y, ); - state.scroll(delta, bounds, content_bounds); + state.scroll( + aligned_delta( + delta, + vertical_alignment, + horizontal_alignment, + ), + bounds, + content_bounds, + ); state.scroll_area_touched_at = Some(cursor_position); @@ -610,6 +658,7 @@ pub fn update( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, + vertical_alignment, ), bounds, content_bounds, @@ -643,6 +692,7 @@ pub fn update( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, + vertical_alignment, ), bounds, content_bounds, @@ -685,6 +735,7 @@ pub fn update( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, + horizontal_alignment, ), bounds, content_bounds, @@ -718,6 +769,7 @@ pub fn update( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, + horizontal_alignment, ), bounds, content_bounds, @@ -1166,13 +1218,22 @@ impl State { content_bounds: Rectangle, ) -> Vector { Vector::new( - if direction.horizontal().is_some() { - self.offset_x.absolute(bounds.width, content_bounds.width) + if let Some(horizontal) = direction.horizontal() { + horizontal.alignment.aligned( + self.offset_x.absolute(bounds.width, content_bounds.width), + bounds.width, + content_bounds.width, + ) } else { 0.0 }, - if direction.vertical().is_some() { - self.offset_y.absolute(bounds.height, content_bounds.height) + if let Some(vertical) = direction.vertical() { + vertical.alignment.aligned( + self.offset_y + .absolute(bounds.height, content_bounds.height), + bounds.height, + content_bounds.height, + ) } else { 0.0 }, @@ -1216,6 +1277,7 @@ impl Scrollbars { width, margin, scroller_width, + .. } = *vertical; // Adjust the height of the vertical scrollbar if the horizontal scrollbar @@ -1275,6 +1337,7 @@ impl Scrollbars { width, margin, scroller_width, + .. } = *horizontal; // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar @@ -1387,9 +1450,27 @@ impl Scrollbars { } } +fn aligned_delta( + delta: Vector, + vertical_alignment: Alignment, + horizontal_alignment: Alignment, +) -> Vector { + let align = |alignment: Alignment, delta: f32| match alignment { + Alignment::Start => delta, + Alignment::End => -delta, + }; + + Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ) +} + pub(super) mod internals { use crate::core::{Point, Rectangle}; + use super::Alignment; + /// The scrollbar of a [`Scrollable`]. #[derive(Debug, Copy, Clone)] pub struct Scrollbar { @@ -1415,8 +1496,9 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, + alignment: Alignment, ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + let pct = 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() @@ -1425,6 +1507,11 @@ pub(super) mod internals { - self.bounds.y - self.scroller.bounds.height * grabbed_at) / (self.bounds.height - self.scroller.bounds.height) + }; + + match alignment { + Alignment::Start => pct, + Alignment::End => 1.0 - pct, } } @@ -1433,14 +1520,20 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, + alignment: Alignment, ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + let pct = 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) + }; + + match alignment { + Alignment::Start => pct, + Alignment::End => 1.0 - pct, } } } -- cgit From 905c307f0b78d9957a82a95b0ba537be23fa4035 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 13 Jun 2023 08:22:33 -0700 Subject: Make viewport bounds public --- widget/src/scrollable.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index b6111975..79e5fe3e 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1099,8 +1099,10 @@ impl Offset { pub struct Viewport { offset_x: Offset, offset_y: Offset, - bounds: Rectangle, - content_bounds: Rectangle, + /// The viewport bounds of the [`Scrollable`]. + pub bounds: Rectangle, + /// The content bounds of the [`Scrollable`]. + pub content_bounds: Rectangle, } impl Viewport { -- cgit From d79cedd8da99345947f5b9fcb1ebc3a145cc4112 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Sat, 24 Jun 2023 18:01:14 -0700 Subject: Add alignment to Viewport --- widget/src/scrollable.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 79e5fe3e..3912f445 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -577,7 +577,15 @@ pub fn update( content_bounds, ); - notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + horizontal_alignment, + vertical_alignment, + shell, + ); return event::Status::Captured; } @@ -623,6 +631,8 @@ pub fn update( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); } @@ -669,6 +679,8 @@ pub fn update( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); @@ -705,6 +717,8 @@ pub fn update( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); } @@ -746,6 +760,8 @@ pub fn update( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); } @@ -782,6 +798,8 @@ pub fn update( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); @@ -997,6 +1015,8 @@ fn notify_on_scroll( on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, + horizontal_alignment: Alignment, + vertical_alignment: Alignment, shell: &mut Shell<'_, Message>, ) { if let Some(on_scroll) = on_scroll { @@ -1011,6 +1031,8 @@ fn notify_on_scroll( offset_y: state.offset_y, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, }; // Don't publish redundant viewports to shell @@ -1103,6 +1125,10 @@ pub struct Viewport { pub bounds: Rectangle, /// The content bounds of the [`Scrollable`]. pub content_bounds: Rectangle, + /// The horizontal [`Alignment`] of the [`Scrollable`]. + pub horizontal_alignment: Alignment, + /// The vertical [`Alignment`] of the [`Scrollable`]. + pub vertical_alignment: Alignment, } impl Viewport { -- cgit From 44460f7b8b9e90c85bd94c19dd15d418e641f8cd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jul 2023 09:26:13 +0200 Subject: Remove public fields from `Viewport` Let's tackle use cases directly instead! --- widget/src/scrollable.rs | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 3912f445..b6111975 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -577,15 +577,7 @@ pub fn update( content_bounds, ); - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - horizontal_alignment, - vertical_alignment, - shell, - ); + notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); return event::Status::Captured; } @@ -631,8 +623,6 @@ pub fn update( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); } @@ -679,8 +669,6 @@ pub fn update( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); @@ -717,8 +705,6 @@ pub fn update( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); } @@ -760,8 +746,6 @@ pub fn update( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); } @@ -798,8 +782,6 @@ pub fn update( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); @@ -1015,8 +997,6 @@ fn notify_on_scroll( on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, - horizontal_alignment: Alignment, - vertical_alignment: Alignment, shell: &mut Shell<'_, Message>, ) { if let Some(on_scroll) = on_scroll { @@ -1031,8 +1011,6 @@ fn notify_on_scroll( offset_y: state.offset_y, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, }; // Don't publish redundant viewports to shell @@ -1121,14 +1099,8 @@ impl Offset { pub struct Viewport { offset_x: Offset, offset_y: Offset, - /// The viewport bounds of the [`Scrollable`]. - pub bounds: Rectangle, - /// The content bounds of the [`Scrollable`]. - pub content_bounds: Rectangle, - /// The horizontal [`Alignment`] of the [`Scrollable`]. - pub horizontal_alignment: Alignment, - /// The vertical [`Alignment`] of the [`Scrollable`]. - pub vertical_alignment: Alignment, + bounds: Rectangle, + content_bounds: Rectangle, } impl Viewport { -- cgit From 2b2f9c07d8962f8a146794c2fd8f1709f104f4f5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jul 2023 09:49:14 +0200 Subject: Increase type-safety of `alignment` in `scrollable` --- widget/src/scrollable.rs | 100 ++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 63 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index b6111975..17d55e7c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -295,7 +295,7 @@ where cursor, clipboard, shell, - &self.direction, + self.direction, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -327,7 +327,7 @@ where theme, layout, cursor, - &self.direction, + self.direction, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -355,7 +355,7 @@ where tree.state.downcast_ref::(), layout, cursor, - &self.direction, + self.direction, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -386,7 +386,7 @@ where let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = tree.state.downcast_ref::().offset( - &self.direction, + self.direction, bounds, content_bounds, ); @@ -494,7 +494,7 @@ pub fn update( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - direction: &Direction, + direction: Direction, on_scroll: &Option Message + '_>>, update_content: impl FnOnce( Event, @@ -512,15 +512,6 @@ pub fn update( let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - let horizontal_alignment = direction - .horizontal() - .map(|p| p.alignment) - .unwrap_or_default(); - let vertical_alignment = direction - .vertical() - .map(|p| p.alignment) - .unwrap_or_default(); - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -571,11 +562,7 @@ pub fn update( mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; - state.scroll( - aligned_delta(delta, vertical_alignment, horizontal_alignment), - bounds, - content_bounds, - ); + state.scroll(delta, direction, bounds, content_bounds); notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); @@ -606,15 +593,7 @@ pub fn update( cursor_position.y - scroll_box_touched_at.y, ); - state.scroll( - aligned_delta( - delta, - vertical_alignment, - horizontal_alignment, - ), - bounds, - content_bounds, - ); + state.scroll(delta, direction, bounds, content_bounds); state.scroll_area_touched_at = Some(cursor_position); @@ -658,7 +637,6 @@ pub fn update( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, - vertical_alignment, ), bounds, content_bounds, @@ -692,7 +670,6 @@ pub fn update( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, - vertical_alignment, ), bounds, content_bounds, @@ -735,7 +712,6 @@ pub fn update( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, - horizontal_alignment, ), bounds, content_bounds, @@ -769,7 +745,6 @@ pub fn update( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, - horizontal_alignment, ), bounds, content_bounds, @@ -800,7 +775,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - direction: &Direction, + direction: Direction, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -853,7 +828,7 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - direction: &Direction, + direction: Direction, style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -1138,9 +1113,30 @@ impl State { pub fn scroll( &mut self, delta: Vector, + direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) { + let horizontal_alignment = direction + .horizontal() + .map(|p| p.alignment) + .unwrap_or_default(); + + let vertical_alignment = direction + .vertical() + .map(|p| p.alignment) + .unwrap_or_default(); + + let align = |alignment: Alignment, delta: f32| match alignment { + Alignment::Start => delta, + Alignment::End => -delta, + }; + + let delta = Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ); + if bounds.height < content_bounds.height { self.offset_y = Offset::Absolute( (self.offset_y.absolute(bounds.height, content_bounds.height) @@ -1213,7 +1209,7 @@ impl State { /// the bounds of the [`Scrollable`] and its contents. pub fn offset( &self, - direction: &Direction, + direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Vector { @@ -1258,7 +1254,7 @@ impl Scrollbars { /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. fn new( state: &State, - direction: &Direction, + direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Self { @@ -1327,6 +1323,7 @@ impl Scrollbars { scroller: internals::Scroller { bounds: scroller_bounds, }, + alignment: vertical.alignment, }) } else { None @@ -1387,6 +1384,7 @@ impl Scrollbars { scroller: internals::Scroller { bounds: scroller_bounds, }, + alignment: horizontal.alignment, }) } else { None @@ -1450,39 +1448,17 @@ impl Scrollbars { } } -fn aligned_delta( - delta: Vector, - vertical_alignment: Alignment, - horizontal_alignment: Alignment, -) -> Vector { - let align = |alignment: Alignment, delta: f32| match alignment { - Alignment::Start => delta, - Alignment::End => -delta, - }; - - Vector::new( - align(horizontal_alignment, delta.x), - align(vertical_alignment, delta.y), - ) -} - pub(super) mod internals { use crate::core::{Point, Rectangle}; use super::Alignment; - /// The scrollbar of a [`Scrollable`]. #[derive(Debug, Copy, Clone)] pub struct Scrollbar { - /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, - /// and the scrollbar margin. pub total_bounds: Rectangle, - - /// The bounds of just the [`Scrollbar`]. pub bounds: Rectangle, - - /// The state of this scrollbar's [`Scroller`]. pub scroller: Scroller, + pub alignment: Alignment, } impl Scrollbar { @@ -1496,7 +1472,6 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, - alignment: Alignment, ) -> f32 { let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { // cursor position is unavailable! Set to either end or beginning of scrollbar depending @@ -1509,7 +1484,7 @@ pub(super) mod internals { / (self.bounds.height - self.scroller.bounds.height) }; - match alignment { + match self.alignment { Alignment::Start => pct, Alignment::End => 1.0 - pct, } @@ -1520,7 +1495,6 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, - alignment: Alignment, ) -> f32 { let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { (self.scroller.bounds.x / self.total_bounds.width).round() @@ -1531,7 +1505,7 @@ pub(super) mod internals { / (self.bounds.width - self.scroller.bounds.width) }; - match alignment { + match self.alignment { Alignment::Start => pct, Alignment::End => 1.0 - pct, } -- cgit From ca2afb04952f177667c4431a102161be9223340f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jul 2023 10:04:26 +0200 Subject: Disambiguate `offset` from `translation` in `scrollable` --- widget/src/scrollable.rs | 77 +++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 37 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 17d55e7c..45fbd72b 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -198,15 +198,6 @@ pub enum Alignment { End, } -impl Alignment { - fn aligned(self, offset: f32, viewport: f32, content: f32) -> f32 { - match self { - Alignment::Start => offset, - Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), - } - } -} - impl<'a, Message, Renderer> Widget for Scrollable<'a, Message, Renderer> where @@ -385,13 +376,12 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = tree.state.downcast_ref::().offset( - self.direction, - bounds, - content_bounds, - ); + let translation = tree + .state + .downcast_ref::() + .translation(self.direction, bounds, content_bounds); - overlay.translate(Vector::new(-offset.x, -offset.y)) + overlay.translate(Vector::new(-translation.x, -translation.y)) }) } } @@ -522,7 +512,7 @@ pub fn update( { mouse::Cursor::Available( cursor_position - + state.offset(direction, bounds, content_bounds), + + state.translation(direction, bounds, content_bounds), ) } _ => mouse::Cursor::Unavailable, @@ -798,13 +788,13 @@ pub fn mouse_interaction( { mouse::Interaction::Idle } else { - let offset = state.offset(direction, bounds, content_bounds); + let translation = state.translation(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => { - mouse::Cursor::Available(cursor_position + offset) + mouse::Cursor::Available(cursor_position + translation) } _ => mouse::Cursor::Unavailable, }; @@ -813,8 +803,8 @@ pub fn mouse_interaction( content_layout, cursor, &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, + y: bounds.y + translation.y, + x: bounds.x + translation.x, ..bounds }, ) @@ -845,13 +835,13 @@ pub fn draw( let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); - let offset = state.offset(direction, bounds, content_bounds); + let translation = state.translation(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => { - mouse::Cursor::Available(cursor_position + offset) + mouse::Cursor::Available(cursor_position + translation) } _ => mouse::Cursor::Unavailable, }; @@ -860,15 +850,15 @@ pub fn draw( if scrollbars.active() { renderer.with_layer(bounds, |renderer| { renderer.with_translation( - Vector::new(-offset.x, -offset.y), + Vector::new(-translation.x, -translation.y), |renderer| { draw_content( renderer, content_layout, cursor, &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, + y: bounds.y + translation.y, + x: bounds.x + translation.x, ..bounds }, ); @@ -959,8 +949,8 @@ pub fn draw( content_layout, cursor, &Rectangle { - x: bounds.x + offset.x, - y: bounds.y + offset.y, + x: bounds.x + translation.x, + y: bounds.y + translation.y, ..bounds }, ); @@ -1067,6 +1057,20 @@ impl Offset { } } } + + fn absolute_from_start( + self, + viewport: f32, + content: f32, + alignment: Alignment, + ) -> f32 { + let offset = self.absolute(viewport, content); + + match alignment { + Alignment::Start => offset, + Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + } + } } /// The current [`Viewport`] of the [`Scrollable`]. @@ -1205,9 +1209,9 @@ impl State { ); } - /// Returns the scrolling offset of the [`State`], given a [`Direction`], + /// Returns the scrolling translation of the [`State`], given a [`Direction`], /// the bounds of the [`Scrollable`] and its contents. - pub fn offset( + fn translation( &self, direction: Direction, bounds: Rectangle, @@ -1215,20 +1219,19 @@ impl State { ) -> Vector { Vector::new( if let Some(horizontal) = direction.horizontal() { - horizontal.alignment.aligned( - self.offset_x.absolute(bounds.width, content_bounds.width), + self.offset_x.absolute_from_start( bounds.width, content_bounds.width, + horizontal.alignment, ) } else { 0.0 }, if let Some(vertical) = direction.vertical() { - vertical.alignment.aligned( - self.offset_y - .absolute(bounds.height, content_bounds.height), + self.offset_y.absolute_from_start( bounds.height, content_bounds.height, + vertical.alignment, ) } else { 0.0 @@ -1258,7 +1261,7 @@ impl Scrollbars { bounds: Rectangle, content_bounds: Rectangle, ) -> Self { - let offset = state.offset(direction, bounds, content_bounds); + let translation = state.translation(direction, bounds, content_bounds); let show_scrollbar_x = direction .horizontal() @@ -1305,7 +1308,7 @@ impl Scrollbars { 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_offset = translation.y * ratio; let scroller_bounds = Rectangle { x: bounds.x + bounds.width @@ -1366,7 +1369,7 @@ impl Scrollbars { 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_offset = translation.x * ratio; let scroller_bounds = Rectangle { x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) -- cgit From d07bac36ab528ea5c3f4a09bb0ae010ae1b5c6da Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jul 2023 10:05:46 +0200 Subject: Rename `absolute_from_start` to `translation` in `scrollable` --- widget/src/scrollable.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 45fbd72b..9a43a978 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1058,7 +1058,7 @@ impl Offset { } } - fn absolute_from_start( + fn translation( self, viewport: f32, content: f32, @@ -1219,7 +1219,7 @@ impl State { ) -> Vector { Vector::new( if let Some(horizontal) = direction.horizontal() { - self.offset_x.absolute_from_start( + self.offset_x.translation( bounds.width, content_bounds.width, horizontal.alignment, @@ -1228,7 +1228,7 @@ impl State { 0.0 }, if let Some(vertical) = direction.vertical() { - self.offset_y.absolute_from_start( + self.offset_y.translation( bounds.height, content_bounds.height, vertical.alignment, -- cgit From ce23e08d0d921040d1e6fb693149e638d291bd16 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jul 2023 10:13:15 +0200 Subject: Remove unnecessary cursor unavailability logic in `scrollable` --- widget/src/scrollable.rs | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) (limited to 'widget/src/scrollable.rs') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 9a43a978..88746ac4 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1476,20 +1476,14 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let pct = 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) - }; + let percentage = (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height); match self.alignment { - Alignment::Start => pct, - Alignment::End => 1.0 - pct, + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, } } @@ -1499,18 +1493,14 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let pct = 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) - }; + let percentage = (cursor_position.x + - self.bounds.x + - self.scroller.bounds.width * grabbed_at) + / (self.bounds.width - self.scroller.bounds.width); match self.alignment { - Alignment::Start => pct, - Alignment::End => 1.0 - pct, + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, } } } -- cgit