summaryrefslogtreecommitdiffstats
path: root/widget/src/scrollable.rs
diff options
context:
space:
mode:
Diffstat (limited to 'widget/src/scrollable.rs')
-rw-r--r--widget/src/scrollable.rs1374
1 files changed, 727 insertions, 647 deletions
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index f736d92e..9770ce57 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -1,4 +1,5 @@
//! Navigate an endless amount of content with a scrollbar.
+// use crate::container;
use crate::container;
use crate::core::event::{self, Event};
use crate::core::keyboard;
@@ -11,14 +12,11 @@ use crate::core::widget;
use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
- Rectangle, Shell, Size, Vector, Widget,
+ Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
+ Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
use crate::runtime::Command;
-pub use crate::style::scrollable::{
- Appearance, Scrollbar, Scroller, StyleSheet,
-};
pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
/// A widget that can vertically display an infinite amount of content with a
@@ -30,7 +28,6 @@ pub struct Scrollable<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
- Theme: StyleSheet,
Renderer: crate::core::Renderer,
{
id: Option<Id>,
@@ -39,18 +36,20 @@ pub struct Scrollable<
direction: Direction,
content: Element<'a, Message, Theme, Renderer>,
on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
- style: Theme::Style,
+ style: Style<Theme>,
}
impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer>
where
- Theme: StyleSheet,
Renderer: crate::core::Renderer,
{
/// Creates a new vertical [`Scrollable`].
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> Self {
+ ) -> Self
+ where
+ Theme: DefaultStyle,
+ {
Self::with_direction(content, Direction::default())
}
@@ -58,6 +57,22 @@ where
pub fn with_direction(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
direction: Direction,
+ ) -> Self
+ where
+ Theme: DefaultStyle,
+ {
+ Self::with_direction_and_style(
+ content,
+ direction,
+ Theme::default_style(),
+ )
+ }
+
+ /// Creates a new [`Scrollable`] with the given [`Direction`] and style.
+ pub fn with_direction_and_style(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+ direction: Direction,
+ style: fn(&Theme, Status) -> Appearance,
) -> Self {
let content = content.into();
@@ -80,7 +95,7 @@ where
direction,
content,
on_scroll: None,
- style: Default::default(),
+ style: style.into(),
}
}
@@ -111,7 +126,7 @@ where
}
/// Sets the style of the [`Scrollable`] .
- pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
+ pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into();
self
}
@@ -223,7 +238,6 @@ pub enum Alignment {
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Scrollable<'a, Message, Theme, Renderer>
where
- Theme: StyleSheet,
Renderer: crate::core::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -255,20 +269,29 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout(
- renderer,
- limits,
- self.width,
- self.height,
- &self.direction,
- |renderer, limits| {
- self.content.as_widget().layout(
- &mut tree.children[0],
- renderer,
- limits,
- )
- },
- )
+ layout::contained(limits, self.width, self.height, |limits| {
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, limits.min().height),
+ Size::new(
+ if self.direction.horizontal().is_some() {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ if self.direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
+ ),
+ );
+
+ self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &child_limits,
+ )
+ })
}
fn operate(
@@ -318,28 +341,316 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- update(
- tree.state.downcast_mut::<State>(),
- event,
- layout,
- cursor,
- clipboard,
- shell,
- self.direction,
- &self.on_scroll,
- |event, layout, cursor, clipboard, shell, viewport| {
- self.content.as_widget_mut().on_event(
- &mut tree.children[0],
- event,
- layout,
- cursor,
- renderer,
- clipboard,
+ let state = tree.state.downcast_mut::<State>();
+ let bounds = layout.bounds();
+ let cursor_over_scrollable = cursor.position_over(bounds);
+
+ let content = layout.children().next().unwrap();
+ let content_bounds = content.bounds();
+
+ let scrollbars =
+ Scrollbars::new(state, self.direction, bounds, content_bounds);
+
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor);
+
+ let mut event_status = {
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(
+ cursor_position
+ + state.translation(
+ self.direction,
+ bounds,
+ content_bounds,
+ ),
+ )
+ }
+ _ => mouse::Cursor::Unavailable,
+ };
+
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
+
+ self.content.as_widget_mut().on_event(
+ &mut tree.children[0],
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ &Rectangle {
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
+ ..bounds
+ },
+ )
+ };
+
+ if let event::Status::Captured = event_status {
+ return event::Status::Captured;
+ }
+
+ if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) =
+ event
+ {
+ state.keyboard_modifiers = modifiers;
+
+ return event::Status::Ignored;
+ }
+
+ match event {
+ Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+ if cursor_over_scrollable.is_none() {
+ return event::Status::Ignored;
+ }
+
+ let delta = match delta {
+ mouse::ScrollDelta::Lines { x, y } => {
+ // TODO: Configurable speed/friction (?)
+ let movement = if state.keyboard_modifiers.shift() {
+ Vector::new(y, x)
+ } else {
+ Vector::new(x, y)
+ };
+
+ movement * 60.0
+ }
+ mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
+ };
+
+ state.scroll(delta, self.direction, bounds, content_bounds);
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
shell,
- viewport,
- )
- },
- )
+ );
+
+ event_status = event::Status::Captured;
+ }
+ Event::Touch(event)
+ if state.scroll_area_touched_at.is_some()
+ || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
+ {
+ match event {
+ touch::Event::FingerPressed { .. } => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ state.scroll_area_touched_at = Some(cursor_position);
+ }
+ touch::Event::FingerMoved { .. } => {
+ if let Some(scroll_box_touched_at) =
+ state.scroll_area_touched_at
+ {
+ let Some(cursor_position) = cursor.position()
+ else {
+ return event::Status::Ignored;
+ };
+
+ let delta = Vector::new(
+ cursor_position.x - scroll_box_touched_at.x,
+ cursor_position.y - scroll_box_touched_at.y,
+ );
+
+ state.scroll(
+ delta,
+ self.direction,
+ bounds,
+ content_bounds,
+ );
+
+ state.scroll_area_touched_at =
+ Some(cursor_position);
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+ }
+ touch::Event::FingerLifted { .. }
+ | touch::Event::FingerLost { .. } => {
+ state.scroll_area_touched_at = None;
+ }
+ }
+
+ event_status = event::Status::Captured;
+ }
+ _ => {}
+ }
+
+ if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ state.y_scroller_grabbed_at = None;
+
+ event_status = event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if let Some(scrollbar) = scrollbars.y {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ event_status = event::Status::Captured;
+ }
+ }
+ _ => {}
+ }
+ } else if mouse_over_y_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
+ scrollbars.grab_y_scroller(cursor_position),
+ scrollbars.y,
+ ) {
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ event_status = event::Status::Captured;
+ }
+ _ => {}
+ }
+ }
+
+ if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ state.x_scroller_grabbed_at = None;
+
+ event_status = event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ if let Some(scrollbar) = scrollbars.x {
+ state.scroll_x_to(
+ scrollbar.scroll_percentage_x(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ event_status = event::Status::Captured;
+ }
+ _ => {}
+ }
+ } else if mouse_over_x_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
+ scrollbars.grab_x_scroller(cursor_position),
+ scrollbars.x,
+ ) {
+ state.scroll_x_to(
+ scrollbar.scroll_percentage_x(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ event_status = event::Status::Captured;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ event_status
}
fn draw(
@@ -352,26 +663,181 @@ where
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- draw(
- tree.state.downcast_ref::<State>(),
+ let state = tree.state.downcast_ref::<State>();
+
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+
+ let scrollbars =
+ Scrollbars::new(state, self.direction, bounds, content_bounds);
+
+ let cursor_over_scrollable = cursor.position_over(bounds);
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor);
+
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
+
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(cursor_position + translation)
+ }
+ _ => mouse::Cursor::Unavailable,
+ };
+
+ let status = if state.y_scroller_grabbed_at.is_some()
+ || state.x_scroller_grabbed_at.is_some()
+ {
+ Status::Dragged {
+ is_horizontal_scrollbar_dragged: state
+ .x_scroller_grabbed_at
+ .is_some(),
+ is_vertical_scrollbar_dragged: state
+ .y_scroller_grabbed_at
+ .is_some(),
+ }
+ } else if cursor_over_scrollable.is_some() {
+ Status::Hovered {
+ is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
+ is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
+ }
+ } else {
+ Status::Active
+ };
+
+ let appearance = (self.style)(theme, status);
+
+ container::draw_background(
renderer,
- theme,
- layout,
- cursor,
- self.direction,
- &self.style,
- |renderer, layout, cursor, viewport| {
- self.content.as_widget().draw(
- &tree.children[0],
- renderer,
- theme,
- style,
- layout,
- cursor,
- viewport,
- );
- },
+ &appearance.container,
+ layout.bounds(),
);
+
+ // Draw inner content
+ if scrollbars.active() {
+ renderer.with_layer(bounds, |renderer| {
+ renderer.with_translation(
+ Vector::new(-translation.x, -translation.y),
+ |renderer| {
+ self.content.as_widget().draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ style,
+ content_layout,
+ cursor,
+ &Rectangle {
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
+ ..bounds
+ },
+ );
+ },
+ );
+ });
+
+ let draw_scrollbar =
+ |renderer: &mut Renderer,
+ style: Scrollbar,
+ scrollbar: &internals::Scrollbar| {
+ if scrollbar.bounds.width > 0.0
+ && scrollbar.bounds.height > 0.0
+ && (style.background.is_some()
+ || (style.border.color != Color::TRANSPARENT
+ && style.border.width > 0.0))
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scrollbar.bounds,
+ border: style.border,
+ ..renderer::Quad::default()
+ },
+ style.background.unwrap_or(Background::Color(
+ Color::TRANSPARENT,
+ )),
+ );
+ }
+
+ if scrollbar.scroller.bounds.width > 0.0
+ && scrollbar.scroller.bounds.height > 0.0
+ && (style.scroller.color != Color::TRANSPARENT
+ || (style.scroller.border.color
+ != Color::TRANSPARENT
+ && style.scroller.border.width > 0.0))
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scrollbar.scroller.bounds,
+ border: style.scroller.border,
+ ..renderer::Quad::default()
+ },
+ style.scroller.color,
+ );
+ }
+ };
+
+ renderer.with_layer(
+ Rectangle {
+ width: bounds.width + 2.0,
+ height: bounds.height + 2.0,
+ ..bounds
+ },
+ |renderer| {
+ if let Some(scrollbar) = scrollbars.y {
+ draw_scrollbar(
+ renderer,
+ appearance.vertical_scrollbar,
+ &scrollbar,
+ );
+ }
+
+ if let Some(scrollbar) = scrollbars.x {
+ draw_scrollbar(
+ renderer,
+ appearance.horizontal_scrollbar,
+ &scrollbar,
+ );
+ }
+
+ if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
+ let background =
+ appearance.gap.or(appearance.container.background);
+
+ if let Some(background) = background {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: y.bounds.x,
+ y: x.bounds.y,
+ width: y.bounds.width,
+ height: x.bounds.height,
+ },
+ ..renderer::Quad::default()
+ },
+ background,
+ );
+ }
+ }
+ },
+ );
+ } else {
+ self.content.as_widget().draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ style,
+ content_layout,
+ cursor,
+ &Rectangle {
+ x: bounds.x + translation.x,
+ y: bounds.y + translation.y,
+ ..bounds
+ },
+ );
+ }
}
fn mouse_interaction(
@@ -382,21 +848,48 @@ where
_viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(
- tree.state.downcast_ref::<State>(),
- layout,
- cursor,
- self.direction,
- |layout, cursor, viewport| {
- self.content.as_widget().mouse_interaction(
- &tree.children[0],
- layout,
- cursor,
- viewport,
- renderer,
- )
- },
- )
+ let state = tree.state.downcast_ref::<State>();
+ let bounds = layout.bounds();
+ let cursor_over_scrollable = cursor.position_over(bounds);
+
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+
+ let scrollbars =
+ Scrollbars::new(state, self.direction, bounds, content_bounds);
+
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor);
+
+ if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
+ || state.scrollers_grabbed()
+ {
+ mouse::Interaction::Idle
+ } else {
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
+
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(cursor_position + translation)
+ }
+ _ => mouse::Cursor::Unavailable,
+ };
+
+ self.content.as_widget().mouse_interaction(
+ &tree.children[0],
+ layout,
+ cursor,
+ &Rectangle {
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
+ ..bounds
+ },
+ renderer,
+ )
+ }
}
fn overlay<'b>(
@@ -430,7 +923,7 @@ impl<'a, Message, Theme, Renderer>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: StyleSheet + 'a,
+ Theme: 'a,
Renderer: 'a + crate::core::Renderer,
{
fn from(
@@ -482,570 +975,6 @@ pub fn scroll_to<Message: 'static>(
Command::widget(operation::scrollable::scroll_to(id.0, offset))
}
-/// Computes the layout of a [`Scrollable`].
-pub fn layout<Renderer>(
- renderer: &Renderer,
- limits: &layout::Limits,
- width: Length,
- height: Length,
- direction: &Direction,
- layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
-) -> layout::Node {
- layout::contained(limits, width, height, |limits| {
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, limits.min().height),
- Size::new(
- if direction.horizontal().is_some() {
- f32::INFINITY
- } else {
- limits.max().width
- },
- if direction.vertical().is_some() {
- f32::MAX
- } else {
- limits.max().height
- },
- ),
- );
-
- layout_content(renderer, &child_limits)
- })
-}
-
-/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`]
-/// accordingly.
-pub fn update<Message>(
- state: &mut State,
- event: Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- clipboard: &mut dyn Clipboard,
- shell: &mut Shell<'_, Message>,
- direction: Direction,
- on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
- update_content: impl FnOnce(
- Event,
- Layout<'_>,
- mouse::Cursor,
- &mut dyn Clipboard,
- &mut Shell<'_, Message>,
- &Rectangle,
- ) -> event::Status,
-) -> event::Status {
- let bounds = layout.bounds();
- let cursor_over_scrollable = cursor.position_over(bounds);
-
- let content = layout.children().next().unwrap();
- let content_bounds = content.bounds();
-
- let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
-
- let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor);
-
- let mut event_status = {
- let cursor = match cursor_over_scrollable {
- Some(cursor_position)
- if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
- {
- mouse::Cursor::Available(
- cursor_position
- + state.translation(direction, bounds, content_bounds),
- )
- }
- _ => mouse::Cursor::Unavailable,
- };
-
- let translation = state.translation(direction, bounds, content_bounds);
-
- update_content(
- event.clone(),
- content,
- cursor,
- clipboard,
- shell,
- &Rectangle {
- y: bounds.y + translation.y,
- x: bounds.x + translation.x,
- ..bounds
- },
- )
- };
-
- if let event::Status::Captured = event_status {
- return event::Status::Captured;
- }
-
- if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event
- {
- state.keyboard_modifiers = modifiers;
-
- return event::Status::Ignored;
- }
-
- match event {
- Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
- if cursor_over_scrollable.is_none() {
- return event::Status::Ignored;
- }
-
- let delta = match delta {
- mouse::ScrollDelta::Lines { x, y } => {
- // TODO: Configurable speed/friction (?)
- let movement = if state.keyboard_modifiers.shift() {
- Vector::new(y, x)
- } else {
- Vector::new(x, y)
- };
-
- movement * 60.0
- }
- mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
- };
-
- state.scroll(delta, direction, bounds, content_bounds);
-
- notify_on_scroll(state, on_scroll, bounds, content_bounds, shell);
-
- event_status = event::Status::Captured;
- }
- Event::Touch(event)
- if state.scroll_area_touched_at.is_some()
- || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
- {
- match event {
- touch::Event::FingerPressed { .. } => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- state.scroll_area_touched_at = Some(cursor_position);
- }
- touch::Event::FingerMoved { .. } => {
- if let Some(scroll_box_touched_at) =
- state.scroll_area_touched_at
- {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
- );
-
- state.scroll(delta, direction, bounds, content_bounds);
-
- state.scroll_area_touched_at = Some(cursor_position);
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
- }
- touch::Event::FingerLifted { .. }
- | touch::Event::FingerLost { .. } => {
- state.scroll_area_touched_at = None;
- }
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
-
- if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
- match event {
- Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerLifted { .. })
- | Event::Touch(touch::Event::FingerLost { .. }) => {
- state.y_scroller_grabbed_at = None;
-
- event_status = event::Status::Captured;
- }
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some(scrollbar) = scrollbars.y {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- } else if mouse_over_y_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let (Some(scroller_grabbed_at), Some(scrollbar)) =
- (scrollbars.grab_y_scroller(cursor_position), scrollbars.y)
- {
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- }
-
- if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
- match event {
- Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerLifted { .. })
- | Event::Touch(touch::Event::FingerLost { .. }) => {
- state.x_scroller_grabbed_at = None;
-
- event_status = event::Status::Captured;
- }
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let Some(scrollbar) = scrollbars.x {
- state.scroll_x_to(
- scrollbar.scroll_percentage_x(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- } else if mouse_over_x_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let (Some(scroller_grabbed_at), Some(scrollbar)) =
- (scrollbars.grab_x_scroller(cursor_position), scrollbars.x)
- {
- state.scroll_x_to(
- scrollbar.scroll_percentage_x(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- }
-
- event_status
-}
-
-/// Computes the current [`mouse::Interaction`] of a [`Scrollable`].
-pub fn mouse_interaction(
- state: &State,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- direction: Direction,
- content_interaction: impl FnOnce(
- Layout<'_>,
- mouse::Cursor,
- &Rectangle,
- ) -> mouse::Interaction,
-) -> mouse::Interaction {
- let bounds = layout.bounds();
- let cursor_over_scrollable = cursor.position_over(bounds);
-
- let content_layout = layout.children().next().unwrap();
- let content_bounds = content_layout.bounds();
-
- let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
-
- let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor);
-
- if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
- || state.scrollers_grabbed()
- {
- mouse::Interaction::Idle
- } else {
- let translation = state.translation(direction, bounds, content_bounds);
-
- let cursor = match cursor_over_scrollable {
- Some(cursor_position)
- if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
- {
- mouse::Cursor::Available(cursor_position + translation)
- }
- _ => mouse::Cursor::Unavailable,
- };
-
- content_interaction(
- content_layout,
- cursor,
- &Rectangle {
- y: bounds.y + translation.y,
- x: bounds.x + translation.x,
- ..bounds
- },
- )
- }
-}
-
-/// Draws a [`Scrollable`].
-pub fn draw<Theme, Renderer>(
- state: &State,
- renderer: &mut Renderer,
- theme: &Theme,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- direction: Direction,
- style: &Theme::Style,
- draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle),
-) where
- Theme: StyleSheet,
- Renderer: crate::core::Renderer,
-{
- let bounds = layout.bounds();
- let content_layout = layout.children().next().unwrap();
- let content_bounds = content_layout.bounds();
-
- let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
-
- let cursor_over_scrollable = cursor.position_over(bounds);
- let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor);
-
- let translation = state.translation(direction, bounds, content_bounds);
-
- let cursor = match cursor_over_scrollable {
- Some(cursor_position)
- if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
- {
- mouse::Cursor::Available(cursor_position + translation)
- }
- _ => mouse::Cursor::Unavailable,
- };
-
- let appearance = if state.y_scroller_grabbed_at.is_some()
- || state.x_scroller_grabbed_at.is_some()
- {
- theme.dragging(style)
- } else if cursor_over_scrollable.is_some() {
- theme.hovered(style, mouse_over_y_scrollbar || mouse_over_x_scrollbar)
- } else {
- theme.active(style)
- };
-
- let scrollbar_style = |is_dragging: bool, mouse_over_scrollbar: bool| {
- if is_dragging {
- theme.dragging(style).scrollbar
- } else if cursor_over_scrollable.is_some() {
- theme.hovered(style, mouse_over_scrollbar).scrollbar
- } else {
- theme.active(style).scrollbar
- }
- };
-
- container::draw_background(
- renderer,
- &appearance.container,
- layout.bounds(),
- );
-
- // Draw inner content
- if scrollbars.active() {
- renderer.with_layer(bounds, |renderer| {
- renderer.with_translation(
- Vector::new(-translation.x, -translation.y),
- |renderer| {
- draw_content(
- renderer,
- content_layout,
- cursor,
- &Rectangle {
- y: bounds.y + translation.y,
- x: bounds.x + translation.x,
- ..bounds
- },
- );
- },
- );
- });
-
- let draw_scrollbar =
- |renderer: &mut Renderer,
- style: Scrollbar,
- scrollbar: &internals::Scrollbar| {
- if scrollbar.bounds.width > 0.0
- && scrollbar.bounds.height > 0.0
- && (style.background.is_some()
- || (style.border.color != Color::TRANSPARENT
- && style.border.width > 0.0))
- {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.bounds,
- border: style.border,
- ..renderer::Quad::default()
- },
- style
- .background
- .unwrap_or(Background::Color(Color::TRANSPARENT)),
- );
- }
-
- if scrollbar.scroller.bounds.width > 0.0
- && scrollbar.scroller.bounds.height > 0.0
- && (style.scroller.color != Color::TRANSPARENT
- || (style.scroller.border.color != Color::TRANSPARENT
- && style.scroller.border.width > 0.0))
- {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.scroller.bounds,
- border: style.scroller.border,
- ..renderer::Quad::default()
- },
- style.scroller.color,
- );
- }
- };
-
- renderer.with_layer(
- Rectangle {
- width: bounds.width + 2.0,
- height: bounds.height + 2.0,
- ..bounds
- },
- |renderer| {
- if let Some(scrollbar) = scrollbars.y {
- draw_scrollbar(
- renderer,
- scrollbar_style(
- state.y_scroller_grabbed_at.is_some(),
- mouse_over_y_scrollbar,
- ),
- &scrollbar,
- );
- }
-
- if let Some(scrollbar) = scrollbars.x {
- draw_scrollbar(
- renderer,
- scrollbar_style(
- state.x_scroller_grabbed_at.is_some(),
- mouse_over_x_scrollbar,
- ),
- &scrollbar,
- );
- }
-
- if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
- let background =
- appearance.gap.or(appearance.container.background);
-
- if let Some(background) = background {
- renderer.fill_quad(
- renderer::Quad {
- bounds: Rectangle {
- x: y.bounds.x,
- y: x.bounds.y,
- width: y.bounds.width,
- height: x.bounds.height,
- },
- ..renderer::Quad::default()
- },
- background,
- );
- }
- }
- },
- );
- } else {
- draw_content(
- renderer,
- content_layout,
- cursor,
- &Rectangle {
- x: bounds.x + translation.x,
- y: bounds.y + translation.y,
- ..bounds
- },
- );
- }
-}
-
fn notify_on_scroll<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
@@ -1093,9 +1022,8 @@ fn notify_on_scroll<Message>(
}
}
-/// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Copy)]
-pub struct State {
+struct State {
scroll_area_touched_at: Option<Point>,
offset_y: Offset,
y_scroller_grabbed_at: Option<f32>,
@@ -1625,3 +1553,155 @@ pub(super) mod internals {
pub bounds: Rectangle,
}
}
+
+/// The possible status of a [`Scrollable`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Status {
+ /// The [`Scrollable`] can be interacted with.
+ Active,
+ /// The [`Scrollable`] is being hovered.
+ Hovered {
+ /// Indicates if the horizontal scrollbar is being hovered.
+ is_horizontal_scrollbar_hovered: bool,
+ /// Indicates if the vertical scrollbar is being hovered.
+ is_vertical_scrollbar_hovered: bool,
+ },
+ /// The [`Scrollable`] is being dragged.
+ Dragged {
+ /// Indicates if the horizontal scrollbar is being dragged.
+ is_horizontal_scrollbar_dragged: bool,
+ /// Indicates if the vertical scrollbar is being dragged.
+ is_vertical_scrollbar_dragged: bool,
+ },
+}
+
+/// The appearance of a scrolable.
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The [`container::Appearance`] of a scrollable.
+ pub container: container::Appearance,
+ /// The vertical [`Scrollbar`] appearance.
+ pub vertical_scrollbar: Scrollbar,
+ /// The horizontal [`Scrollbar`] appearance.
+ pub horizontal_scrollbar: Scrollbar,
+ /// The [`Background`] of the gap between a horizontal and vertical scrollbar.
+ pub gap: Option<Background>,
+}
+
+/// The appearance of the scrollbar of a scrollable.
+#[derive(Debug, Clone, Copy)]
+pub struct Scrollbar {
+ /// The [`Background`] of a scrollbar.
+ pub background: Option<Background>,
+ /// The [`Border`] of a scrollbar.
+ pub border: Border,
+ /// The appearance of the [`Scroller`] of a scrollbar.
+ pub scroller: Scroller,
+}
+
+/// The appearance of the scroller of a scrollable.
+#[derive(Debug, Clone, Copy)]
+pub struct Scroller {
+ /// The [`Color`] of the scroller.
+ pub color: Color,
+ /// The [`Border`] of the scroller.
+ pub border: Border,
+}
+
+/// The style of a [`Scrollable`].
+pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
+
+/// The default style of a [`Scrollable`].
+pub trait DefaultStyle {
+ /// Returns the default style of a [`Scrollable`].
+ fn default_style() -> Style<Self>;
+}
+
+impl DefaultStyle for Theme {
+ fn default_style() -> Style<Self> {
+ default
+ }
+}
+
+impl DefaultStyle for Appearance {
+ fn default_style() -> Style<Self> {
+ |appearance, _status| *appearance
+ }
+}
+
+/// The default style of a [`Scrollable`].
+pub fn default(theme: &Theme, status: Status) -> Appearance {
+ let palette = theme.extended_palette();
+
+ let scrollbar = Scrollbar {
+ background: Some(palette.background.weak.color.into()),
+ border: Border::rounded(2),
+ scroller: Scroller {
+ color: palette.background.strong.color,
+ border: Border::rounded(2),
+ },
+ };
+
+ match status {
+ Status::Active => Appearance {
+ container: container::Appearance::default(),
+ vertical_scrollbar: scrollbar,
+ horizontal_scrollbar: scrollbar,
+ gap: None,
+ },
+ Status::Hovered {
+ is_horizontal_scrollbar_hovered,
+ is_vertical_scrollbar_hovered,
+ } => {
+ let hovered_scrollbar = Scrollbar {
+ scroller: Scroller {
+ color: palette.primary.strong.color,
+ ..scrollbar.scroller
+ },
+ ..scrollbar
+ };
+
+ Appearance {
+ container: container::Appearance::default(),
+ vertical_scrollbar: if is_vertical_scrollbar_hovered {
+ hovered_scrollbar
+ } else {
+ scrollbar
+ },
+ horizontal_scrollbar: if is_horizontal_scrollbar_hovered {
+ hovered_scrollbar
+ } else {
+ scrollbar
+ },
+ gap: None,
+ }
+ }
+ Status::Dragged {
+ is_horizontal_scrollbar_dragged,
+ is_vertical_scrollbar_dragged,
+ } => {
+ let dragged_scrollbar = Scrollbar {
+ scroller: Scroller {
+ color: palette.primary.base.color,
+ ..scrollbar.scroller
+ },
+ ..scrollbar
+ };
+
+ Appearance {
+ container: container::Appearance::default(),
+ vertical_scrollbar: if is_vertical_scrollbar_dragged {
+ dragged_scrollbar
+ } else {
+ scrollbar
+ },
+ horizontal_scrollbar: if is_horizontal_scrollbar_dragged {
+ dragged_scrollbar
+ } else {
+ scrollbar
+ },
+ gap: None,
+ }
+ }
+ }
+}