summaryrefslogtreecommitdiffstats
path: root/native
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón <hector@lich.io>2021-06-04 19:39:08 +0700
committerLibravatar Héctor Ramón <hector@lich.io>2021-06-04 20:09:15 +0700
commit827577c179f78c9fb82a46142d2cdb9e61f4662b (patch)
treece1679dadced0a8c730fefa60cf8cae9b0ec9193 /native
parentf7d6e40bf0a0a61dc86d8a53303fab7cf93514a5 (diff)
downloadiced-827577c179f78c9fb82a46142d2cdb9e61f4662b.tar.gz
iced-827577c179f78c9fb82a46142d2cdb9e61f4662b.tar.bz2
iced-827577c179f78c9fb82a46142d2cdb9e61f4662b.zip
Introduce `snap_to` and `unsnap` to `scrollable::State`
Diffstat (limited to 'native')
-rw-r--r--native/src/widget/scrollable.rs323
1 files changed, 139 insertions, 184 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<Box<dyn Fn(f32, f32) -> 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<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.
///
/// 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;
- }
- }
- _ => {}
- }
- } 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);
+ self.state.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(
@@ -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<f32>,
scroll_box_touched_at: Option<Point>,
- prev_offset: u32,
- snap_to_bottom: bool,
- offset: RefCell<f32>,
- scroll_to: RefCell<Option<f32>>,
+ 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.