Introduce snap_to
and unsnap
to scrollable::State
This commit is contained in:
parent
f7d6e40bf0
commit
827577c179
@ -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,8 +211,9 @@ 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 {
|
if is_mouse_over {
|
||||||
match event {
|
match event {
|
||||||
@ -249,18 +221,14 @@ where
|
|||||||
match delta {
|
match delta {
|
||||||
mouse::ScrollDelta::Lines { y, .. } => {
|
mouse::ScrollDelta::Lines { y, .. } => {
|
||||||
// TODO: Configurable speed (?)
|
// TODO: Configurable speed (?)
|
||||||
self.state.scroll(
|
self.state.scroll(y * 60.0, bounds, content_bounds);
|
||||||
y * 60.0,
|
|
||||||
bounds,
|
|
||||||
content_bounds,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
mouse::ScrollDelta::Pixels { y, .. } => {
|
mouse::ScrollDelta::Pixels { y, .. } => {
|
||||||
self.state.scroll(y, bounds, content_bounds);
|
self.state.scroll(y, bounds, content_bounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event_status = event::Status::Captured;
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
Event::Touch(event) => {
|
Event::Touch(event) => {
|
||||||
match event {
|
match event {
|
||||||
@ -272,8 +240,8 @@ where
|
|||||||
if let Some(scroll_box_touched_at) =
|
if let Some(scroll_box_touched_at) =
|
||||||
self.state.scroll_box_touched_at
|
self.state.scroll_box_touched_at
|
||||||
{
|
{
|
||||||
let delta = cursor_position.y
|
let delta =
|
||||||
- scroll_box_touched_at.y;
|
cursor_position.y - scroll_box_touched_at.y;
|
||||||
|
|
||||||
self.state.scroll(
|
self.state.scroll(
|
||||||
delta,
|
delta,
|
||||||
@ -291,7 +259,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event_status = event::Status::Captured;
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -306,7 +274,7 @@ where
|
|||||||
| 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::Mouse(mouse::Event::CursorMoved { .. })
|
||||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||||
@ -322,7 +290,7 @@ where
|
|||||||
content_bounds,
|
content_bounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
event_status = event::Status::Captured;
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -349,45 +317,16 @@ where
|
|||||||
self.state.scroller_grabbed_at =
|
self.state.scroller_grabbed_at =
|
||||||
Some(scroller_grabbed_at);
|
Some(scroller_grabbed_at);
|
||||||
|
|
||||||
event_status = event::Status::Captured;
|
return 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(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user