summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--examples/scrollable/src/main.rs88
-rw-r--r--native/src/widget/scrollable.rs118
2 files changed, 177 insertions, 29 deletions
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index a570f0f6..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 {
@@ -17,6 +17,9 @@ struct ScrollableDemo {
#[derive(Debug, Clone)]
enum Message {
ThemeChanged(style::Theme),
+ ScrollToTop(usize),
+ ScrollToBottom(usize),
+ Scrolled(usize, f32),
}
impl Sandbox for ScrollableDemo {
@@ -36,6 +39,25 @@ 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);
+
+ 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;
+ }
+ }
}
}
@@ -62,15 +84,28 @@ 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)
.spacing(10)
.width(Length::Fill)
.height(Length::Fill)
+ .on_scroll(move |offset| {
+ Message::Scrolled(i, offset)
+ })
.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,20 +145,31 @@ 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)
+ 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(),
@@ -153,10 +199,12 @@ 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<u16>,
scrollbar_margin: Option<u16>,
scroller_width: Option<u16>,
+ latest_offset: f32,
}
impl Variant {
@@ -165,34 +213,42 @@ 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,
+ latest_offset: 0.0,
},
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),
+ latest_offset: 0.0,
},
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),
+ latest_offset: 0.0,
},
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),
+ latest_offset: 0.0,
},
]
}
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index 7c4ea16c..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<Box<dyn Fn(f32) -> 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<Renderer::Style>) -> 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<Message>,
+ ) {
+ 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<Message, Renderer>
@@ -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;
}
}
@@ -418,11 +464,44 @@ where
}
/// The local state of a [`Scrollable`].
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Copy)]
pub struct State {
scroller_grabbed_at: Option<f32>,
scroll_box_touched_at: Option<Point>,
- offset: 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 {
@@ -443,13 +522,14 @@ impl State {
return;
}
- self.offset = (self.offset - 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.
@@ -459,17 +539,29 @@ impl State {
bounds: Rectangle,
content_bounds: Rectangle,
) {
+ self.snap_to(percentage);
+ self.unsnap(bounds, content_bounds);
+ }
+
+ /// Snaps the scroll position to a relative amount.
+ ///
+ /// `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));
+ }
+
+ /// 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 =
- ((content_bounds.height - bounds.height) * percentage).max(0.0);
+ 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.min(hidden_content as f32) as u32
+ self.offset.absolute(bounds, content_bounds) as u32
}
/// Returns whether the scroller is currently grabbed or not.