Implement scroll_by operation for scrollable
`scroll_by` allows scrolling an absolute offset that is applied to the current scrolling position.
This commit is contained in:
parent
44235f0c0b
commit
e102e89c6a
5 changed files with 157 additions and 36 deletions
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub trait Operation<T = ()>: Send {
|
|||
_state: &mut dyn Scrollable,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
) {
|
||||
}
|
||||
|
|
@ -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>) {
|
||||
|
|
@ -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>) {
|
||||
|
|
@ -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(
|
||||
|
|
@ -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>) {
|
||||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -430,6 +448,7 @@ where
|
|||
state,
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
);
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
@ -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,
|
||||
|
|
@ -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)]
|
||||
|
|
@ -1313,34 +1352,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: 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),
|
||||
);
|
||||
}
|
||||
|
|
@ -1348,7 +1366,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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue