summaryrefslogtreecommitdiffstats
path: root/native/src/widget/scrollable.rs
diff options
context:
space:
mode:
Diffstat (limited to 'native/src/widget/scrollable.rs')
-rw-r--r--native/src/widget/scrollable.rs1094
1 files changed, 653 insertions, 441 deletions
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index ce734ad8..4ebb07a0 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -5,66 +5,65 @@ use crate::mouse;
use crate::overlay;
use crate::renderer;
use crate::touch;
-use crate::widget::Column;
+use crate::widget;
+use crate::widget::operation::{self, Operation};
+use crate::widget::tree::{self, Tree};
use crate::{
- Alignment, Background, Clipboard, Color, Element, Layout, Length, Padding,
- Point, Rectangle, Shell, Size, Vector, Widget,
+ Background, Clipboard, Color, Command, Element, Layout, Length, Point,
+ Rectangle, Shell, Size, Vector, Widget,
};
use std::{f32, u32};
pub use iced_style::scrollable::StyleSheet;
+pub mod style {
+ //! The styles of a [`Scrollable`].
+ //!
+ //! [`Scrollable`]: crate::widget::Scrollable
+ pub use iced_style::scrollable::{Scrollbar, Scroller};
+}
+
/// A widget that can vertically display an infinite amount of content with a
/// scrollbar.
#[allow(missing_debug_implementations)]
-pub struct Scrollable<'a, Message, Renderer> {
- state: &'a mut State,
+pub struct Scrollable<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ id: Option<Id>,
height: Length,
- max_height: u32,
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
- content: Column<'a, Message, Renderer>,
- on_scroll: Option<Box<dyn Fn(f32) -> Message>>,
- style_sheet: Box<dyn StyleSheet + 'a>,
+ content: Element<'a, Message, Renderer>,
+ on_scroll: Option<Box<dyn Fn(f32) -> Message + 'a>>,
+ style: <Renderer::Theme as StyleSheet>::Style,
}
-impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> {
- /// Creates a new [`Scrollable`] with the given [`State`].
- pub fn new(state: &'a mut State) -> Self {
+impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Creates a new [`Scrollable`].
+ pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
Scrollable {
- state,
+ id: None,
height: Length::Shrink,
- max_height: u32::MAX,
scrollbar_width: 10,
scrollbar_margin: 0,
scroller_width: 10,
- content: Column::new(),
+ content: content.into(),
on_scroll: None,
- style_sheet: Default::default(),
+ style: Default::default(),
}
}
- /// Sets the vertical spacing _between_ elements.
- ///
- /// Custom margins per element do not exist in Iced. You should use this
- /// method instead! While less flexible, it helps you keep spacing between
- /// elements consistent.
- pub fn spacing(mut self, units: u16) -> Self {
- self.content = self.content.spacing(units);
- self
- }
-
- /// Sets the [`Padding`] of the [`Scrollable`].
- pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
- self.content = self.content.padding(padding);
- self
- }
-
- /// Sets the width of the [`Scrollable`].
- pub fn width(mut self, width: Length) -> Self {
- self.content = self.content.width(width);
+ /// Sets the [`Id`] of the [`Scrollable`].
+ pub fn id(mut self, id: Id) -> Self {
+ self.id = Some(id);
self
}
@@ -74,24 +73,6 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> {
self
}
- /// Sets the maximum width of the [`Scrollable`].
- pub fn max_width(mut self, max_width: u32) -> Self {
- self.content = self.content.max_width(max_width);
- self
- }
-
- /// Sets the maximum height of the [`Scrollable`] in pixels.
- pub fn max_height(mut self, max_height: u32) -> Self {
- self.max_height = max_height;
- self
- }
-
- /// Sets the horizontal alignment of the contents of the [`Scrollable`] .
- pub fn align_items(mut self, align_items: Alignment) -> Self {
- self.content = self.content.align_items(align_items);
- self
- }
-
/// Sets the scrollbar width of the [`Scrollable`] .
/// Silently enforces a minimum value of 1.
pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self {
@@ -117,7 +98,7 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> {
///
/// 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 {
+ pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self {
self.on_scroll = Some(Box::new(f));
self
}
@@ -125,97 +106,37 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> {
/// Sets the style of the [`Scrollable`] .
pub fn style(
mut self,
- style_sheet: impl Into<Box<dyn StyleSheet + 'a>>,
+ style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
- self.style_sheet = style_sheet.into();
+ self.style = style.into();
self
}
+}
- /// Adds an element to the [`Scrollable`].
- pub fn push<E>(mut self, child: E) -> Self
- where
- E: Into<Element<'a, Message, Renderer>>,
- {
- self.content = self.content.push(child);
- self
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for Scrollable<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State>()
}
- fn notify_on_scroll(
- &self,
- bounds: Rectangle,
- content_bounds: Rectangle,
- shell: &mut Shell<'_, Message>,
- ) {
- if content_bounds.height <= bounds.height {
- return;
- }
+ fn state(&self) -> tree::State {
+ tree::State::new(State::new())
+ }
- if let Some(on_scroll) = &self.on_scroll {
- shell.publish(on_scroll(
- self.state.offset.absolute(bounds, content_bounds)
- / (content_bounds.height - bounds.height),
- ));
- }
+ fn children(&self) -> Vec<Tree> {
+ vec![Tree::new(&self.content)]
}
- fn scrollbar(
- &self,
- bounds: Rectangle,
- content_bounds: Rectangle,
- ) -> Option<Scrollbar> {
- let offset = self.state.offset(bounds, content_bounds);
-
- if content_bounds.height > bounds.height {
- let outer_width = self.scrollbar_width.max(self.scroller_width)
- + 2 * self.scrollbar_margin;
-
- let outer_bounds = Rectangle {
- x: bounds.x + bounds.width - outer_width as f32,
- y: bounds.y,
- width: outer_width as f32,
- height: bounds.height,
- };
-
- let scrollbar_bounds = Rectangle {
- x: bounds.x + bounds.width
- - f32::from(outer_width / 2 + self.scrollbar_width / 2),
- y: bounds.y,
- width: self.scrollbar_width as f32,
- height: bounds.height,
- };
-
- let ratio = bounds.height / content_bounds.height;
- let scroller_height = bounds.height * ratio;
- let y_offset = offset as f32 * ratio;
-
- let scroller_bounds = Rectangle {
- x: bounds.x + bounds.width
- - f32::from(outer_width / 2 + self.scroller_width / 2),
- y: scrollbar_bounds.y + y_offset,
- width: self.scroller_width as f32,
- height: scroller_height,
- };
-
- Some(Scrollbar {
- outer_bounds,
- bounds: scrollbar_bounds,
- scroller: Scroller {
- bounds: scroller_bounds,
- },
- })
- } else {
- None
- }
+ fn diff(&self, tree: &mut Tree) {
+ tree.diff_children(std::slice::from_ref(&self.content))
}
-}
-impl<'a, Message, Renderer> Widget<Message, Renderer>
- for Scrollable<'a, Message, Renderer>
-where
- Renderer: crate::Renderer,
-{
fn width(&self) -> Length {
- Widget::<Message, Renderer>::width(&self.content)
+ self.content.as_widget().width()
}
fn height(&self) -> Length {
@@ -227,24 +148,40 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .max_height(self.max_height)
- .width(Widget::<Message, Renderer>::width(&self.content))
- .height(self.height);
-
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, 0.0),
- Size::new(limits.max().width, f32::INFINITY),
- );
+ layout(
+ renderer,
+ limits,
+ Widget::<Message, Renderer>::width(self),
+ self.height,
+ u32::MAX,
+ |renderer, limits| {
+ self.content.as_widget().layout(renderer, limits)
+ },
+ )
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ operation: &mut dyn Operation<Message>,
+ ) {
+ let state = tree.state.downcast_mut::<State>();
- let content = self.content.layout(renderer, &child_limits);
- let size = limits.resolve(content.size());
+ operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
- layout::Node::with_children(size, vec![content])
+ operation.container(None, &mut |operation| {
+ self.content.as_widget().operate(
+ &mut tree.children[0],
+ layout.children().next().unwrap(),
+ operation,
+ );
+ });
}
fn on_event(
&mut self,
+ tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
@@ -252,122 +189,351 @@ where
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
-
- let content = layout.children().next().unwrap();
- let content_bounds = content.bounds();
-
- let scrollbar = self.scrollbar(bounds, content_bounds);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
-
- let event_status = {
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(
- cursor_position.x,
- cursor_position.y
- + self.state.offset(bounds, content_bounds) as f32,
+ update(
+ tree.state.downcast_mut::<State>(),
+ event,
+ layout,
+ cursor_position,
+ clipboard,
+ shell,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ &self.on_scroll,
+ |event, layout, cursor_position, clipboard, shell| {
+ self.content.as_widget_mut().on_event(
+ &mut tree.children[0],
+ event,
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ shell,
)
- } else {
- // TODO: Make `cursor_position` an `Option<Point>` so we can encode
- // cursor availability.
- // This will probably happen naturally once we add multi-window
- // support.
- Point::new(cursor_position.x, -1.0)
- };
-
- self.content.on_event(
- event.clone(),
- content,
- cursor_position,
+ },
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) {
+ draw(
+ tree.state.downcast_ref::<State>(),
+ renderer,
+ theme,
+ layout,
+ cursor_position,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ self.style,
+ |renderer, layout, cursor_position, viewport| {
+ self.content.as_widget().draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ style,
+ layout,
+ cursor_position,
+ viewport,
+ )
+ },
+ )
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ mouse_interaction(
+ tree.state.downcast_ref::<State>(),
+ layout,
+ cursor_position,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ |layout, cursor_position, viewport| {
+ self.content.as_widget().mouse_interaction(
+ &tree.children[0],
+ layout,
+ cursor_position,
+ viewport,
+ renderer,
+ )
+ },
+ )
+ }
+
+ fn overlay<'b>(
+ &'b self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ self.content
+ .as_widget()
+ .overlay(
+ &mut tree.children[0],
+ layout.children().next().unwrap(),
renderer,
- clipboard,
- shell,
)
+ .map(|overlay| {
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+ let offset = tree
+ .state
+ .downcast_ref::<State>()
+ .offset(bounds, content_bounds);
+
+ overlay.translate(Vector::new(0.0, -(offset as f32)))
+ })
+ }
+}
+
+impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Message: 'a,
+ Renderer: 'a + crate::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(
+ text_input: Scrollable<'a, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(text_input)
+ }
+}
+
+/// The identifier of a [`Scrollable`].
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Id(widget::Id);
+
+impl Id {
+ /// Creates a custom [`Id`].
+ pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
+ Self(widget::Id::new(id))
+ }
+
+ /// Creates a unique [`Id`].
+ ///
+ /// This function produces a different [`Id`] every time it is called.
+ pub fn unique() -> Self {
+ Self(widget::Id::unique())
+ }
+}
+
+/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`]
+/// to the provided `percentage`.
+pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> {
+ Command::widget(operation::scrollable::snap_to(id.0, percentage))
+}
+
+/// Computes the layout of a [`Scrollable`].
+pub fn layout<Renderer>(
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ width: Length,
+ height: Length,
+ max_height: u32,
+ layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
+) -> layout::Node {
+ let limits = limits.max_height(max_height).width(width).height(height);
+
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, 0.0),
+ Size::new(limits.max().width, f32::INFINITY),
+ );
+
+ let content = layout_content(renderer, &child_limits);
+ let size = limits.resolve(content.size());
+
+ layout::Node::with_children(size, vec![content])
+}
+
+/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`]
+/// accordingly.
+pub fn update<Message>(
+ state: &mut State,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
+ on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>,
+ update_content: impl FnOnce(
+ Event,
+ Layout<'_>,
+ Point,
+ &mut dyn Clipboard,
+ &mut Shell<'_, Message>,
+ ) -> event::Status,
+) -> event::Status {
+ let bounds = layout.bounds();
+ let is_mouse_over = bounds.contains(cursor_position);
+
+ let content = layout.children().next().unwrap();
+ let content_bounds = content.bounds();
+
+ let scrollbar = scrollbar(
+ state,
+ scrollbar_width,
+ scrollbar_margin,
+ scroller_width,
+ bounds,
+ content_bounds,
+ );
+ let is_mouse_over_scrollbar = scrollbar
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false);
+
+ let event_status = {
+ let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
+ Point::new(
+ cursor_position.x,
+ cursor_position.y + state.offset(bounds, content_bounds) as f32,
+ )
+ } else {
+ // TODO: Make `cursor_position` an `Option<Point>` so we can encode
+ // cursor availability.
+ // This will probably happen naturally once we add multi-window
+ // support.
+ Point::new(cursor_position.x, -1.0)
};
- if let event::Status::Captured = event_status {
- return event::Status::Captured;
- }
+ update_content(
+ event.clone(),
+ content,
+ cursor_position,
+ clipboard,
+ shell,
+ )
+ };
+
+ 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 (?)
+ state.scroll(y * 60.0, bounds, content_bounds);
+ }
+ mouse::ScrollDelta::Pixels { y, .. } => {
+ state.scroll(y, bounds, content_bounds);
+ }
+ }
- 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);
- }
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ return event::Status::Captured;
+ }
+ Event::Touch(event) => {
+ match event {
+ touch::Event::FingerPressed { .. } => {
+ state.scroll_box_touched_at = Some(cursor_position);
}
+ touch::Event::FingerMoved { .. } => {
+ if let Some(scroll_box_touched_at) =
+ state.scroll_box_touched_at
+ {
+ let delta =
+ cursor_position.y - scroll_box_touched_at.y;
- self.notify_on_scroll(bounds, content_bounds, shell);
+ state.scroll(delta, 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;
-
- self.state.scroll(
- delta,
- bounds,
- content_bounds,
- );
-
- self.state.scroll_box_touched_at =
- Some(cursor_position);
-
- self.notify_on_scroll(
- bounds,
- content_bounds,
- shell,
- );
- }
- }
- touch::Event::FingerLifted { .. }
- | touch::Event::FingerLost { .. } => {
- self.state.scroll_box_touched_at = None;
+ state.scroll_box_touched_at = Some(cursor_position);
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
}
}
-
- return event::Status::Captured;
+ touch::Event::FingerLifted { .. }
+ | touch::Event::FingerLost { .. } => {
+ 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 state.is_scroller_grabbed() {
+ match event {
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ state.scroller_grabbed_at = None;
+
+ return event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if let (Some(scrollbar), Some(scroller_grabbed_at)) =
+ (scrollbar, state.scroller_grabbed_at)
+ {
+ state.scroll_to(
+ scrollbar.scroll_percentage(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
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(
+ state.scroll_to(
scrollbar.scroll_percentage(
scroller_grabbed_at,
cursor_position,
@@ -376,112 +542,63 @@ where
content_bounds,
);
- self.notify_on_scroll(bounds, content_bounds, shell);
+ state.scroller_grabbed_at = Some(scroller_grabbed_at);
- return 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.notify_on_scroll(
- bounds,
- content_bounds,
- shell,
- );
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
- return event::Status::Captured;
- }
+ return event::Status::Captured;
}
}
- _ => {}
}
+ _ => {}
}
-
- event::Status::Ignored
}
- fn mouse_interaction(
- &self,
- layout: Layout<'_>,
- cursor_position: Point,
- _viewport: &Rectangle,
- renderer: &Renderer,
- ) -> mouse::Interaction {
- let bounds = layout.bounds();
- let content_layout = layout.children().next().unwrap();
- let content_bounds = content_layout.bounds();
- let scrollbar = self.scrollbar(bounds, content_bounds);
-
- let is_mouse_over = bounds.contains(cursor_position);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
-
- if is_mouse_over_scrollbar || self.state.is_scroller_grabbed() {
- mouse::Interaction::Idle
- } else {
- let offset = self.state.offset(bounds, content_bounds);
-
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(cursor_position.x, cursor_position.y + offset as f32)
- } else {
- Point::new(cursor_position.x, -1.0)
- };
-
- self.content.mouse_interaction(
- content_layout,
- cursor_position,
- &Rectangle {
- y: bounds.y + offset as f32,
- ..bounds
- },
- renderer,
- )
- }
- }
+ event::Status::Ignored
+}
- fn draw(
- &self,
- renderer: &mut Renderer,
- style: &renderer::Style,
- layout: Layout<'_>,
- cursor_position: Point,
- _viewport: &Rectangle,
- ) {
- let bounds = layout.bounds();
- let content_layout = layout.children().next().unwrap();
- let content_bounds = content_layout.bounds();
- let offset = self.state.offset(bounds, content_bounds);
- let scrollbar = self.scrollbar(bounds, content_bounds);
-
- let is_mouse_over = bounds.contains(cursor_position);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
+/// Computes the current [`mouse::Interaction`] of a [`Scrollable`].
+pub fn mouse_interaction(
+ state: &State,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
+ content_interaction: impl FnOnce(
+ Layout<'_>,
+ Point,
+ &Rectangle,
+ ) -> mouse::Interaction,
+) -> mouse::Interaction {
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+ let scrollbar = scrollbar(
+ state,
+ scrollbar_width,
+ scrollbar_margin,
+ scroller_width,
+ bounds,
+ content_bounds,
+ );
+
+ let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over_scrollbar = scrollbar
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false);
+
+ if is_mouse_over_scrollbar || state.is_scroller_grabbed() {
+ mouse::Interaction::Idle
+ } else {
+ let offset = state.offset(bounds, content_bounds);
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
Point::new(cursor_position.x, cursor_position.y + offset as f32)
@@ -489,104 +606,206 @@ where
Point::new(cursor_position.x, -1.0)
};
- if let Some(scrollbar) = scrollbar {
- renderer.with_layer(bounds, |renderer| {
- renderer.with_translation(
- Vector::new(0.0, -(offset as f32)),
- |renderer| {
- self.content.draw(
- renderer,
- style,
- content_layout,
- cursor_position,
- &Rectangle {
- y: bounds.y + offset as f32,
- ..bounds
- },
- );
- },
- );
- });
-
- let style = if self.state.is_scroller_grabbed() {
- self.style_sheet.dragging()
- } else if is_mouse_over_scrollbar {
- self.style_sheet.hovered()
- } else {
- self.style_sheet.active()
- };
-
- let is_scrollbar_visible =
- style.background.is_some() || style.border_width > 0.0;
+ content_interaction(
+ content_layout,
+ cursor_position,
+ &Rectangle {
+ y: bounds.y + offset as f32,
+ ..bounds
+ },
+ )
+ }
+}
- renderer.with_layer(
- Rectangle {
- width: bounds.width + 2.0,
- height: bounds.height + 2.0,
- ..bounds
- },
+/// Draws a [`Scrollable`].
+pub fn draw<Renderer>(
+ state: &State,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
+ style: <Renderer::Theme as StyleSheet>::Style,
+ draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle),
+) where
+ Renderer: crate::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+ let offset = state.offset(bounds, content_bounds);
+ let scrollbar = scrollbar(
+ state,
+ scrollbar_width,
+ scrollbar_margin,
+ scroller_width,
+ bounds,
+ content_bounds,
+ );
+
+ let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over_scrollbar = scrollbar
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false);
+
+ let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
+ Point::new(cursor_position.x, cursor_position.y + offset as f32)
+ } else {
+ Point::new(cursor_position.x, -1.0)
+ };
+
+ if let Some(scrollbar) = scrollbar {
+ renderer.with_layer(bounds, |renderer| {
+ renderer.with_translation(
+ Vector::new(0.0, -(offset as f32)),
|renderer| {
- if is_scrollbar_visible {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.bounds,
- border_radius: style.border_radius,
- border_width: style.border_width,
- border_color: style.border_color,
- },
- style.background.unwrap_or(Background::Color(
- Color::TRANSPARENT,
- )),
- );
- }
-
- if is_mouse_over
- || self.state.is_scroller_grabbed()
- || is_scrollbar_visible
- {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.scroller.bounds,
- border_radius: style.scroller.border_radius,
- border_width: style.scroller.border_width,
- border_color: style.scroller.border_color,
- },
- style.scroller.color,
- );
- }
+ draw_content(
+ renderer,
+ content_layout,
+ cursor_position,
+ &Rectangle {
+ y: bounds.y + offset as f32,
+ ..bounds
+ },
+ );
},
);
+ });
+
+ let style = if state.is_scroller_grabbed() {
+ theme.dragging(style)
+ } else if is_mouse_over_scrollbar {
+ theme.hovered(style)
} else {
- self.content.draw(
- renderer,
- style,
- content_layout,
- cursor_position,
- &Rectangle {
- y: bounds.y + offset as f32,
- ..bounds
- },
- );
- }
+ theme.active(style)
+ };
+
+ let is_scrollbar_visible =
+ style.background.is_some() || style.border_width > 0.0;
+
+ renderer.with_layer(
+ Rectangle {
+ width: bounds.width + 2.0,
+ height: bounds.height + 2.0,
+ ..bounds
+ },
+ |renderer| {
+ if is_scrollbar_visible {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scrollbar.bounds,
+ border_radius: style.border_radius,
+ border_width: style.border_width,
+ border_color: style.border_color,
+ },
+ style
+ .background
+ .unwrap_or(Background::Color(Color::TRANSPARENT)),
+ );
+ }
+
+ if is_mouse_over
+ || state.is_scroller_grabbed()
+ || is_scrollbar_visible
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scrollbar.scroller.bounds,
+ border_radius: style.scroller.border_radius,
+ border_width: style.scroller.border_width,
+ border_color: style.scroller.border_color,
+ },
+ style.scroller.color,
+ );
+ }
+ },
+ );
+ } else {
+ draw_content(
+ renderer,
+ content_layout,
+ cursor_position,
+ &Rectangle {
+ y: bounds.y + offset as f32,
+ ..bounds
+ },
+ );
}
+}
- fn overlay(
- &mut self,
- layout: Layout<'_>,
- renderer: &Renderer,
- ) -> Option<overlay::Element<'_, Message, Renderer>> {
- let Self { content, state, .. } = self;
+fn scrollbar(
+ state: &State,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+) -> Option<Scrollbar> {
+ let offset = state.offset(bounds, content_bounds);
+
+ if content_bounds.height > bounds.height {
+ let outer_width =
+ scrollbar_width.max(scroller_width) + 2 * scrollbar_margin;
+
+ let outer_bounds = Rectangle {
+ x: bounds.x + bounds.width - outer_width as f32,
+ y: bounds.y,
+ width: outer_width as f32,
+ height: bounds.height,
+ };
- content
- .overlay(layout.children().next().unwrap(), renderer)
- .map(|overlay| {
- let bounds = layout.bounds();
- let content_layout = layout.children().next().unwrap();
- let content_bounds = content_layout.bounds();
- let offset = state.offset(bounds, content_bounds);
+ let scrollbar_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - f32::from(outer_width / 2 + scrollbar_width / 2),
+ y: bounds.y,
+ width: scrollbar_width as f32,
+ height: bounds.height,
+ };
- overlay.translate(Vector::new(0.0, -(offset as f32)))
- })
+ let ratio = bounds.height / content_bounds.height;
+ let scroller_height = bounds.height * ratio;
+ let y_offset = offset as f32 * ratio;
+
+ let scroller_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - f32::from(outer_width / 2 + scroller_width / 2),
+ y: scrollbar_bounds.y + y_offset,
+ width: scroller_width as f32,
+ height: scroller_height,
+ };
+
+ Some(Scrollbar {
+ outer_bounds,
+ bounds: scrollbar_bounds,
+ scroller: Scroller {
+ bounds: scroller_bounds,
+ },
+ })
+ } else {
+ None
+ }
+}
+
+fn notify_on_scroll<Message>(
+ state: &State,
+ on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ shell: &mut Shell<'_, Message>,
+) {
+ if content_bounds.height <= bounds.height {
+ return;
+ }
+
+ if let Some(on_scroll) = on_scroll {
+ shell.publish(on_scroll(
+ state.offset.absolute(bounds, content_bounds)
+ / (content_bounds.height - bounds.height),
+ ));
}
}
@@ -608,6 +827,12 @@ impl Default for State {
}
}
+impl operation::Scrollable for State {
+ fn snap_to(&mut self, percentage: f32) {
+ State::snap_to(self, percentage);
+ }
+}
+
/// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Copy)]
enum Offset {
@@ -752,16 +977,3 @@ struct Scroller {
/// The bounds of the [`Scroller`].
bounds: Rectangle,
}
-
-impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
- for Element<'a, Message, Renderer>
-where
- Renderer: 'a + crate::Renderer,
- Message: 'a,
-{
- fn from(
- scrollable: Scrollable<'a, Message, Renderer>,
- ) -> Element<'a, Message, Renderer> {
- Element::new(scrollable)
- }
-}