Skip to content

Commit 130f7e5

Browse files
committed
feat: Add smooth scroll to Flickable
1 parent c5ff907 commit 130f7e5

File tree

13 files changed

+92
-62
lines changed

13 files changed

+92
-62
lines changed

internal/compiler/widgets/cosmic/scrollview.slint

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ export component ScrollBar {
1313
in-out property <length> value;
1414
in property <ScrollBarPolicy> policy: ScrollBarPolicy.as-needed;
1515

16-
callback scrolled();
17-
1816
private property <length> track-size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset;
1917
private property <length> step-size: 10px;
2018
private property <length> offset: 2px;
@@ -65,7 +63,6 @@ export component ScrollBar {
6563
root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width))
6664
: (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height))
6765
)));
68-
root.scrolled();
6966
}
7067
}
7168

@@ -125,8 +122,6 @@ export component ScrollView {
125122
horizontal: false;
126123
maximum: flickable.viewport-height - flickable.height;
127124
page-size: flickable.height;
128-
129-
scrolled => {root.scrolled()}
130125
}
131126

132127
horizontal-bar := ScrollBar {
@@ -138,7 +133,5 @@ export component ScrollView {
138133
horizontal: true;
139134
maximum: flickable.viewport-width - flickable.width;
140135
page-size: flickable.width;
141-
142-
scrolled => {root.scrolled()}
143136
}
144137
}

internal/compiler/widgets/cupertino/scrollview.slint

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ export component ScrollBar inherits Rectangle {
1212
in-out property <length> value;
1313
in property <ScrollBarPolicy> policy: ScrollBarPolicy.as-needed;
1414

15-
callback scrolled();
16-
1715
property <length> track-size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset;
1816
property <length> step-size: 10px;
1917
property <length> offset: 2px;
@@ -73,7 +71,6 @@ export component ScrollBar inherits Rectangle {
7371
root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width))
7472
: (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height))
7573
)));
76-
root.scrolled();
7774
}
7875
}
7976

@@ -135,8 +132,6 @@ export component ScrollView {
135132
horizontal: false;
136133
maximum: flickable.viewport-height - flickable.height;
137134
page-size: flickable.height;
138-
139-
scrolled => {root.scrolled()}
140135
}
141136

142137
horizontal-bar := ScrollBar {
@@ -148,7 +143,5 @@ export component ScrollView {
148143
horizontal: true;
149144
maximum: flickable.viewport-width - flickable.width;
150145
page-size: flickable.width;
151-
152-
scrolled => {root.scrolled()}
153146
}
154147
}

internal/compiler/widgets/fluent/scrollview.slint

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ component ScrollBar inherits Rectangle {
4242
in property <ScrollBarPolicy> policy: ScrollBarPolicy.as-needed;
4343
in property <bool> enabled;
4444

45-
callback scrolled();
46-
4745
property <length> offset: 16px;
4846
property <length> size: 2px;
4947
property <length> track-size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset;
@@ -94,7 +92,6 @@ component ScrollBar inherits Rectangle {
9492
root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width))
9593
: (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height))
9694
)));
97-
root.scrolled();
9895
}
9996
}
10097

@@ -178,8 +175,6 @@ export component ScrollView {
178175
horizontal: false;
179176
maximum: flickable.viewport-height - flickable.height;
180177
page-size: flickable.height;
181-
182-
scrolled => {root.scrolled()}
183178
}
184179

185180
horizontal-bar := ScrollBar {
@@ -191,7 +186,5 @@ export component ScrollView {
191186
horizontal: true;
192187
maximum: flickable.viewport-width - flickable.width;
193188
page-size: flickable.width;
194-
195-
scrolled => {root.scrolled()}
196189
}
197190
}

internal/compiler/widgets/material/scrollview.slint

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ component ScrollBar inherits Rectangle {
1313
in-out property <bool> enabled <=> touch-area.enabled;
1414
in property <ScrollBarPolicy> policy: ScrollBarPolicy.as-needed;
1515

16-
callback scrolled();
17-
1816
states [
1917
disabled when !touch-area.enabled : {
2018
background.border-color: MaterialPalette.control-foreground;
@@ -75,7 +73,6 @@ component ScrollBar inherits Rectangle {
7573
root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.width - handle.width))
7674
: (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.height - handle.height))
7775
)));
78-
root.scrolled();
7976
}
8077
}
8178

@@ -135,8 +132,6 @@ export component ScrollView {
135132
maximum: flickable.viewport-height - flickable.height;
136133
page-size: flickable.height;
137134
enabled: root.enabled;
138-
139-
scrolled => {root.scrolled()}
140135
}
141136

142137
horizontal-bar := ScrollBar {
@@ -148,7 +143,5 @@ export component ScrollView {
148143
maximum: flickable.viewport-width - flickable.width;
149144
page-size: flickable.width;
150145
enabled: root.enabled;
151-
152-
scrolled => {root.scrolled()}
153146
}
154147
}

internal/core/items/flickable.rs

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::input::{
1414
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent, MouseEvent,
1515
};
1616
use crate::item_rendering::CachedRenderingData;
17-
use crate::items::PropertyAnimation;
17+
use crate::items::{AnimationDirection, PropertyAnimation};
1818
use crate::layout::{LayoutInfo, Orientation};
1919
use crate::lengths::{
2020
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
@@ -92,6 +92,26 @@ impl Item for Flickable {
9292
}
9393
},
9494
);
95+
96+
self.data.viewport_change_handler.init_delayed(
97+
self_rc.downgrade(),
98+
|self_weak| {
99+
let Some(flick_rc) = self_weak.upgrade() else { return Default::default() };
100+
let Some(flick) = flick_rc.downcast::<Flickable>() else {
101+
return Default::default();
102+
};
103+
let flick = flick.as_pin_ref();
104+
105+
(flick.viewport_x().get(), flick.viewport_y().get())
106+
},
107+
|self_weak, _| {
108+
let Some(flick_rc) = self_weak.upgrade() else { return };
109+
let Some(flick) = flick_rc.downcast::<Flickable>() else { return };
110+
let flick = flick.as_pin_ref();
111+
112+
flick.flicked.call(&())
113+
},
114+
);
95115
}
96116

97117
fn layout_info(
@@ -242,6 +262,15 @@ pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
242262
/// The delay to which press are forwarded to the inner item
243263
pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
244264

265+
const SMOOTH_SCROLL_DURATION: i32 = 250;
266+
const SMOOTH_SCROLL_ANIM: PropertyAnimation = PropertyAnimation {
267+
duration: SMOOTH_SCROLL_DURATION,
268+
easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]),
269+
delay: 0,
270+
iteration_count: 1.,
271+
direction: AnimationDirection::Normal,
272+
};
273+
245274
#[derive(Default, Debug)]
246275
struct FlickableDataInner {
247276
/// The position in which the press was made
@@ -250,13 +279,17 @@ struct FlickableDataInner {
250279
pressed_viewport_pos: LogicalPoint,
251280
/// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children
252281
capture_events: bool,
282+
smooth_scroll_time: Option<Instant>,
283+
smooth_scroll_target: LogicalPoint,
253284
}
254285

255286
#[derive(Default, Debug)]
256287
pub struct FlickableData {
257288
inner: RefCell<FlickableDataInner>,
258289
/// Tracker that tracks the property to make sure that the flickable is in bounds
259290
in_bound_change_handler: crate::properties::ChangeTracker,
291+
// Scroll trackers for flicked callback
292+
viewport_change_handler: crate::properties::ChangeTracker,
260293
}
261294

262295
impl FlickableData {
@@ -372,12 +405,8 @@ impl FlickableData {
372405
if inner.capture_events || should_capture() {
373406
let new_pos = ensure_in_bound(flick, new_pos, flick_rc);
374407

375-
let old_pos = (x.get(), y.get());
376408
x.set(new_pos.x_length());
377409
y.set(new_pos.y_length());
378-
if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
379-
(Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
380-
}
381410

382411
inner.capture_events = true;
383412
InputEventResult::GrabMouse
@@ -395,7 +424,7 @@ impl FlickableData {
395424
}
396425
}
397426
MouseEvent::Wheel { delta_x, delta_y, .. } => {
398-
let delta = if window_adapter.window().0.modifiers.get().shift()
427+
let mut delta = if window_adapter.window().0.modifiers.get().shift()
399428
&& !cfg!(target_os = "macos")
400429
{
401430
// Shift invert coordinate for the purpose of scrolling. But not on macOs because there the OS already take care of the change
@@ -413,20 +442,36 @@ impl FlickableData {
413442
return InputEventResult::EventIgnored;
414443
}
415444

416-
let old_pos = LogicalPoint::from_lengths(
417-
(Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
418-
(Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
419-
);
420-
let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc);
421-
422445
let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
423446
let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
424-
let old_pos = (viewport_x.get(), viewport_y.get());
425-
viewport_x.set(new_pos.x_length());
426-
viewport_y.set(new_pos.y_length());
427-
if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
428-
(Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
447+
448+
let old_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get());
449+
450+
// Accumulate scroll delta
451+
if let Some(smooth_scroll_time) = inner.smooth_scroll_time.take() {
452+
let millis =
453+
(crate::animations::current_tick() - smooth_scroll_time).as_millis() as i32;
454+
455+
if millis < SMOOTH_SCROLL_DURATION {
456+
let remaining_delta = inner.smooth_scroll_target - old_pos;
457+
458+
// Only if is in the same direction.
459+
// `Default` is because `dot` returns `i32` in embedded
460+
// but it returns `f32` in any other platform
461+
if delta.dot(remaining_delta) > Default::default() {
462+
delta += remaining_delta;
463+
}
464+
}
429465
}
466+
467+
let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc);
468+
469+
inner.smooth_scroll_target = new_pos;
470+
inner.smooth_scroll_time = Some(Instant::now());
471+
472+
viewport_y.set_animated_value(new_pos.y_length(), SMOOTH_SCROLL_ANIM);
473+
viewport_x.set_animated_value(new_pos.x_length(), SMOOTH_SCROLL_ANIM);
474+
430475
InputEventResult::EventAccepted
431476
}
432477
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
@@ -449,26 +494,19 @@ impl FlickableData {
449494
{
450495
let speed = dist / (millis as f32);
451496

452-
let duration = 250;
453497
let final_pos = ensure_in_bound(
454498
flick,
455-
(inner.pressed_viewport_pos.cast() + dist + speed * (duration as f32)).cast(),
499+
(inner.pressed_viewport_pos.cast()
500+
+ dist
501+
+ speed * (SMOOTH_SCROLL_DURATION as f32))
502+
.cast(),
456503
flick_rc,
457504
);
458-
let anim = PropertyAnimation {
459-
duration,
460-
easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]),
461-
..PropertyAnimation::default()
462-
};
463505

464506
let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
465507
let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
466-
let old_pos = (viewport_x.get(), viewport_y.get());
467-
viewport_x.set_animated_value(final_pos.x_length(), anim.clone());
468-
viewport_y.set_animated_value(final_pos.y_length(), anim);
469-
if old_pos.0 != final_pos.x_length() || old_pos.1 != final_pos.y_length() {
470-
(Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
471-
}
508+
viewport_x.set_animated_value(final_pos.x_length(), SMOOTH_SCROLL_ANIM);
509+
viewport_y.set_animated_value(final_pos.y_length(), SMOOTH_SCROLL_ANIM);
472510
}
473511
}
474512
inner.capture_events = false; // FIXME: should only be set to false once the flick animation is over

internal/core/model.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,9 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
12811281
viewport_height.set(inner.cached_item_height * row_count as Coord);
12821282
viewport_width.set(vp_width);
12831283
let new_viewport_y = -inner.anchor_y + new_offset_y;
1284-
viewport_y.set(new_viewport_y);
1284+
if viewport_y.get() != new_viewport_y {
1285+
viewport_y.set(new_viewport_y);
1286+
}
12851287
inner.previous_viewport_y = new_viewport_y;
12861288
break;
12871289
}

tests/cases/elements/flickable.slint

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ TestCase := Window {
4444
property<bool> inner_ta_has_hover: inner_ta.has_hover;
4545
property<int> clicked;
4646
property<int> double-clicked;
47-
property <int> flicked;
47+
property<int> flicked;
4848
}
4949

5050
/*
@@ -233,6 +233,7 @@ assert!((instance.get_offset_y() - 55.).abs() < 5.);
233233
use slint::{LogicalPosition, platform::{WindowEvent, Key} };
234234
let instance = TestCase::new().unwrap();
235235
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: -30.0, delta_y: -50.0 });
236+
slint_testing::mock_elapsed_time(250);
236237
assert_eq!(instance.get_offset_x(), 30.);
237238
assert_eq!(instance.get_offset_y(), 50.);
238239
@@ -242,6 +243,7 @@ if !cfg!(target_os = "macos") {
242243
slint_testing::send_keyboard_char(&instance, Key::Shift.into(), true);
243244
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: 15.0, delta_y: -60.0 });
244245
slint_testing::send_keyboard_char(&instance, Key::Shift.into(), false);
246+
slint_testing::mock_elapsed_time(250);
245247
assert_eq!(instance.get_offset_x(), 30. + 60.);
246248
assert_eq!(instance.get_offset_y(), 50. - 15.);
247249
}
@@ -255,6 +257,7 @@ assert_eq!(instance.get_flicked(), 0);
255257
256258
// test scrolling behaviour
257259
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: -30.0, delta_y: -50.0 });
260+
slint_testing::mock_elapsed_time(250);
258261
dbg!(instance.get_flicked());
259262
assert_eq!(instance.get_flicked(), -3000050); //flicked got called after scrolling
260263
instance.set_flicked(0);
@@ -268,7 +271,8 @@ slint_testing::mock_elapsed_time(10000);
268271
assert_eq!(instance.get_flicked(), -10500105); //flicked got called during drag
269272
instance.set_flicked(0);
270273
instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(100.0, 120.0), button: PointerEventButton::Left });
271-
assert_eq!(instance.get_flicked(), -10500105); //flicked got called after drag
274+
slint_testing::mock_elapsed_time(250);
275+
assert_eq!(instance.get_flicked(), -10682145); //flicked got called after drag
272276
instance.set_flicked(0);
273277
274278
```

tests/cases/elements/flickable2.slint

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ TestCase := Window {
4747
}
4848
}
4949

50-
Flickable { for i in 5: Rectangle {} }
50+
Flickable {
51+
for i in 5: Rectangle {}
52+
}
5153

5254
property<bool> all_ok: r1.ok && r2.ok && r3.ok && r4.ok;
5355
property<bool> test: all_ok;

tests/cases/elements/flickable3.slint

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,14 @@ assert_eq!(instance.get_t1_has_hover(), true);
6969
assert_eq!(instance.get_t1sec_has_hover(), false);
7070
assert_eq!(instance.get_t2_has_hover(), false);
7171
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 25.0), delta_x: 0.0, delta_y: -30.0 });
72+
slint_testing::mock_elapsed_time(250);
7273
assert_eq!(instance.get_t1_has_hover(), false);
7374
assert_eq!(instance.get_t1sec_has_hover(), true);
7475
assert_eq!(instance.get_t2_has_hover(), false);
7576
assert_eq!(instance.get_f1_pos(), -30.0);
7677
7778
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 25.0), delta_x: 0.0, delta_y: -30.0 });
79+
slint_testing::mock_elapsed_time(250);
7880
assert_eq!(instance.get_t1_has_hover(), false);
7981
assert_eq!(instance.get_t1sec_has_hover(), false);
8082
assert_eq!(instance.get_t2_has_hover(), false);
@@ -88,6 +90,7 @@ instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPo
8890
assert_eq!(instance.get_t2_has_hover(), true);
8991
assert_eq!(instance.get_t1_has_hover(), false);
9092
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(275.0, 25.0), delta_x: -30.0, delta_y: 0.0 });
93+
slint_testing::mock_elapsed_time(250);
9194
assert_eq!(instance.get_t2_has_hover(), false);
9295
assert_eq!(instance.get_t1_has_hover(), false);
9396

0 commit comments

Comments
 (0)