Skip to content

Commit

Permalink
Implement scroll_by operation for scrollable
Browse files Browse the repository at this point in the history
`scroll_by` allows scrolling an absolute offset
that is applied to the current scrolling position.
  • Loading branch information
lufte authored and hecrj committed Sep 10, 2024
1 parent 44235f0 commit e102e89
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 36 deletions.
11 changes: 11 additions & 0 deletions core/src/vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ impl Vector {
pub const ZERO: Self = Self::new(0.0, 0.0);
}

impl<T> std::ops::Neg for Vector<T>
where
T: std::ops::Neg<Output = T>,
{
type Output = Self;

fn neg(self) -> Self::Output {
Self::new(-self.x, -self.y)
}
}

impl<T> std::ops::Add for Vector<T>
where
T: std::ops::Add<Output = T>,
Expand Down
46 changes: 41 additions & 5 deletions core/src/widget/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub trait Operation<T = ()>: Send {
_state: &mut dyn Scrollable,
_id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
) {
}
Expand Down Expand Up @@ -76,9 +77,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
) {
self.as_mut().scrollable(state, id, bounds, translation);
self.as_mut().scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
Expand Down Expand Up @@ -151,9 +159,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
self.operation.scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
Expand Down Expand Up @@ -222,9 +237,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
self.operation.scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn focusable(
Expand Down Expand Up @@ -262,9 +284,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
self.operation.scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
Expand Down Expand Up @@ -341,9 +370,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: crate::Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
self.operation.scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
Expand Down
45 changes: 45 additions & 0 deletions core/src/widget/operation/scrollable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ pub trait Scrollable {

/// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
fn scroll_to(&mut self, offset: AbsoluteOffset);

/// Scroll the widget by the given [`AbsoluteOffset`] along the horizontal & vertical axis.
fn scroll_by(
&mut self,
offset: AbsoluteOffset,
bounds: Rectangle,
content_bounds: Rectangle,
);
}

/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
Expand All @@ -34,6 +42,7 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
Expand Down Expand Up @@ -68,6 +77,7 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
Expand All @@ -79,6 +89,41 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
ScrollTo { target, offset }
}

/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by
/// the provided [`AbsoluteOffset`].
pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
struct ScrollBy {
target: Id,
offset: AbsoluteOffset,
}

impl<T> Operation<T> for ScrollBy {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
}

fn scrollable(
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
state.scroll_by(self.offset, bounds, content_bounds);
}
}
}

ScrollBy { target, offset }
}

/// The amount of absolute offset in each direction of a [`Scrollable`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct AbsoluteOffset {
Expand Down
1 change: 1 addition & 0 deletions widget/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
_state: &mut dyn widget::operation::Scrollable,
_id: Option<&widget::Id>,
bounds: Rectangle,
_content_bounds: Rectangle,
translation: Vector,
) {
match self.scrollables.last() {
Expand Down
90 changes: 59 additions & 31 deletions widget/src/scrollable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,24 @@ impl Direction {
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 {
Expand Down Expand Up @@ -430,6 +448,7 @@ where
state,
self.id.as_ref().map(|id| &id.0),
bounds,
content_bounds,
translation,
);

Expand Down Expand Up @@ -729,12 +748,16 @@ where
};

// TODO: Configurable speed/friction (?)
movement * 60.0
-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,
);

if notify_on_scroll(
state,
Expand Down Expand Up @@ -770,13 +793,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,
);
Expand Down Expand Up @@ -1110,19 +1132,27 @@ 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)))
}

/// 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(
id.0, offset,
)))
}

/// 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,
)))
}

/// Returns [`true`] if the viewport actually changed.
fn notify_on_scroll<Message>(
state: &mut State,
Expand Down Expand Up @@ -1210,6 +1240,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)]
Expand Down Expand Up @@ -1313,42 +1352,21 @@ 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: Anchor, delta: f32| match alignment {
Anchor::Start => delta,
Anchor::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),
);
}

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),
);
}
Expand Down Expand Up @@ -1394,6 +1412,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) {
Expand Down

0 comments on commit e102e89

Please sign in to comment.