Merge pull request #607 from yusdacra/scrollable_programmatically
Add methods to control `Scrollable` programmatically
This commit is contained in:
commit
0e70b11e00
|
@ -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,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue