diff options
| -rw-r--r-- | examples/scrollable/src/main.rs | 88 | ||||
| -rw-r--r-- | native/src/widget/scrollable.rs | 118 | 
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.  | 
