Introduce snap_to and unsnap to scrollable::State

This commit is contained in:
Héctor Ramón 2021-06-04 19:39:08 +07:00
parent f7d6e40bf0
commit 827577c179

View File

@ -10,7 +10,7 @@ use crate::{
Rectangle, Size, Vector, Widget, 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 /// A widget that can vertically display an infinite amount of content with a
/// scrollbar. /// scrollbar.
@ -24,8 +24,6 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
scroller_width: u16, scroller_width: u16,
content: Column<'a, Message, Renderer>, content: Column<'a, Message, Renderer>,
style: Renderer::Style, style: Renderer::Style,
on_scroll: Option<Box<dyn Fn(f32, f32) -> Message>>,
snap_to_bottom: bool,
} }
impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { 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, scroller_width: 10,
content: Column::new(), content: Column::new(),
style: Renderer::Style::default(), 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<F>(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. /// Sets the vertical spacing _between_ elements.
/// ///
/// Custom margins per element do not exist in Iced. You should use this /// 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)) .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
.unwrap_or(false); .unwrap_or(false);
let mut event_status = { let event_status = {
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
Point::new( Point::new(
cursor_position.x, cursor_position.x,
@ -240,78 +211,99 @@ where
) )
}; };
if let event::Status::Ignored = event_status { if let event::Status::Captured = event_status {
self.state.prev_offset = self.state.offset(bounds, content_bounds); 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( self.state.scroll(
y * 60.0, delta,
bounds, bounds,
content_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 = self.state.scroll_box_touched_at =
Some(cursor_position); 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 { .. }
event_status = event::Status::Captured; | touch::Event::FingerLost { .. } => {
self.state.scroll_box_touched_at = None;
}
} }
_ => {}
return event::Status::Captured;
} }
_ => {}
} }
}
if self.state.is_scroller_grabbed() { if self.state.is_scroller_grabbed() {
match event { match event {
Event::Mouse(mouse::Event::ButtonReleased( Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left, mouse::Button::Left,
)) ))
| Event::Touch(touch::Event::FingerLifted { .. }) | Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => { | Event::Touch(touch::Event::FingerLost { .. }) => {
self.state.scroller_grabbed_at = None; 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( self.state.scroll_to(
scrollbar.scroll_percentage( scrollbar.scroll_percentage(
@ -322,71 +314,18 @@ where
content_bounds, 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 { event::Status::Ignored
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( fn draw(
@ -400,15 +339,6 @@ where
let bounds = layout.bounds(); let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap(); let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds(); 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 offset = self.state.offset(bounds, content_bounds);
let scrollbar = renderer.scrollbar( let scrollbar = renderer.scrollbar(
bounds, bounds,
@ -488,14 +418,44 @@ where
} }
/// The local state of a [`Scrollable`]. /// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Copy)]
pub struct State { pub struct State {
scroller_grabbed_at: Option<f32>, scroller_grabbed_at: Option<f32>,
scroll_box_touched_at: Option<Point>, scroll_box_touched_at: Option<Point>,
prev_offset: u32, offset: Offset,
snap_to_bottom: bool, }
offset: RefCell<f32>,
scroll_to: RefCell<Option<f32>>, 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 { impl State {
@ -516,51 +476,46 @@ impl State {
return; return;
} }
let offset_val = *self.offset.borrow(); self.offset = Offset::Absolute(
*self.offset.borrow_mut() = (offset_val - delta_y) (self.offset.absolute(bounds, content_bounds) - delta_y)
.max(0.0) .max(0.0)
.min((content_bounds.height - bounds.height) as f32); .min((content_bounds.height - bounds.height) as f32),
);
} }
/// Moves the scroll position to a relative amount, given the bounds of /// Scrolls the [`Scrollable`] to a relative amount.
/// the [`Scrollable`] and its contents.
/// ///
/// `0` represents scrollbar at the top, while `1` represents scrollbar at /// `0` represents scrollbar at the top, while `1` represents scrollbar at
/// the bottom. /// the bottom.
pub fn scroll_to( pub fn scroll_to(
&self, &mut self,
percentage: f32, percentage: f32,
bounds: Rectangle, bounds: Rectangle,
content_bounds: Rectangle, content_bounds: Rectangle,
) { ) {
*self.offset.borrow_mut() = self.snap_to(percentage);
((content_bounds.height - bounds.height) * percentage).max(0.0); self.unsnap(bounds, content_bounds);
} }
/// Marks the scrollable to scroll to `perc` percentage (between 0.0 and 1.0) /// Snaps the scroll position to a relative amount.
/// in the next `draw` call.
/// ///
/// [`Scrollable`]: struct.Scrollable.html /// `0` represents scrollbar at the top, while `1` represents scrollbar at
/// [`State`]: struct.State.html /// the bottom.
pub fn scroll_to_percentage(&mut self, perc: f32) { pub fn snap_to(&mut self, percentage: f32) {
*self.scroll_to.borrow_mut() = Some(perc.max(0.0).min(1.0)); self.offset = Offset::Relative(percentage.max(0.0).min(1.0));
} }
/// Marks the scrollable to scroll to bottom in the next `draw` call. /// Unsnaps the current scroll position, if snapped, given the bounds of the
/// /// [`Scrollable`] and its contents.
/// [`Scrollable`]: struct.Scrollable.html pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
/// [`State`]: struct.State.html self.offset =
pub fn scroll_to_bottom(&mut self) { Offset::Absolute(self.offset.absolute(bounds, content_bounds));
self.scroll_to_percentage(1.0);
} }
/// Returns the current scrolling offset of the [`State`], given the bounds /// Returns the current scrolling offset of the [`State`], given the bounds
/// of the [`Scrollable`] and its contents. /// of the [`Scrollable`] and its contents.
pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
let hidden_content = self.offset.absolute(bounds, content_bounds) as u32
(content_bounds.height - bounds.height).max(0.0).round() as u32;
self.offset.borrow().min(hidden_content as f32) as u32
} }
/// Returns whether the scroller is currently grabbed or not. /// Returns whether the scroller is currently grabbed or not.