Skip to content

Commit 4052ccf

Browse files
authored
Merge pull request #1796 from tarkah/feat/scrollable-scroll-to
Add `scroll_to` operation
2 parents 2155d0a + 8a71140 commit 4052ccf

4 files changed

Lines changed: 116 additions & 33 deletions

File tree

examples/scrollable/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ enum Message {
3636
ScrollerWidthChanged(u16),
3737
ScrollToBeginning,
3838
ScrollToEnd,
39-
Scrolled(scrollable::RelativeOffset),
39+
Scrolled(scrollable::Viewport),
4040
}
4141

4242
impl Application for ScrollableDemo {
@@ -104,8 +104,8 @@ impl Application for ScrollableDemo {
104104
self.current_scroll_offset,
105105
)
106106
}
107-
Message::Scrolled(offset) => {
108-
self.current_scroll_offset = offset;
107+
Message::Scrolled(viewport) => {
108+
self.current_scroll_offset = viewport.relative_offset();
109109

110110
Command::none()
111111
}

native/src/widget/operation/scrollable.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ use crate::widget::{Id, Operation};
55
pub trait Scrollable {
66
/// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis.
77
fn snap_to(&mut self, offset: RelativeOffset);
8+
9+
/// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
10+
fn scroll_to(&mut self, offset: AbsoluteOffset);
811
}
912

1013
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
@@ -34,7 +37,43 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
3437
SnapTo { target, offset }
3538
}
3639

37-
/// The amount of offset in each direction of a [`Scrollable`].
40+
/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] to
41+
/// the provided [`AbsoluteOffset`].
42+
pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
43+
struct ScrollTo {
44+
target: Id,
45+
offset: AbsoluteOffset,
46+
}
47+
48+
impl<T> Operation<T> for ScrollTo {
49+
fn container(
50+
&mut self,
51+
_id: Option<&Id>,
52+
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
53+
) {
54+
operate_on_children(self)
55+
}
56+
57+
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
58+
if Some(&self.target) == id {
59+
state.scroll_to(self.offset);
60+
}
61+
}
62+
}
63+
64+
ScrollTo { target, offset }
65+
}
66+
67+
/// The amount of absolute offset in each direction of a [`Scrollable`].
68+
#[derive(Debug, Clone, Copy, PartialEq, Default)]
69+
pub struct AbsoluteOffset {
70+
/// The amount of horizontal offset
71+
pub x: f32,
72+
/// The amount of vertical offset
73+
pub y: f32,
74+
}
75+
76+
/// The amount of relative offset in each direction of a [`Scrollable`].
3877
///
3978
/// A value of `0.0` means start, while `1.0` means end.
4079
#[derive(Debug, Clone, Copy, PartialEq, Default)]

native/src/widget/scrollable.rs

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
};
1616

1717
pub use iced_style::scrollable::StyleSheet;
18-
pub use operation::scrollable::RelativeOffset;
18+
pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
1919

2020
pub mod style {
2121
//! The styles of a [`Scrollable`].
@@ -38,7 +38,7 @@ where
3838
vertical: Properties,
3939
horizontal: Option<Properties>,
4040
content: Element<'a, Message, Renderer>,
41-
on_scroll: Option<Box<dyn Fn(RelativeOffset) -> Message + 'a>>,
41+
on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
4242
style: <Renderer::Theme as StyleSheet>::Style,
4343
}
4444

@@ -93,12 +93,8 @@ where
9393

9494
/// Sets a function to call when the [`Scrollable`] is scrolled.
9595
///
96-
/// The function takes the new relative x & y offset of the [`Scrollable`]
97-
/// (e.g. `0` means beginning, while `1` means end).
98-
pub fn on_scroll(
99-
mut self,
100-
f: impl Fn(RelativeOffset) -> Message + 'a,
101-
) -> Self {
96+
/// The function takes the [`Viewport`] of the [`Scrollable`]
97+
pub fn on_scroll(mut self, f: impl Fn(Viewport) -> Message + 'a) -> Self {
10298
self.on_scroll = Some(Box::new(f));
10399
self
104100
}
@@ -436,7 +432,7 @@ pub fn update<Message>(
436432
shell: &mut Shell<'_, Message>,
437433
vertical: &Properties,
438434
horizontal: Option<&Properties>,
439-
on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
435+
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
440436
update_content: impl FnOnce(
441437
Event,
442438
Layout<'_>,
@@ -896,7 +892,7 @@ pub fn draw<Renderer>(
896892

897893
fn notify_on_scroll<Message>(
898894
state: &mut State,
899-
on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
895+
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
900896
bounds: Rectangle,
901897
content_bounds: Rectangle,
902898
shell: &mut Shell<'_, Message>,
@@ -908,31 +904,36 @@ fn notify_on_scroll<Message>(
908904
return;
909905
}
910906

911-
let x = state.offset_x.absolute(bounds.width, content_bounds.width)
912-
/ (content_bounds.width - bounds.width);
907+
let viewport = Viewport {
908+
offset_x: state.offset_x,
909+
offset_y: state.offset_y,
910+
bounds,
911+
content_bounds,
912+
};
913913

914-
let y = state
915-
.offset_y
916-
.absolute(bounds.height, content_bounds.height)
917-
/ (content_bounds.height - bounds.height);
914+
// Don't publish redundant viewports to shell
915+
if let Some(last_notified) = state.last_notified {
916+
let last_relative_offset = last_notified.relative_offset();
917+
let current_relative_offset = viewport.relative_offset();
918918

919-
let new_offset = RelativeOffset { x, y };
919+
let last_absolute_offset = last_notified.absolute_offset();
920+
let current_absolute_offset = viewport.absolute_offset();
920921

921-
// Don't publish redundant offsets to shell
922-
if let Some(prev_offset) = state.last_notified {
923922
let unchanged = |a: f32, b: f32| {
924923
(a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
925924
};
926925

927-
if unchanged(prev_offset.x, new_offset.x)
928-
&& unchanged(prev_offset.y, new_offset.y)
926+
if unchanged(last_relative_offset.x, current_relative_offset.x)
927+
&& unchanged(last_relative_offset.y, current_relative_offset.y)
928+
&& unchanged(last_absolute_offset.x, current_absolute_offset.x)
929+
&& unchanged(last_absolute_offset.y, current_absolute_offset.y)
929930
{
930931
return;
931932
}
932933
}
933934

934-
shell.publish(on_scroll(new_offset));
935-
state.last_notified = Some(new_offset);
935+
shell.publish(on_scroll(viewport));
936+
state.last_notified = Some(viewport);
936937
}
937938
}
938939

@@ -945,7 +946,7 @@ pub struct State {
945946
offset_x: Offset,
946947
x_scroller_grabbed_at: Option<f32>,
947948
keyboard_modifiers: keyboard::Modifiers,
948-
last_notified: Option<RelativeOffset>,
949+
last_notified: Option<Viewport>,
949950
}
950951

951952
impl Default for State {
@@ -966,6 +967,10 @@ impl operation::Scrollable for State {
966967
fn snap_to(&mut self, offset: RelativeOffset) {
967968
State::snap_to(self, offset);
968969
}
970+
971+
fn scroll_to(&mut self, offset: AbsoluteOffset) {
972+
State::scroll_to(self, offset)
973+
}
969974
}
970975

971976
#[derive(Debug, Clone, Copy)]
@@ -975,18 +980,51 @@ enum Offset {
975980
}
976981

977982
impl Offset {
978-
fn absolute(self, window: f32, content: f32) -> f32 {
983+
fn absolute(self, viewport: f32, content: f32) -> f32 {
979984
match self {
980985
Offset::Absolute(absolute) => {
981-
absolute.min((content - window).max(0.0))
986+
absolute.min((content - viewport).max(0.0))
982987
}
983988
Offset::Relative(percentage) => {
984-
((content - window) * percentage).max(0.0)
989+
((content - viewport) * percentage).max(0.0)
985990
}
986991
}
987992
}
988993
}
989994

995+
/// The current [`Viewport`] of the [`Scrollable`].
996+
#[derive(Debug, Clone, Copy)]
997+
pub struct Viewport {
998+
offset_x: Offset,
999+
offset_y: Offset,
1000+
bounds: Rectangle,
1001+
content_bounds: Rectangle,
1002+
}
1003+
1004+
impl Viewport {
1005+
/// Returns the [`AbsoluteOffset`] of the current [`Viewport`].
1006+
pub fn absolute_offset(&self) -> AbsoluteOffset {
1007+
let x = self
1008+
.offset_x
1009+
.absolute(self.bounds.width, self.content_bounds.width);
1010+
let y = self
1011+
.offset_y
1012+
.absolute(self.bounds.height, self.content_bounds.height);
1013+
1014+
AbsoluteOffset { x, y }
1015+
}
1016+
1017+
/// Returns the [`RelativeOffset`] of the current [`Viewport`].
1018+
pub fn relative_offset(&self) -> RelativeOffset {
1019+
let AbsoluteOffset { x, y } = self.absolute_offset();
1020+
1021+
let x = x / (self.content_bounds.width - self.bounds.width);
1022+
let y = y / (self.content_bounds.height - self.bounds.height);
1023+
1024+
RelativeOffset { x, y }
1025+
}
1026+
}
1027+
9901028
impl State {
9911029
/// Creates a new [`State`] with the scrollbar(s) at the beginning.
9921030
pub fn new() -> Self {
@@ -1052,6 +1090,12 @@ impl State {
10521090
self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
10531091
}
10541092

1093+
/// Scroll to the provided [`AbsoluteOffset`].
1094+
pub fn scroll_to(&mut self, offset: AbsoluteOffset) {
1095+
self.offset_x = Offset::Absolute(offset.x.max(0.0));
1096+
self.offset_y = Offset::Absolute(offset.y.max(0.0));
1097+
}
1098+
10551099
/// Unsnaps the current scroll position, if snapped, given the bounds of the
10561100
/// [`Scrollable`] and its contents.
10571101
pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {

src/widget.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ pub mod radio {
109109
pub mod scrollable {
110110
//! Navigate an endless amount of content with a scrollbar.
111111
pub use iced_native::widget::scrollable::{
112-
snap_to, style::Scrollbar, style::Scroller, Id, Properties,
113-
RelativeOffset, StyleSheet,
112+
snap_to, style::Scrollbar, style::Scroller, AbsoluteOffset, Id,
113+
Properties, RelativeOffset, StyleSheet, Viewport,
114114
};
115115

116116
/// A widget that can vertically display an infinite amount of content

0 commit comments

Comments
 (0)