From f7d6e40bf0a0a61dc86d8a53303fab7cf93514a5 Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Tue, 10 Nov 2020 14:23:49 +0300 Subject: [PATCH 1/5] feat(native): Make scrollable programmatically scrollable for some use cases, add snap_to_bottom by default --- native/src/widget/scrollable.rs | 273 +++++++++++++++++++++----------- 1 file changed, 182 insertions(+), 91 deletions(-) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 7c4ea16c..374dcf76 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -10,7 +10,7 @@ use crate::{ Rectangle, Size, Vector, Widget, }; -use std::{f32, hash::Hash, u32}; +use std::{cell::RefCell, f32, hash::Hash, u32}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. @@ -24,6 +24,8 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { scroller_width: u16, content: Column<'a, Message, Renderer>, style: Renderer::Style, + on_scroll: Option Message>>, + snap_to_bottom: bool, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { @@ -38,9 +40,36 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { scroller_width: 10, content: Column::new(), style: Renderer::Style::default(), + on_scroll: None, + snap_to_bottom: false, } } + /// Whether to set the [`Scrollable`] to snap to bottom when the user + /// scrolls to bottom or not. This will keep the scrollable at the bottom + /// even if new content is added to the scrollable. + /// + /// [`Scrollable`]: struct.Scrollable.html + pub fn snap_to_bottom(mut self, snap: bool) -> Self { + self.snap_to_bottom = snap; + self + } + + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes two `f32` as arguments. First is the percentage of + /// where the scrollable is at right now. Second is the percentage of where + /// the scrollable was *before*. `0.0` means top and `1.0` means bottom. + /// + /// [`Scrollable`]: struct.Scrollable.html + pub fn on_scroll(mut self, message_constructor: F) -> Self + where + F: 'static + Fn(f32, f32) -> Message, + { + self.on_scroll = Some(Box::new(message_constructor)); + self + } + /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in Iced. You should use this @@ -186,7 +215,7 @@ where .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) .unwrap_or(false); - let event_status = { + let mut event_status = { let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { Point::new( cursor_position.x, @@ -211,99 +240,78 @@ where ) }; - if let event::Status::Captured = event_status { - return event::Status::Captured; - } - - if is_mouse_over { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) - self.state.scroll(y * 60.0, bounds, content_bounds); - } - mouse::ScrollDelta::Pixels { y, .. } => { - self.state.scroll(y, bounds, content_bounds); - } - } - - return event::Status::Captured; - } - Event::Touch(event) => { - match event { - touch::Event::FingerPressed { .. } => { - self.state.scroll_box_touched_at = - Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - self.state.scroll_box_touched_at - { - let delta = - cursor_position.y - scroll_box_touched_at.y; + if let event::Status::Ignored = event_status { + self.state.prev_offset = self.state.offset(bounds, content_bounds); + if is_mouse_over { + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + match delta { + mouse::ScrollDelta::Lines { y, .. } => { + // TODO: Configurable speed (?) self.state.scroll( - delta, + y * 60.0, bounds, content_bounds, ); + } + mouse::ScrollDelta::Pixels { y, .. } => { + self.state.scroll(y, bounds, content_bounds); + } + } + event_status = event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { self.state.scroll_box_touched_at = Some(cursor_position); } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + self.state.scroll_box_touched_at + { + let delta = cursor_position.y + - scroll_box_touched_at.y; + + self.state.scroll( + delta, + bounds, + content_bounds, + ); + + self.state.scroll_box_touched_at = + Some(cursor_position); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - self.state.scroll_box_touched_at = None; - } + + event_status = event::Status::Captured; } - - return event::Status::Captured; + _ => {} } - _ => {} } - } - if self.state.is_scroller_grabbed() { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.scroller_grabbed_at = None; + if self.state.is_scroller_grabbed() { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.scroller_grabbed_at = None; - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, self.state.scroller_grabbed_at) - { - self.state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - return event::Status::Captured; + event_status = event::Status::Captured; } - } - _ => {} - } - } else if is_mouse_over_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let (Some(scrollbar), Some(scroller_grabbed_at)) = + (scrollbar, self.state.scroller_grabbed_at) { self.state.scroll_to( scrollbar.scroll_percentage( @@ -314,18 +322,71 @@ where content_bounds, ); - self.state.scroller_grabbed_at = - Some(scroller_grabbed_at); - - return event::Status::Captured; + event_status = event::Status::Captured; } } + _ => {} + } + } else if is_mouse_over_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(scrollbar) = scrollbar { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) + { + self.state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + self.state.scroller_grabbed_at = + Some(scroller_grabbed_at); + + event_status = event::Status::Captured; + } + } + } + _ => {} } - _ => {} } } - event::Status::Ignored + if let event::Status::Captured = event_status { + if self.snap_to_bottom { + let new_offset = self.state.offset(bounds, content_bounds); + + if new_offset < self.state.prev_offset { + self.state.snap_to_bottom = false; + } else { + let scroll_perc = new_offset as f32 + / (content_bounds.height - bounds.height); + + if scroll_perc >= 1.0 - f32::EPSILON { + self.state.snap_to_bottom = true; + } + } + } + + if let Some(on_scroll) = &self.on_scroll { + messages.push(on_scroll( + self.state.offset(bounds, content_bounds) as f32 + / (content_bounds.height - bounds.height), + self.state.prev_offset as f32 + / (content_bounds.height - bounds.height), + )); + } + + event::Status::Captured + } else { + event::Status::Ignored + } } fn draw( @@ -339,6 +400,15 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); + + if self.state.snap_to_bottom { + self.state.scroll_to(1.0, bounds, content_bounds); + } + + if let Some(scroll_to) = self.state.scroll_to.borrow_mut().take() { + self.state.scroll_to(scroll_to, bounds, content_bounds); + } + let offset = self.state.offset(bounds, content_bounds); let scrollbar = renderer.scrollbar( bounds, @@ -418,11 +488,14 @@ where } /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Default)] pub struct State { scroller_grabbed_at: Option, scroll_box_touched_at: Option, - offset: f32, + prev_offset: u32, + snap_to_bottom: bool, + offset: RefCell, + scroll_to: RefCell>, } impl State { @@ -443,7 +516,8 @@ impl State { return; } - self.offset = (self.offset - delta_y) + let offset_val = *self.offset.borrow(); + *self.offset.borrow_mut() = (offset_val - delta_y) .max(0.0) .min((content_bounds.height - bounds.height) as f32); } @@ -454,22 +528,39 @@ impl State { /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. pub fn scroll_to( - &mut self, + &self, percentage: f32, bounds: Rectangle, content_bounds: Rectangle, ) { - self.offset = + *self.offset.borrow_mut() = ((content_bounds.height - bounds.height) * percentage).max(0.0); } + /// Marks the scrollable to scroll to `perc` percentage (between 0.0 and 1.0) + /// in the next `draw` call. + /// + /// [`Scrollable`]: struct.Scrollable.html + /// [`State`]: struct.State.html + pub fn scroll_to_percentage(&mut self, perc: f32) { + *self.scroll_to.borrow_mut() = Some(perc.max(0.0).min(1.0)); + } + + /// Marks the scrollable to scroll to bottom in the next `draw` call. + /// + /// [`Scrollable`]: struct.Scrollable.html + /// [`State`]: struct.State.html + pub fn scroll_to_bottom(&mut self) { + self.scroll_to_percentage(1.0); + } + /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { let hidden_content = (content_bounds.height - bounds.height).max(0.0).round() as u32; - self.offset.min(hidden_content as f32) as u32 + self.offset.borrow().min(hidden_content as f32) as u32 } /// Returns whether the scroller is currently grabbed or not. From 827577c179f78c9fb82a46142d2cdb9e61f4662b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Fri, 4 Jun 2021 19:39:08 +0700 Subject: [PATCH 2/5] Introduce `snap_to` and `unsnap` to `scrollable::State` --- native/src/widget/scrollable.rs | 325 ++++++++++++++------------------ 1 file changed, 140 insertions(+), 185 deletions(-) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 374dcf76..28d695ba 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -10,7 +10,7 @@ use crate::{ Rectangle, Size, Vector, Widget, }; -use std::{cell::RefCell, f32, hash::Hash, u32}; +use std::{f32, hash::Hash, u32}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. @@ -24,8 +24,6 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { scroller_width: u16, content: Column<'a, Message, Renderer>, style: Renderer::Style, - on_scroll: Option Message>>, - snap_to_bottom: bool, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { @@ -40,36 +38,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { scroller_width: 10, content: Column::new(), style: Renderer::Style::default(), - on_scroll: None, - snap_to_bottom: false, } } - /// Whether to set the [`Scrollable`] to snap to bottom when the user - /// scrolls to bottom or not. This will keep the scrollable at the bottom - /// even if new content is added to the scrollable. - /// - /// [`Scrollable`]: struct.Scrollable.html - pub fn snap_to_bottom(mut self, snap: bool) -> Self { - self.snap_to_bottom = snap; - self - } - - /// Sets a function to call when the [`Scrollable`] is scrolled. - /// - /// The function takes two `f32` as arguments. First is the percentage of - /// where the scrollable is at right now. Second is the percentage of where - /// the scrollable was *before*. `0.0` means top and `1.0` means bottom. - /// - /// [`Scrollable`]: struct.Scrollable.html - pub fn on_scroll(mut self, message_constructor: F) -> Self - where - F: 'static + Fn(f32, f32) -> Message, - { - self.on_scroll = Some(Box::new(message_constructor)); - self - } - /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in Iced. You should use this @@ -215,7 +186,7 @@ where .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) .unwrap_or(false); - let mut event_status = { + let event_status = { let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { Point::new( cursor_position.x, @@ -240,78 +211,99 @@ where ) }; - if let event::Status::Ignored = event_status { - self.state.prev_offset = self.state.offset(bounds, content_bounds); + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if is_mouse_over { + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + match delta { + mouse::ScrollDelta::Lines { y, .. } => { + // TODO: Configurable speed (?) + self.state.scroll(y * 60.0, bounds, content_bounds); + } + mouse::ScrollDelta::Pixels { y, .. } => { + self.state.scroll(y, bounds, content_bounds); + } + } + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + self.state.scroll_box_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + self.state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; - if is_mouse_over { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) self.state.scroll( - y * 60.0, + delta, bounds, content_bounds, ); - } - mouse::ScrollDelta::Pixels { y, .. } => { - self.state.scroll(y, bounds, content_bounds); - } - } - event_status = event::Status::Captured; - } - Event::Touch(event) => { - match event { - touch::Event::FingerPressed { .. } => { self.state.scroll_box_touched_at = Some(cursor_position); } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - self.state.scroll_box_touched_at - { - let delta = cursor_position.y - - scroll_box_touched_at.y; - - self.state.scroll( - delta, - bounds, - content_bounds, - ); - - self.state.scroll_box_touched_at = - Some(cursor_position); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - self.state.scroll_box_touched_at = None; - } } - - event_status = event::Status::Captured; + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } } - _ => {} + + return event::Status::Captured; } + _ => {} } + } - if self.state.is_scroller_grabbed() { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.scroller_grabbed_at = None; + if self.state.is_scroller_grabbed() { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.scroller_grabbed_at = None; - event_status = event::Status::Captured; + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let (Some(scrollbar), Some(scroller_grabbed_at)) = + (scrollbar, self.state.scroller_grabbed_at) + { + self.state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, self.state.scroller_grabbed_at) + } + _ => {} + } + } else if is_mouse_over_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(scrollbar) = scrollbar { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) { self.state.scroll_to( scrollbar.scroll_percentage( @@ -322,71 +314,18 @@ where content_bounds, ); - event_status = event::Status::Captured; + self.state.scroller_grabbed_at = + Some(scroller_grabbed_at); + + return event::Status::Captured; } } - _ => {} - } - } else if is_mouse_over_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - self.state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - self.state.scroller_grabbed_at = - Some(scroller_grabbed_at); - - event_status = event::Status::Captured; - } - } - } - _ => {} } + _ => {} } } - if let event::Status::Captured = event_status { - if self.snap_to_bottom { - let new_offset = self.state.offset(bounds, content_bounds); - - if new_offset < self.state.prev_offset { - self.state.snap_to_bottom = false; - } else { - let scroll_perc = new_offset as f32 - / (content_bounds.height - bounds.height); - - if scroll_perc >= 1.0 - f32::EPSILON { - self.state.snap_to_bottom = true; - } - } - } - - if let Some(on_scroll) = &self.on_scroll { - messages.push(on_scroll( - self.state.offset(bounds, content_bounds) as f32 - / (content_bounds.height - bounds.height), - self.state.prev_offset as f32 - / (content_bounds.height - bounds.height), - )); - } - - event::Status::Captured - } else { - event::Status::Ignored - } + event::Status::Ignored } fn draw( @@ -400,15 +339,6 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - - if self.state.snap_to_bottom { - self.state.scroll_to(1.0, bounds, content_bounds); - } - - if let Some(scroll_to) = self.state.scroll_to.borrow_mut().take() { - self.state.scroll_to(scroll_to, bounds, content_bounds); - } - let offset = self.state.offset(bounds, content_bounds); let scrollbar = renderer.scrollbar( bounds, @@ -488,14 +418,44 @@ where } /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Copy)] pub struct State { scroller_grabbed_at: Option, scroll_box_touched_at: Option, - prev_offset: u32, - snap_to_bottom: bool, - offset: RefCell, - scroll_to: RefCell>, + offset: Offset, +} + +impl Default for State { + fn default() -> Self { + Self { + scroller_grabbed_at: None, + scroll_box_touched_at: None, + offset: Offset::Absolute(0.0), + } + } +} + +/// The local state of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + match self { + Self::Absolute(absolute) => { + let hidden_content = + (content_bounds.height - bounds.height).max(0.0); + + absolute.min(hidden_content) + } + Self::Relative(percentage) => { + ((content_bounds.height - bounds.height) * percentage).max(0.0) + } + } + } } impl State { @@ -516,51 +476,46 @@ impl State { return; } - let offset_val = *self.offset.borrow(); - *self.offset.borrow_mut() = (offset_val - delta_y) - .max(0.0) - .min((content_bounds.height - bounds.height) as f32); + self.offset = Offset::Absolute( + (self.offset.absolute(bounds, content_bounds) - delta_y) + .max(0.0) + .min((content_bounds.height - bounds.height) as f32), + ); } - /// Moves the scroll position to a relative amount, given the bounds of - /// the [`Scrollable`] and its contents. + /// Scrolls the [`Scrollable`] to a relative amount. /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. pub fn scroll_to( - &self, + &mut self, percentage: f32, bounds: Rectangle, content_bounds: Rectangle, ) { - *self.offset.borrow_mut() = - ((content_bounds.height - bounds.height) * percentage).max(0.0); + self.snap_to(percentage); + self.unsnap(bounds, content_bounds); } - /// Marks the scrollable to scroll to `perc` percentage (between 0.0 and 1.0) - /// in the next `draw` call. + /// Snaps the scroll position to a relative amount. /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html - pub fn scroll_to_percentage(&mut self, perc: f32) { - *self.scroll_to.borrow_mut() = Some(perc.max(0.0).min(1.0)); + /// `0` represents scrollbar at the top, while `1` represents scrollbar at + /// the bottom. + pub fn snap_to(&mut self, percentage: f32) { + self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); } - /// Marks the scrollable to scroll to bottom in the next `draw` call. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html - pub fn scroll_to_bottom(&mut self) { - self.scroll_to_percentage(1.0); + /// Unsnaps the current scroll position, if snapped, given the bounds of the + /// [`Scrollable`] and its contents. + pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { + self.offset = + Offset::Absolute(self.offset.absolute(bounds, content_bounds)); } /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0).round() as u32; - - self.offset.borrow().min(hidden_content as f32) as u32 + self.offset.absolute(bounds, content_bounds) as u32 } /// Returns whether the scroller is currently grabbed or not. From 57510c43c853c7332890f8e7f36c6ba1f2a7f252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Fri, 4 Jun 2021 20:15:06 +0700 Subject: [PATCH 3/5] Add buttons to control scrolling in `scrollable` example --- examples/scrollable/src/main.rs | 52 +++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index a570f0f6..32c44df8 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -17,6 +17,8 @@ struct ScrollableDemo { #[derive(Debug, Clone)] enum Message { ThemeChanged(style::Theme), + ScrollToTop(usize), + ScrollToBottom(usize), } impl Sandbox for ScrollableDemo { @@ -36,6 +38,16 @@ impl Sandbox for ScrollableDemo { fn update(&mut self, message: Message) { match message { Message::ThemeChanged(theme) => self.theme = theme, + Message::ScrollToTop(i) => { + if let Some(variant) = self.variants.get_mut(i) { + variant.scrollable.snap_to(0.0); + } + } + Message::ScrollToBottom(i) => { + if let Some(variant) = self.variants.get_mut(i) { + variant.scrollable.snap_to(1.0); + } + } } } @@ -62,7 +74,8 @@ impl Sandbox for ScrollableDemo { let scrollable_row = Row::with_children( variants .iter_mut() - .map(|variant| { + .enumerate() + .map(|(i, variant)| { let mut scrollable = Scrollable::new(&mut variant.scrollable) .padding(10) @@ -70,7 +83,16 @@ impl Sandbox for ScrollableDemo { .width(Length::Fill) .height(Length::Fill) .style(*theme) - .push(Text::new(variant.title)); + .push(Text::new(variant.title)) + .push( + Button::new( + &mut variant.scroll_to_bottom, + Text::new("Scroll to bottom"), + ) + .width(Length::Fill) + .padding(10) + .on_press(Message::ScrollToBottom(i)), + ); if let Some(scrollbar_width) = variant.scrollbar_width { scrollable = scrollable @@ -110,15 +132,16 @@ impl Sandbox for ScrollableDemo { .push(Space::with_height(Length::Units(1200))) .push(Text::new("Middle")) .push(Space::with_height(Length::Units(1200))) + .push(Text::new("The End.")) .push( Button::new( - &mut variant.button, - Text::new("I am a button"), + &mut variant.scroll_to_top, + Text::new("Scroll to top"), ) .width(Length::Fill) - .padding(10), - ) - .push(Text::new("The End.")); + .padding(10) + .on_press(Message::ScrollToTop(i)), + ); Container::new(scrollable) .width(Length::Fill) @@ -153,7 +176,8 @@ impl Sandbox for ScrollableDemo { struct Variant { title: &'static str, scrollable: scrollable::State, - button: button::State, + scroll_to_top: button::State, + scroll_to_bottom: button::State, scrollbar_width: Option, scrollbar_margin: Option, scroller_width: Option, @@ -165,7 +189,8 @@ impl Variant { Self { title: "Default Scrollbar", scrollable: scrollable::State::new(), - button: button::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: None, scrollbar_margin: None, scroller_width: None, @@ -173,7 +198,8 @@ impl Variant { Self { title: "Slimmed & Margin", scrollable: scrollable::State::new(), - button: button::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: Some(3), scroller_width: Some(4), @@ -181,7 +207,8 @@ impl Variant { Self { title: "Wide Scroller", scrollable: scrollable::State::new(), - button: button::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: None, scroller_width: Some(10), @@ -189,7 +216,8 @@ impl Variant { Self { title: "Narrow Scroller", scrollable: scrollable::State::new(), - button: button::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: Some(10), scrollbar_margin: None, scroller_width: Some(4), From 3051d4ec763f0e073dd94526fde04f953967bd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Fri, 4 Jun 2021 20:46:27 +0700 Subject: [PATCH 4/5] Introduce `on_scroll` event in `Scrollable` --- native/src/widget/scrollable.rs | 48 ++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 28d695ba..68da2e67 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -23,6 +23,7 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { scrollbar_margin: u16, scroller_width: u16, content: Column<'a, Message, Renderer>, + on_scroll: Option Message>>, style: Renderer::Style, } @@ -37,6 +38,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { scrollbar_margin: 0, scroller_width: 10, content: Column::new(), + on_scroll: None, style: Renderer::Style::default(), } } @@ -101,12 +103,22 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. + /// + /// It silently enforces a minimum value of 1. pub fn scroller_width(mut self, scroller_width: u16) -> Self { self.scroller_width = scroller_width.max(1); self } + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the new relative offset of the [`Scrollable`] + /// (e.g. `0` means top, while `1` means bottom). + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + self.on_scroll = Some(Box::new(f)); + self + } + /// Sets the style of the [`Scrollable`] . pub fn style(mut self, style: impl Into) -> Self { self.style = style.into(); @@ -121,6 +133,24 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self.content = self.content.push(child); self } + + fn notify_on_scroll( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + messages: &mut Vec, + ) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = &self.on_scroll { + messages.push(on_scroll( + self.state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); + } + } } impl<'a, Message, Renderer> Widget @@ -228,6 +258,8 @@ where } } + self.notify_on_scroll(bounds, content_bounds, messages); + return event::Status::Captured; } Event::Touch(event) => { @@ -251,6 +283,12 @@ where self.state.scroll_box_touched_at = Some(cursor_position); + + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); } } touch::Event::FingerLifted { .. } @@ -290,6 +328,8 @@ where content_bounds, ); + self.notify_on_scroll(bounds, content_bounds, messages); + return event::Status::Captured; } } @@ -317,6 +357,12 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + return event::Status::Captured; } } From ce3a5f19b92889d03f564133a90d328d430137af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Fri, 4 Jun 2021 20:46:47 +0700 Subject: [PATCH 5/5] Add scrolling progress indicators to `scrollable` example --- examples/scrollable/src/main.rs | 36 +++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 32c44df8..3416b83d 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,8 +1,8 @@ mod style; use iced::{ - button, scrollable, Button, Column, Container, Element, Length, Radio, Row, - Rule, Sandbox, Scrollable, Settings, Space, Text, + button, scrollable, Button, Column, Container, Element, Length, + ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text, }; pub fn main() -> iced::Result { @@ -19,6 +19,7 @@ enum Message { ThemeChanged(style::Theme), ScrollToTop(usize), ScrollToBottom(usize), + Scrolled(usize, f32), } impl Sandbox for ScrollableDemo { @@ -41,11 +42,20 @@ impl Sandbox for ScrollableDemo { Message::ScrollToTop(i) => { if let Some(variant) = self.variants.get_mut(i) { variant.scrollable.snap_to(0.0); + + variant.latest_offset = 0.0; } } Message::ScrollToBottom(i) => { if let Some(variant) = self.variants.get_mut(i) { variant.scrollable.snap_to(1.0); + + variant.latest_offset = 1.0; + } + } + Message::Scrolled(i, offset) => { + if let Some(variant) = self.variants.get_mut(i) { + variant.latest_offset = offset; } } } @@ -82,6 +92,9 @@ impl Sandbox for ScrollableDemo { .spacing(10) .width(Length::Fill) .height(Length::Fill) + .on_scroll(move |offset| { + Message::Scrolled(i, offset) + }) .style(*theme) .push(Text::new(variant.title)) .push( @@ -143,10 +156,20 @@ impl Sandbox for ScrollableDemo { .on_press(Message::ScrollToTop(i)), ); - Container::new(scrollable) + Column::new() .width(Length::Fill) .height(Length::Fill) - .style(*theme) + .spacing(10) + .push( + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .style(*theme), + ) + .push(ProgressBar::new( + 0.0..=1.0, + variant.latest_offset, + )) .into() }) .collect(), @@ -181,6 +204,7 @@ struct Variant { scrollbar_width: Option, scrollbar_margin: Option, scroller_width: Option, + latest_offset: f32, } impl Variant { @@ -194,6 +218,7 @@ impl Variant { scrollbar_width: None, scrollbar_margin: None, scroller_width: None, + latest_offset: 0.0, }, Self { title: "Slimmed & Margin", @@ -203,6 +228,7 @@ impl Variant { scrollbar_width: Some(4), scrollbar_margin: Some(3), scroller_width: Some(4), + latest_offset: 0.0, }, Self { title: "Wide Scroller", @@ -212,6 +238,7 @@ impl Variant { scrollbar_width: Some(4), scrollbar_margin: None, scroller_width: Some(10), + latest_offset: 0.0, }, Self { title: "Narrow Scroller", @@ -221,6 +248,7 @@ impl Variant { scrollbar_width: Some(10), scrollbar_margin: None, scroller_width: Some(4), + latest_offset: 0.0, }, ] }