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.rs728
1 files changed, 486 insertions, 242 deletions
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index c3d08223..af6a3945 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -1,21 +1,24 @@
//! Navigate an endless amount of content with a scrollbar.
-// use crate::container;
use crate::container;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
+use crate::core::window;
use crate::core::{
- self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ self, Background, Clipboard, Color, Element, Layout, Length, Padding,
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
@@ -52,34 +55,51 @@ where
Self::with_direction(content, Direction::default())
}
- /// Creates a new [`Scrollable`] with the given [`Direction`].
+ /// Creates a new vertical [`Scrollable`].
pub fn with_direction(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- direction: Direction,
+ direction: impl Into<Direction>,
) -> Self {
- let content = content.into();
+ Scrollable {
+ id: None,
+ width: Length::Shrink,
+ height: Length::Shrink,
+ direction: direction.into(),
+ content: content.into(),
+ on_scroll: None,
+ class: Theme::default(),
+ }
+ .validate()
+ }
+
+ fn validate(mut self) -> Self {
+ let size_hint = self.content.as_widget().size_hint();
debug_assert!(
- direction.vertical().is_none()
- || !content.as_widget().size_hint().height.is_fill(),
+ self.direction.vertical().is_none() || !size_hint.height.is_fill(),
"scrollable content must not fill its vertical scrolling axis"
);
debug_assert!(
- direction.horizontal().is_none()
- || !content.as_widget().size_hint().width.is_fill(),
+ self.direction.horizontal().is_none() || !size_hint.width.is_fill(),
"scrollable content must not fill its horizontal scrolling axis"
);
- Scrollable {
- id: None,
- width: Length::Shrink,
- height: Length::Shrink,
- direction,
- content,
- on_scroll: None,
- class: Theme::default(),
+ if self.direction.horizontal().is_none() {
+ self.width = self.width.enclose(size_hint.width);
}
+
+ if self.direction.vertical().is_none() {
+ self.height = self.height.enclose(size_hint.height);
+ }
+
+ self
+ }
+
+ /// Creates a new [`Scrollable`] with the given [`Direction`].
+ pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
+ self.direction = direction.into();
+ self.validate()
}
/// Sets the [`Id`] of the [`Scrollable`].
@@ -108,6 +128,69 @@ where
self
}
+ /// Anchors the vertical [`Scrollable`] direction to the top.
+ pub fn anchor_top(self) -> Self {
+ self.anchor_y(Anchor::Start)
+ }
+
+ /// Anchors the vertical [`Scrollable`] direction to the bottom.
+ pub fn anchor_bottom(self) -> Self {
+ self.anchor_y(Anchor::End)
+ }
+
+ /// Anchors the horizontal [`Scrollable`] direction to the left.
+ pub fn anchor_left(self) -> Self {
+ self.anchor_x(Anchor::Start)
+ }
+
+ /// Anchors the horizontal [`Scrollable`] direction to the right.
+ pub fn anchor_right(self) -> Self {
+ self.anchor_x(Anchor::End)
+ }
+
+ /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable.
+ pub fn anchor_x(mut self, alignment: Anchor) -> Self {
+ match &mut self.direction {
+ Direction::Horizontal(horizontal)
+ | Direction::Both { horizontal, .. } => {
+ horizontal.alignment = alignment;
+ }
+ Direction::Vertical { .. } => {}
+ }
+
+ self
+ }
+
+ /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable.
+ pub fn anchor_y(mut self, alignment: Anchor) -> Self {
+ match &mut self.direction {
+ Direction::Vertical(vertical)
+ | Direction::Both { vertical, .. } => {
+ vertical.alignment = alignment;
+ }
+ Direction::Horizontal { .. } => {}
+ }
+
+ self
+ }
+
+ /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the
+ /// content.
+ ///
+ /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents
+ /// of the [`Scrollable`].
+ pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
+ match &mut self.direction {
+ Direction::Horizontal(scrollbar)
+ | Direction::Vertical(scrollbar) => {
+ scrollbar.spacing = Some(new_spacing.into().0);
+ }
+ Direction::Both { .. } => {}
+ }
+
+ self
+ }
+
/// Sets the style of this [`Scrollable`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -131,102 +214,133 @@ where
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
/// Vertical scrolling
- Vertical(Properties),
+ Vertical(Scrollbar),
/// Horizontal scrolling
- Horizontal(Properties),
+ Horizontal(Scrollbar),
/// Both vertical and horizontal scrolling
Both {
/// The properties of the vertical scrollbar.
- vertical: Properties,
+ vertical: Scrollbar,
/// The properties of the horizontal scrollbar.
- horizontal: Properties,
+ horizontal: Scrollbar,
},
}
impl Direction {
- /// Returns the [`Properties`] of the horizontal scrollbar, if any.
- pub fn horizontal(&self) -> Option<&Properties> {
+ /// Returns the horizontal [`Scrollbar`], if any.
+ pub fn horizontal(&self) -> Option<&Scrollbar> {
match self {
- Self::Horizontal(properties) => Some(properties),
+ Self::Horizontal(scrollbar) => Some(scrollbar),
Self::Both { horizontal, .. } => Some(horizontal),
Self::Vertical(_) => None,
}
}
- /// Returns the [`Properties`] of the vertical scrollbar, if any.
- pub fn vertical(&self) -> Option<&Properties> {
+ /// Returns the vertical [`Scrollbar`], if any.
+ pub fn vertical(&self) -> Option<&Scrollbar> {
match self {
- Self::Vertical(properties) => Some(properties),
+ Self::Vertical(scrollbar) => Some(scrollbar),
Self::Both { vertical, .. } => Some(vertical),
Self::Horizontal(_) => None,
}
}
+
+ fn align(&self, delta: Vector) -> Vector {
+ let horizontal_alignment =
+ self.horizontal().map(|p| p.alignment).unwrap_or_default();
+
+ let vertical_alignment =
+ self.vertical().map(|p| p.alignment).unwrap_or_default();
+
+ let align = |alignment: Anchor, delta: f32| match alignment {
+ Anchor::Start => delta,
+ Anchor::End => -delta,
+ };
+
+ Vector::new(
+ align(horizontal_alignment, delta.x),
+ align(vertical_alignment, delta.y),
+ )
+ }
}
impl Default for Direction {
fn default() -> Self {
- Self::Vertical(Properties::default())
+ Self::Vertical(Scrollbar::default())
}
}
-/// Properties of a scrollbar within a [`Scrollable`].
+/// A scrollbar within a [`Scrollable`].
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Properties {
+pub struct Scrollbar {
width: f32,
margin: f32,
scroller_width: f32,
- alignment: Alignment,
+ alignment: Anchor,
+ spacing: Option<f32>,
}
-impl Default for Properties {
+impl Default for Scrollbar {
fn default() -> Self {
Self {
width: 10.0,
margin: 0.0,
scroller_width: 10.0,
- alignment: Alignment::Start,
+ alignment: Anchor::Start,
+ spacing: None,
}
}
}
-impl Properties {
- /// Creates new [`Properties`] for use in a [`Scrollable`].
+impl Scrollbar {
+ /// Creates new [`Scrollbar`] for use in a [`Scrollable`].
pub fn new() -> Self {
Self::default()
}
- /// Sets the scrollbar width of the [`Scrollable`] .
+ /// Sets the scrollbar width of the [`Scrollbar`] .
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = width.into().0.max(0.0);
self
}
- /// Sets the scrollbar margin of the [`Scrollable`] .
+ /// Sets the scrollbar margin of the [`Scrollbar`] .
pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
self.margin = margin.into().0;
self
}
- /// Sets the scroller width of the [`Scrollable`] .
+ /// Sets the scroller width of the [`Scrollbar`] .
pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
self.scroller_width = scroller_width.into().0.max(0.0);
self
}
- /// Sets the alignment of the [`Scrollable`] .
- pub fn alignment(mut self, alignment: Alignment) -> Self {
+ /// Sets the [`Anchor`] of the [`Scrollbar`] .
+ pub fn anchor(mut self, alignment: Anchor) -> Self {
self.alignment = alignment;
self
}
+
+ /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using
+ /// the given spacing between itself and the contents.
+ ///
+ /// An embedded [`Scrollbar`] will always be displayed, will take layout space,
+ /// and will not float over the contents.
+ pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
+ self.spacing = Some(spacing.into().0);
+ self
+ }
}
-/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction.
+/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`]
+/// on a given axis.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
-pub enum Alignment {
- /// Content is aligned to the start of the [`Viewport`].
+pub enum Anchor {
+ /// Scroller is anchoer to the start of the [`Viewport`].
#[default]
Start,
- /// Content is aligned to the end of the [`Viewport`]
+ /// Content is aligned to the end of the [`Viewport`].
End,
}
@@ -265,29 +379,55 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- 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
- },
- ),
- );
+ let (right_padding, bottom_padding) = match self.direction {
+ Direction::Vertical(Scrollbar {
+ width,
+ margin,
+ spacing: Some(spacing),
+ ..
+ }) => (width + margin * 2.0 + spacing, 0.0),
+ Direction::Horizontal(Scrollbar {
+ width,
+ margin,
+ spacing: Some(spacing),
+ ..
+ }) => (0.0, width + margin * 2.0 + spacing),
+ _ => (0.0, 0.0),
+ };
- self.content.as_widget().layout(
- &mut tree.children[0],
- renderer,
- &child_limits,
- )
- })
+ layout::padded(
+ limits,
+ self.width,
+ self.height,
+ Padding {
+ right: right_padding,
+ bottom: bottom_padding,
+ ..Padding::ZERO
+ },
+ |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(
@@ -295,7 +435,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
let state = tree.state.downcast_mut::<State>();
@@ -309,6 +449,7 @@ where
state,
self.id.as_ref().map(|id| &id.0),
bounds,
+ content_bounds,
translation,
);
@@ -350,6 +491,24 @@ where
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
scrollbars.is_mouse_over(cursor);
+ if let Some(last_scrolled) = state.last_scrolled {
+ let clear_transaction = match event {
+ Event::Mouse(
+ mouse::Event::ButtonPressed(_)
+ | mouse::Event::ButtonReleased(_)
+ | mouse::Event::CursorLeft,
+ ) => true,
+ Event::Mouse(mouse::Event::CursorMoved { .. }) => {
+ last_scrolled.elapsed() > Duration::from_millis(100)
+ }
+ _ => last_scrolled.elapsed() > Duration::from_millis(1500),
+ };
+
+ if clear_transaction {
+ state.last_scrolled = None;
+ }
+ }
+
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
match event {
Event::Mouse(mouse::Event::CursorMoved { .. })
@@ -368,7 +527,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -406,7 +565,7 @@ where
state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -439,7 +598,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -477,7 +636,7 @@ where
state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -492,7 +651,11 @@ where
}
}
- let mut event_status = {
+ let content_status = if state.last_scrolled.is_some()
+ && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
+ {
+ event::Status::Ignored
+ } else {
let cursor = match cursor_over_scrollable {
Some(cursor_position)
if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
@@ -540,10 +703,10 @@ where
state.x_scroller_grabbed_at = None;
state.y_scroller_grabbed_at = None;
- return event_status;
+ return content_status;
}
- if let event::Status::Captured = event_status {
+ if let event::Status::Captured = content_status {
return event::Status::Captured;
}
@@ -563,23 +726,41 @@ where
let delta = match delta {
mouse::ScrollDelta::Lines { x, y } => {
- // TODO: Configurable speed/friction (?)
- let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed
- && state.keyboard_modifiers.shift()
- {
- Vector::new(y, x)
- } else {
+ let is_shift_pressed = state.keyboard_modifiers.shift();
+
+ // macOS automatically inverts the axes when Shift is pressed
+ let (x, y) =
+ if cfg!(target_os = "macos") && is_shift_pressed {
+ (y, x)
+ } else {
+ (x, y)
+ };
+
+ let is_vertical = match self.direction {
+ Direction::Vertical(_) => true,
+ Direction::Horizontal(_) => false,
+ Direction::Both { .. } => !is_shift_pressed,
+ };
+
+ let movement = if is_vertical {
Vector::new(x, y)
+ } else {
+ Vector::new(y, x)
};
- movement * 60.0
+ // TODO: Configurable speed/friction (?)
+ -movement * 60.0
}
mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
};
- state.scroll(delta, self.direction, bounds, content_bounds);
+ state.scroll(
+ self.direction.align(delta),
+ bounds,
+ content_bounds,
+ );
- event_status = if notify_on_scroll(
+ if notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -589,7 +770,7 @@ where
event::Status::Captured
} else {
event::Status::Ignored
- };
+ }
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
@@ -613,13 +794,12 @@ where
};
let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
+ scroll_box_touched_at.x - cursor_position.x,
+ scroll_box_touched_at.y - cursor_position.y,
);
state.scroll(
- delta,
- self.direction,
+ self.direction.align(delta),
bounds,
content_bounds,
);
@@ -628,7 +808,7 @@ where
Some(cursor_position);
// TODO: bubble up touch movements if not consumed.
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -640,12 +820,21 @@ where
_ => {}
}
- event_status = event::Status::Captured;
+ event::Status::Captured
}
- _ => {}
- }
+ Event::Window(window::Event::RedrawRequested(_)) => {
+ let _ = notify_viewport(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
- event_status
+ event::Status::Ignored
+ }
+ _ => event::Status::Ignored,
+ }
}
fn draw(
@@ -736,7 +925,7 @@ where
let draw_scrollbar =
|renderer: &mut Renderer,
- style: Scrollbar,
+ style: Rail,
scrollbar: &internals::Scrollbar| {
if scrollbar.bounds.width > 0.0
&& scrollbar.bounds.height > 0.0
@@ -756,21 +945,23 @@ where
);
}
- 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,
- );
+ if let Some(scroller) = scrollbar.scroller {
+ if scroller.bounds.width > 0.0
+ && 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: scroller.bounds,
+ border: style.scroller.border,
+ ..renderer::Quad::default()
+ },
+ style.scroller.color,
+ );
+ }
}
};
@@ -784,7 +975,7 @@ where
if let Some(scrollbar) = scrollbars.y {
draw_scrollbar(
renderer,
- style.vertical_scrollbar,
+ style.vertical_rail,
&scrollbar,
);
}
@@ -792,7 +983,7 @@ where
if let Some(scrollbar) = scrollbars.x {
draw_scrollbar(
renderer,
- style.horizontal_scrollbar,
+ style.horizontal_rail,
&scrollbar,
);
}
@@ -953,21 +1144,44 @@ impl From<Id> for widget::Id {
}
/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`]
-/// to the provided `percentage` along the x & y axis.
+/// to the provided [`RelativeOffset`].
pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> {
- Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
+ task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
}
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
-/// to the provided [`AbsoluteOffset`] along the x & y axis.
+/// to the provided [`AbsoluteOffset`].
pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
- Task::effect(Action::widget(operation::scrollable::scroll_to(
+ task::effect(Action::widget(operation::scrollable::scroll_to(
id.0, offset,
)))
}
-/// Returns [`true`] if the viewport actually changed.
-fn notify_on_scroll<Message>(
+/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
+/// by the provided [`AbsoluteOffset`].
+pub fn scroll_by<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
+ task::effect(Action::widget(operation::scrollable::scroll_by(
+ id.0, offset,
+ )))
+}
+
+fn notify_scroll<Message>(
+ state: &mut State,
+ on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ shell: &mut Shell<'_, Message>,
+) -> bool {
+ if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
+ state.last_scrolled = Some(Instant::now());
+
+ true
+ } else {
+ false
+ }
+}
+
+fn notify_viewport<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
bounds: Rectangle,
@@ -980,6 +1194,11 @@ fn notify_on_scroll<Message>(
return false;
}
+ let Some(on_scroll) = on_scroll else {
+ state.last_notified = None;
+ return false;
+ };
+
let viewport = Viewport {
offset_x: state.offset_x,
offset_y: state.offset_y,
@@ -999,7 +1218,9 @@ fn notify_on_scroll<Message>(
(a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
};
- if unchanged(last_relative_offset.x, current_relative_offset.x)
+ if last_notified.bounds == bounds
+ && last_notified.content_bounds == content_bounds
+ && unchanged(last_relative_offset.x, current_relative_offset.x)
&& unchanged(last_relative_offset.y, current_relative_offset.y)
&& unchanged(last_absolute_offset.x, current_absolute_offset.x)
&& unchanged(last_absolute_offset.y, current_absolute_offset.y)
@@ -1008,9 +1229,7 @@ fn notify_on_scroll<Message>(
}
}
- if let Some(on_scroll) = on_scroll {
- shell.publish(on_scroll(viewport));
- }
+ shell.publish(on_scroll(viewport));
state.last_notified = Some(viewport);
true
@@ -1025,6 +1244,7 @@ struct State {
x_scroller_grabbed_at: Option<f32>,
keyboard_modifiers: keyboard::Modifiers,
last_notified: Option<Viewport>,
+ last_scrolled: Option<Instant>,
}
impl Default for State {
@@ -1037,6 +1257,7 @@ impl Default for State {
x_scroller_grabbed_at: None,
keyboard_modifiers: keyboard::Modifiers::default(),
last_notified: None,
+ last_scrolled: None,
}
}
}
@@ -1049,6 +1270,15 @@ impl operation::Scrollable for State {
fn scroll_to(&mut self, offset: AbsoluteOffset) {
State::scroll_to(self, offset);
}
+
+ fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ State::scroll_by(self, offset, bounds, content_bounds);
+ }
}
#[derive(Debug, Clone, Copy)]
@@ -1073,13 +1303,13 @@ impl Offset {
self,
viewport: f32,
content: f32,
- alignment: Alignment,
+ alignment: Anchor,
) -> f32 {
let offset = self.absolute(viewport, content);
match alignment {
- Alignment::Start => offset,
- Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0),
+ Anchor::Start => offset,
+ Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
}
}
}
@@ -1152,34 +1382,13 @@ impl State {
pub fn scroll(
&mut self,
delta: Vector<f32>,
- direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- let horizontal_alignment = direction
- .horizontal()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let vertical_alignment = direction
- .vertical()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let align = |alignment: Alignment, delta: f32| match alignment {
- Alignment::Start => delta,
- Alignment::End => -delta,
- };
-
- let delta = Vector::new(
- align(horizontal_alignment, delta.x),
- align(vertical_alignment, delta.y),
- );
-
if bounds.height < content_bounds.height {
self.offset_y = Offset::Absolute(
(self.offset_y.absolute(bounds.height, content_bounds.height)
- - delta.y)
+ + delta.y)
.clamp(0.0, content_bounds.height - bounds.height),
);
}
@@ -1187,7 +1396,7 @@ impl State {
if bounds.width < content_bounds.width {
self.offset_x = Offset::Absolute(
(self.offset_x.absolute(bounds.width, content_bounds.width)
- - delta.x)
+ + delta.x)
.clamp(0.0, content_bounds.width - bounds.width),
);
}
@@ -1233,6 +1442,16 @@ impl State {
self.offset_y = Offset::Absolute(offset.y.max(0.0));
}
+ /// Scroll by the provided [`AbsoluteOffset`].
+ pub fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
+ }
+
/// 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) {
@@ -1298,16 +1517,16 @@ impl Scrollbars {
) -> Self {
let translation = state.translation(direction, bounds, content_bounds);
- let show_scrollbar_x = direction
- .horizontal()
- .filter(|_| content_bounds.width > bounds.width);
+ let show_scrollbar_x = direction.horizontal().filter(|scrollbar| {
+ scrollbar.spacing.is_some() || content_bounds.width > bounds.width
+ });
- let show_scrollbar_y = direction
- .vertical()
- .filter(|_| content_bounds.height > bounds.height);
+ let show_scrollbar_y = direction.vertical().filter(|scrollbar| {
+ scrollbar.spacing.is_some() || content_bounds.height > bounds.height
+ });
let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
- let Properties {
+ let Scrollbar {
width,
margin,
scroller_width,
@@ -1341,26 +1560,35 @@ impl Scrollbars {
};
let ratio = bounds.height / content_bounds.height;
- // min height for easier grabbing with super tall content
- let scroller_height = (scrollbar_bounds.height * ratio).max(2.0);
- let scroller_offset =
- translation.y * ratio * scrollbar_bounds.height / bounds.height;
- let scroller_bounds = Rectangle {
- x: bounds.x + bounds.width
- - total_scrollbar_width / 2.0
- - scroller_width / 2.0,
- y: (scrollbar_bounds.y + scroller_offset).max(0.0),
- width: scroller_width,
- height: scroller_height,
+ let scroller = if ratio >= 1.0 {
+ None
+ } else {
+ // min height for easier grabbing with super tall content
+ let scroller_height =
+ (scrollbar_bounds.height * ratio).max(2.0);
+ let scroller_offset =
+ translation.y * ratio * scrollbar_bounds.height
+ / bounds.height;
+
+ let scroller_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - total_scrollbar_width / 2.0
+ - scroller_width / 2.0,
+ y: (scrollbar_bounds.y + scroller_offset).max(0.0),
+ width: scroller_width,
+ height: scroller_height,
+ };
+
+ Some(internals::Scroller {
+ bounds: scroller_bounds,
+ })
};
Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: internals::Scroller {
- bounds: scroller_bounds,
- },
+ scroller,
alignment: vertical.alignment,
})
} else {
@@ -1368,7 +1596,7 @@ impl Scrollbars {
};
let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
- let Properties {
+ let Scrollbar {
width,
margin,
scroller_width,
@@ -1402,26 +1630,34 @@ impl Scrollbars {
};
let ratio = bounds.width / content_bounds.width;
- // min width for easier grabbing with extra wide content
- let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
- let scroller_offset =
- translation.x * ratio * scrollbar_bounds.width / bounds.width;
- let scroller_bounds = Rectangle {
- x: (scrollbar_bounds.x + scroller_offset).max(0.0),
- y: bounds.y + bounds.height
- - total_scrollbar_height / 2.0
- - scroller_width / 2.0,
- width: scroller_length,
- height: scroller_width,
+ let scroller = if ratio >= 1.0 {
+ None
+ } else {
+ // min width for easier grabbing with extra wide content
+ let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
+ let scroller_offset =
+ translation.x * ratio * scrollbar_bounds.width
+ / bounds.width;
+
+ let scroller_bounds = Rectangle {
+ x: (scrollbar_bounds.x + scroller_offset).max(0.0),
+ y: bounds.y + bounds.height
+ - total_scrollbar_height / 2.0
+ - scroller_width / 2.0,
+ width: scroller_length,
+ height: scroller_width,
+ };
+
+ Some(internals::Scroller {
+ bounds: scroller_bounds,
+ })
};
Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: internals::Scroller {
- bounds: scroller_bounds,
- },
+ scroller,
alignment: horizontal.alignment,
})
} else {
@@ -1452,33 +1688,33 @@ impl Scrollbars {
}
fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
- self.y.and_then(|scrollbar| {
- if scrollbar.total_bounds.contains(cursor_position) {
- Some(if scrollbar.scroller.bounds.contains(cursor_position) {
- (cursor_position.y - scrollbar.scroller.bounds.y)
- / scrollbar.scroller.bounds.height
- } else {
- 0.5
- })
+ let scrollbar = self.y?;
+ let scroller = scrollbar.scroller?;
+
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scroller.bounds.contains(cursor_position) {
+ (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
} else {
- None
- }
- })
+ 0.5
+ })
+ } else {
+ None
+ }
}
fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
- self.x.and_then(|scrollbar| {
- if scrollbar.total_bounds.contains(cursor_position) {
- Some(if scrollbar.scroller.bounds.contains(cursor_position) {
- (cursor_position.x - scrollbar.scroller.bounds.x)
- / scrollbar.scroller.bounds.width
- } else {
- 0.5
- })
+ let scrollbar = self.x?;
+ let scroller = scrollbar.scroller?;
+
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scroller.bounds.contains(cursor_position) {
+ (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
} else {
- None
- }
- })
+ 0.5
+ })
+ } else {
+ None
+ }
}
fn active(&self) -> bool {
@@ -1489,14 +1725,14 @@ impl Scrollbars {
pub(super) mod internals {
use crate::core::{Point, Rectangle};
- use super::Alignment;
+ use super::Anchor;
#[derive(Debug, Copy, Clone)]
pub struct Scrollbar {
pub total_bounds: Rectangle,
pub bounds: Rectangle,
- pub scroller: Scroller,
- pub alignment: Alignment,
+ pub scroller: Option<Scroller>,
+ pub alignment: Anchor,
}
impl Scrollbar {
@@ -1511,14 +1747,18 @@ pub(super) mod internals {
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- let percentage = (cursor_position.y
- - self.bounds.y
- - self.scroller.bounds.height * grabbed_at)
- / (self.bounds.height - self.scroller.bounds.height);
-
- match self.alignment {
- Alignment::Start => percentage,
- Alignment::End => 1.0 - percentage,
+ if let Some(scroller) = self.scroller {
+ let percentage = (cursor_position.y
+ - self.bounds.y
+ - scroller.bounds.height * grabbed_at)
+ / (self.bounds.height - scroller.bounds.height);
+
+ match self.alignment {
+ Anchor::Start => percentage,
+ Anchor::End => 1.0 - percentage,
+ }
+ } else {
+ 0.0
}
}
@@ -1528,14 +1768,18 @@ pub(super) mod internals {
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- let percentage = (cursor_position.x
- - self.bounds.x
- - self.scroller.bounds.width * grabbed_at)
- / (self.bounds.width - self.scroller.bounds.width);
-
- match self.alignment {
- Alignment::Start => percentage,
- Alignment::End => 1.0 - percentage,
+ if let Some(scroller) = self.scroller {
+ let percentage = (cursor_position.x
+ - self.bounds.x
+ - scroller.bounds.width * grabbed_at)
+ / (self.bounds.width - scroller.bounds.width);
+
+ match self.alignment {
+ Anchor::Start => percentage,
+ Anchor::End => 1.0 - percentage,
+ }
+ } else {
+ 0.0
}
}
}
@@ -1569,22 +1813,22 @@ pub enum Status {
},
}
-/// The appearance of a scrolable.
+/// The appearance of a scrollable.
#[derive(Debug, Clone, Copy)]
pub struct Style {
/// The [`container::Style`] of a scrollable.
pub container: container::Style,
- /// The vertical [`Scrollbar`] appearance.
- pub vertical_scrollbar: Scrollbar,
- /// The horizontal [`Scrollbar`] appearance.
- pub horizontal_scrollbar: Scrollbar,
+ /// The vertical [`Rail`] appearance.
+ pub vertical_rail: Rail,
+ /// The horizontal [`Rail`] appearance.
+ pub horizontal_rail: Rail,
/// 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 {
+pub struct Rail {
/// The [`Background`] of a scrollbar.
pub background: Option<Background>,
/// The [`Border`] of a scrollbar.
@@ -1633,27 +1877,27 @@ impl Catalog for Theme {
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let scrollbar = Scrollbar {
+ let scrollbar = Rail {
background: Some(palette.background.weak.color.into()),
- border: Border::rounded(2),
+ border: border::rounded(2),
scroller: Scroller {
color: palette.background.strong.color,
- border: Border::rounded(2),
+ border: border::rounded(2),
},
};
match status {
Status::Active => Style {
container: container::Style::default(),
- vertical_scrollbar: scrollbar,
- horizontal_scrollbar: scrollbar,
+ vertical_rail: scrollbar,
+ horizontal_rail: scrollbar,
gap: None,
},
Status::Hovered {
is_horizontal_scrollbar_hovered,
is_vertical_scrollbar_hovered,
} => {
- let hovered_scrollbar = Scrollbar {
+ let hovered_scrollbar = Rail {
scroller: Scroller {
color: palette.primary.strong.color,
..scrollbar.scroller
@@ -1663,12 +1907,12 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
container: container::Style::default(),
- vertical_scrollbar: if is_vertical_scrollbar_hovered {
+ vertical_rail: if is_vertical_scrollbar_hovered {
hovered_scrollbar
} else {
scrollbar
},
- horizontal_scrollbar: if is_horizontal_scrollbar_hovered {
+ horizontal_rail: if is_horizontal_scrollbar_hovered {
hovered_scrollbar
} else {
scrollbar
@@ -1680,7 +1924,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
is_horizontal_scrollbar_dragged,
is_vertical_scrollbar_dragged,
} => {
- let dragged_scrollbar = Scrollbar {
+ let dragged_scrollbar = Rail {
scroller: Scroller {
color: palette.primary.base.color,
..scrollbar.scroller
@@ -1690,12 +1934,12 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
container: container::Style::default(),
- vertical_scrollbar: if is_vertical_scrollbar_dragged {
+ vertical_rail: if is_vertical_scrollbar_dragged {
dragged_scrollbar
} else {
scrollbar
},
- horizontal_scrollbar: if is_horizontal_scrollbar_dragged {
+ horizontal_rail: if is_horizontal_scrollbar_dragged {
dragged_scrollbar
} else {
scrollbar