Merge pull request #607 from yusdacra/scrollable_programmatically

Add methods to control `Scrollable` programmatically
This commit is contained in:
Héctor Ramón 2021-06-07 22:00:07 +07:00 committed by GitHub
commit 0e70b11e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 177 additions and 29 deletions

View File

@ -1,8 +1,8 @@
mod style; mod style;
use iced::{ use iced::{
button, scrollable, Button, Column, Container, Element, Length, Radio, Row, button, scrollable, Button, Column, Container, Element, Length,
Rule, Sandbox, Scrollable, Settings, Space, Text, ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text,
}; };
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
@ -17,6 +17,9 @@ struct ScrollableDemo {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
ThemeChanged(style::Theme), ThemeChanged(style::Theme),
ScrollToTop(usize),
ScrollToBottom(usize),
Scrolled(usize, f32),
} }
impl Sandbox for ScrollableDemo { impl Sandbox for ScrollableDemo {
@ -36,6 +39,25 @@ impl Sandbox for ScrollableDemo {
fn update(&mut self, message: Message) { fn update(&mut self, message: Message) {
match message { match message {
Message::ThemeChanged(theme) => self.theme = theme, 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( let scrollable_row = Row::with_children(
variants variants
.iter_mut() .iter_mut()
.map(|variant| { .enumerate()
.map(|(i, variant)| {
let mut scrollable = let mut scrollable =
Scrollable::new(&mut variant.scrollable) Scrollable::new(&mut variant.scrollable)
.padding(10) .padding(10)
.spacing(10) .spacing(10)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.on_scroll(move |offset| {
Message::Scrolled(i, offset)
})
.style(*theme) .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 { if let Some(scrollbar_width) = variant.scrollbar_width {
scrollable = scrollable scrollable = scrollable
@ -110,20 +145,31 @@ impl Sandbox for ScrollableDemo {
.push(Space::with_height(Length::Units(1200))) .push(Space::with_height(Length::Units(1200)))
.push(Text::new("Middle")) .push(Text::new("Middle"))
.push(Space::with_height(Length::Units(1200))) .push(Space::with_height(Length::Units(1200)))
.push(Text::new("The End."))
.push( .push(
Button::new( Button::new(
&mut variant.button, &mut variant.scroll_to_top,
Text::new("I am a button"), Text::new("Scroll to top"),
) )
.width(Length::Fill) .width(Length::Fill)
.padding(10), .padding(10)
) .on_press(Message::ScrollToTop(i)),
.push(Text::new("The End.")); );
Container::new(scrollable) Column::new()
.width(Length::Fill) .width(Length::Fill)
.height(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() .into()
}) })
.collect(), .collect(),
@ -153,10 +199,12 @@ impl Sandbox for ScrollableDemo {
struct Variant { struct Variant {
title: &'static str, title: &'static str,
scrollable: scrollable::State, scrollable: scrollable::State,
button: button::State, scroll_to_top: button::State,
scroll_to_bottom: button::State,
scrollbar_width: Option<u16>, scrollbar_width: Option<u16>,
scrollbar_margin: Option<u16>, scrollbar_margin: Option<u16>,
scroller_width: Option<u16>, scroller_width: Option<u16>,
latest_offset: f32,
} }
impl Variant { impl Variant {
@ -165,34 +213,42 @@ impl Variant {
Self { Self {
title: "Default Scrollbar", title: "Default Scrollbar",
scrollable: scrollable::State::new(), scrollable: scrollable::State::new(),
button: button::State::new(), scroll_to_top: button::State::new(),
scroll_to_bottom: button::State::new(),
scrollbar_width: None, scrollbar_width: None,
scrollbar_margin: None, scrollbar_margin: None,
scroller_width: None, scroller_width: None,
latest_offset: 0.0,
}, },
Self { Self {
title: "Slimmed & Margin", title: "Slimmed & Margin",
scrollable: scrollable::State::new(), 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_width: Some(4),
scrollbar_margin: Some(3), scrollbar_margin: Some(3),
scroller_width: Some(4), scroller_width: Some(4),
latest_offset: 0.0,
}, },
Self { Self {
title: "Wide Scroller", title: "Wide Scroller",
scrollable: scrollable::State::new(), 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_width: Some(4),
scrollbar_margin: None, scrollbar_margin: None,
scroller_width: Some(10), scroller_width: Some(10),
latest_offset: 0.0,
}, },
Self { Self {
title: "Narrow Scroller", title: "Narrow Scroller",
scrollable: scrollable::State::new(), 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_width: Some(10),
scrollbar_margin: None, scrollbar_margin: None,
scroller_width: Some(4), scroller_width: Some(4),
latest_offset: 0.0,
}, },
] ]
} }

View File

@ -23,6 +23,7 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
scrollbar_margin: u16, scrollbar_margin: u16,
scroller_width: u16, scroller_width: u16,
content: Column<'a, Message, Renderer>, content: Column<'a, Message, Renderer>,
on_scroll: Option<Box<dyn Fn(f32) -> Message>>,
style: Renderer::Style, style: Renderer::Style,
} }
@ -37,6 +38,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
scrollbar_margin: 0, scrollbar_margin: 0,
scroller_width: 10, scroller_width: 10,
content: Column::new(), content: Column::new(),
on_scroll: None,
style: Renderer::Style::default(), 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`] . /// 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 { pub fn scroller_width(mut self, scroller_width: u16) -> Self {
self.scroller_width = scroller_width.max(1); self.scroller_width = scroller_width.max(1);
self 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`] . /// Sets the style of the [`Scrollable`] .
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into(); 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.content = self.content.push(child);
self 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> impl<'a, Message, Renderer> Widget<Message, Renderer>
@ -228,6 +258,8 @@ where
} }
} }
self.notify_on_scroll(bounds, content_bounds, messages);
return event::Status::Captured; return event::Status::Captured;
} }
Event::Touch(event) => { Event::Touch(event) => {
@ -251,6 +283,12 @@ where
self.state.scroll_box_touched_at = self.state.scroll_box_touched_at =
Some(cursor_position); Some(cursor_position);
self.notify_on_scroll(
bounds,
content_bounds,
messages,
);
} }
} }
touch::Event::FingerLifted { .. } touch::Event::FingerLifted { .. }
@ -290,6 +328,8 @@ where
content_bounds, content_bounds,
); );
self.notify_on_scroll(bounds, content_bounds, messages);
return event::Status::Captured; return event::Status::Captured;
} }
} }
@ -317,6 +357,12 @@ where
self.state.scroller_grabbed_at = self.state.scroller_grabbed_at =
Some(scroller_grabbed_at); Some(scroller_grabbed_at);
self.notify_on_scroll(
bounds,
content_bounds,
messages,
);
return event::Status::Captured; return event::Status::Captured;
} }
} }
@ -418,11 +464,44 @@ where
} }
/// The local state of a [`Scrollable`]. /// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Copy, 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>,
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 { impl State {
@ -443,13 +522,14 @@ impl State {
return; return;
} }
self.offset = (self.offset - delta_y) self.offset = Offset::Absolute(
.max(0.0) (self.offset.absolute(bounds, content_bounds) - delta_y)
.min((content_bounds.height - bounds.height) as f32); .max(0.0)
.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.
@ -459,17 +539,29 @@ impl State {
bounds: Rectangle, bounds: Rectangle,
content_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 = 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 /// 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.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.