diff --git a/internal/compiler/widgets/cosmic/scrollview.slint b/internal/compiler/widgets/cosmic/scrollview.slint index 168602859d5..ed552ce902c 100644 --- a/internal/compiler/widgets/cosmic/scrollview.slint +++ b/internal/compiler/widgets/cosmic/scrollview.slint @@ -13,8 +13,6 @@ export component ScrollBar { in-out property value; in property policy: ScrollBarPolicy.as-needed; - callback scrolled(); - private property track-size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset; private property step-size: 10px; private property offset: 2px; @@ -65,7 +63,6 @@ export component ScrollBar { root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width)) : (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height)) ))); - root.scrolled(); } } @@ -125,8 +122,6 @@ export component ScrollView { horizontal: false; maximum: flickable.viewport-height - flickable.height; page-size: flickable.height; - - scrolled => {root.scrolled()} } horizontal-bar := ScrollBar { @@ -138,7 +133,5 @@ export component ScrollView { horizontal: true; maximum: flickable.viewport-width - flickable.width; page-size: flickable.width; - - scrolled => {root.scrolled()} } } diff --git a/internal/compiler/widgets/cupertino/scrollview.slint b/internal/compiler/widgets/cupertino/scrollview.slint index 76041c8b26d..ee47d1af6fd 100644 --- a/internal/compiler/widgets/cupertino/scrollview.slint +++ b/internal/compiler/widgets/cupertino/scrollview.slint @@ -12,8 +12,6 @@ export component ScrollBar inherits Rectangle { in-out property value; in property policy: ScrollBarPolicy.as-needed; - callback scrolled(); - property track-size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset; property step-size: 10px; property offset: 2px; @@ -73,7 +71,6 @@ export component ScrollBar inherits Rectangle { root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width)) : (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height)) ))); - root.scrolled(); } } @@ -135,8 +132,6 @@ export component ScrollView { horizontal: false; maximum: flickable.viewport-height - flickable.height; page-size: flickable.height; - - scrolled => {root.scrolled()} } horizontal-bar := ScrollBar { @@ -148,7 +143,5 @@ export component ScrollView { horizontal: true; maximum: flickable.viewport-width - flickable.width; page-size: flickable.width; - - scrolled => {root.scrolled()} } } diff --git a/internal/compiler/widgets/fluent/scrollview.slint b/internal/compiler/widgets/fluent/scrollview.slint index 75d8c28657e..2b89e81f1a3 100644 --- a/internal/compiler/widgets/fluent/scrollview.slint +++ b/internal/compiler/widgets/fluent/scrollview.slint @@ -42,8 +42,6 @@ component ScrollBar inherits Rectangle { in property policy: ScrollBarPolicy.as-needed; in property enabled; - callback scrolled(); - property offset: 16px; property size: 2px; property track-size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset; @@ -94,7 +92,6 @@ component ScrollBar inherits Rectangle { root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width)) : (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height)) ))); - root.scrolled(); } } @@ -178,8 +175,6 @@ export component ScrollView { horizontal: false; maximum: flickable.viewport-height - flickable.height; page-size: flickable.height; - - scrolled => {root.scrolled()} } horizontal-bar := ScrollBar { @@ -191,7 +186,5 @@ export component ScrollView { horizontal: true; maximum: flickable.viewport-width - flickable.width; page-size: flickable.width; - - scrolled => {root.scrolled()} } } diff --git a/internal/compiler/widgets/material/scrollview.slint b/internal/compiler/widgets/material/scrollview.slint index 23f78514dbd..81848539288 100644 --- a/internal/compiler/widgets/material/scrollview.slint +++ b/internal/compiler/widgets/material/scrollview.slint @@ -13,8 +13,6 @@ component ScrollBar inherits Rectangle { in-out property enabled <=> touch-area.enabled; in property policy: ScrollBarPolicy.as-needed; - callback scrolled(); - states [ disabled when !touch-area.enabled : { background.border-color: MaterialPalette.control-foreground; @@ -75,7 +73,6 @@ component ScrollBar inherits Rectangle { root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.width - handle.width)) : (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.height - handle.height)) ))); - root.scrolled(); } } @@ -135,8 +132,6 @@ export component ScrollView { maximum: flickable.viewport-height - flickable.height; page-size: flickable.height; enabled: root.enabled; - - scrolled => {root.scrolled()} } horizontal-bar := ScrollBar { @@ -148,7 +143,5 @@ export component ScrollView { maximum: flickable.viewport-width - flickable.width; page-size: flickable.width; enabled: root.enabled; - - scrolled => {root.scrolled()} } } diff --git a/internal/core/items/flickable.rs b/internal/core/items/flickable.rs index 2237cbc4186..0d28828b3ed 100644 --- a/internal/core/items/flickable.rs +++ b/internal/core/items/flickable.rs @@ -14,7 +14,7 @@ use crate::input::{ FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent, MouseEvent, }; use crate::item_rendering::CachedRenderingData; -use crate::items::PropertyAnimation; +use crate::items::{AnimationDirection, PropertyAnimation}; use crate::layout::{LayoutInfo, Orientation}; use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, @@ -92,6 +92,26 @@ impl Item for Flickable { } }, ); + + self.data.viewport_change_handler.init_delayed( + self_rc.downgrade(), + |self_weak| { + let Some(flick_rc) = self_weak.upgrade() else { return Default::default() }; + let Some(flick) = flick_rc.downcast::() else { + return Default::default(); + }; + let flick = flick.as_pin_ref(); + + (flick.viewport_x().get(), flick.viewport_y().get()) + }, + |self_weak, _| { + let Some(flick_rc) = self_weak.upgrade() else { return }; + let Some(flick) = flick_rc.downcast::() else { return }; + let flick = flick.as_pin_ref(); + + flick.flicked.call(&()) + }, + ); } fn layout_info( @@ -242,6 +262,15 @@ pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500); /// The delay to which press are forwarded to the inner item pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100); +const SMOOTH_SCROLL_DURATION: i32 = 250; +const SMOOTH_SCROLL_ANIM: PropertyAnimation = PropertyAnimation { + duration: SMOOTH_SCROLL_DURATION, + easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]), + delay: 0, + iteration_count: 1., + direction: AnimationDirection::Normal, +}; + #[derive(Default, Debug)] struct FlickableDataInner { /// The position in which the press was made @@ -250,6 +279,8 @@ struct FlickableDataInner { pressed_viewport_pos: LogicalPoint, /// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children capture_events: bool, + smooth_scroll_time: Option, + smooth_scroll_target: LogicalPoint, } #[derive(Default, Debug)] @@ -257,6 +288,8 @@ pub struct FlickableData { inner: RefCell, /// Tracker that tracks the property to make sure that the flickable is in bounds in_bound_change_handler: crate::properties::ChangeTracker, + // Scroll trackers for flicked callback + viewport_change_handler: crate::properties::ChangeTracker, } impl FlickableData { @@ -372,12 +405,8 @@ impl FlickableData { if inner.capture_events || should_capture() { let new_pos = ensure_in_bound(flick, new_pos, flick_rc); - let old_pos = (x.get(), y.get()); x.set(new_pos.x_length()); y.set(new_pos.y_length()); - if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() { - (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&()); - } inner.capture_events = true; InputEventResult::GrabMouse @@ -395,7 +424,7 @@ impl FlickableData { } } MouseEvent::Wheel { delta_x, delta_y, .. } => { - let delta = if window_adapter.window().0.modifiers.get().shift() + let mut delta = if window_adapter.window().0.modifiers.get().shift() && !cfg!(target_os = "macos") { // 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 { return InputEventResult::EventIgnored; } - let old_pos = LogicalPoint::from_lengths( - (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(), - (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(), - ); - let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc); - let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick); let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick); - let old_pos = (viewport_x.get(), viewport_y.get()); - viewport_x.set(new_pos.x_length()); - viewport_y.set(new_pos.y_length()); - if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() { - (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&()); + + let old_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get()); + + // Accumulate scroll delta + if let Some(smooth_scroll_time) = inner.smooth_scroll_time.take() { + let millis = + (crate::animations::current_tick() - smooth_scroll_time).as_millis() as i32; + + if millis < SMOOTH_SCROLL_DURATION { + let remaining_delta = inner.smooth_scroll_target - old_pos; + + // Only if is in the same direction. + // `Default` is because `dot` returns `i32` in embedded + // but it returns `f32` in any other platform + if delta.dot(remaining_delta) > Default::default() { + delta += remaining_delta; + } + } } + + let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc); + + inner.smooth_scroll_target = new_pos; + inner.smooth_scroll_time = Some(Instant::now()); + + viewport_y.set_animated_value(new_pos.y_length(), SMOOTH_SCROLL_ANIM); + viewport_x.set_animated_value(new_pos.x_length(), SMOOTH_SCROLL_ANIM); + InputEventResult::EventAccepted } MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored, @@ -449,26 +494,19 @@ impl FlickableData { { let speed = dist / (millis as f32); - let duration = 250; let final_pos = ensure_in_bound( flick, - (inner.pressed_viewport_pos.cast() + dist + speed * (duration as f32)).cast(), + (inner.pressed_viewport_pos.cast() + + dist + + speed * (SMOOTH_SCROLL_DURATION as f32)) + .cast(), flick_rc, ); - let anim = PropertyAnimation { - duration, - easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]), - ..PropertyAnimation::default() - }; let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick); let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick); - let old_pos = (viewport_x.get(), viewport_y.get()); - viewport_x.set_animated_value(final_pos.x_length(), anim.clone()); - viewport_y.set_animated_value(final_pos.y_length(), anim); - if old_pos.0 != final_pos.x_length() || old_pos.1 != final_pos.y_length() { - (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&()); - } + viewport_x.set_animated_value(final_pos.x_length(), SMOOTH_SCROLL_ANIM); + viewport_y.set_animated_value(final_pos.y_length(), SMOOTH_SCROLL_ANIM); } } inner.capture_events = false; // FIXME: should only be set to false once the flick animation is over diff --git a/internal/core/model.rs b/internal/core/model.rs index 4a82017cd87..0bd9199ecaa 100644 --- a/internal/core/model.rs +++ b/internal/core/model.rs @@ -1281,7 +1281,9 @@ impl Repeater { viewport_height.set(inner.cached_item_height * row_count as Coord); viewport_width.set(vp_width); let new_viewport_y = -inner.anchor_y + new_offset_y; - viewport_y.set(new_viewport_y); + if viewport_y.get() != new_viewport_y { + viewport_y.set(new_viewport_y); + } inner.previous_viewport_y = new_viewport_y; break; } diff --git a/tests/cases/elements/flickable.slint b/tests/cases/elements/flickable.slint index 0d40cc615b6..3a9d209fda9 100644 --- a/tests/cases/elements/flickable.slint +++ b/tests/cases/elements/flickable.slint @@ -44,7 +44,7 @@ TestCase := Window { property inner_ta_has_hover: inner_ta.has_hover; property clicked; property double-clicked; - property flicked; + property flicked; } /* @@ -233,6 +233,7 @@ assert!((instance.get_offset_y() - 55.).abs() < 5.); use slint::{LogicalPosition, platform::{WindowEvent, Key} }; let instance = TestCase::new().unwrap(); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: -30.0, delta_y: -50.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_offset_x(), 30.); assert_eq!(instance.get_offset_y(), 50.); @@ -242,6 +243,7 @@ if !cfg!(target_os = "macos") { slint_testing::send_keyboard_char(&instance, Key::Shift.into(), true); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: 15.0, delta_y: -60.0 }); slint_testing::send_keyboard_char(&instance, Key::Shift.into(), false); + slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_offset_x(), 30. + 60.); assert_eq!(instance.get_offset_y(), 50. - 15.); } @@ -255,6 +257,7 @@ assert_eq!(instance.get_flicked(), 0); // test scrolling behaviour instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: -30.0, delta_y: -50.0 }); +slint_testing::mock_elapsed_time(250); dbg!(instance.get_flicked()); assert_eq!(instance.get_flicked(), -3000050); //flicked got called after scrolling instance.set_flicked(0); @@ -268,7 +271,8 @@ slint_testing::mock_elapsed_time(10000); assert_eq!(instance.get_flicked(), -10500105); //flicked got called during drag instance.set_flicked(0); instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(100.0, 120.0), button: PointerEventButton::Left }); -assert_eq!(instance.get_flicked(), -10500105); //flicked got called after drag +slint_testing::mock_elapsed_time(250); +assert_eq!(instance.get_flicked(), -10682145); //flicked got called after drag instance.set_flicked(0); ``` diff --git a/tests/cases/elements/flickable2.slint b/tests/cases/elements/flickable2.slint index 871705417aa..9ab30d70678 100644 --- a/tests/cases/elements/flickable2.slint +++ b/tests/cases/elements/flickable2.slint @@ -47,7 +47,9 @@ TestCase := Window { } } - Flickable { for i in 5: Rectangle {} } + Flickable { + for i in 5: Rectangle {} + } property all_ok: r1.ok && r2.ok && r3.ok && r4.ok; property test: all_ok; diff --git a/tests/cases/elements/flickable3.slint b/tests/cases/elements/flickable3.slint index 0b4a3115e4d..578ccc8710d 100644 --- a/tests/cases/elements/flickable3.slint +++ b/tests/cases/elements/flickable3.slint @@ -69,12 +69,14 @@ assert_eq!(instance.get_t1_has_hover(), true); assert_eq!(instance.get_t1sec_has_hover(), false); assert_eq!(instance.get_t2_has_hover(), false); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 25.0), delta_x: 0.0, delta_y: -30.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_t1_has_hover(), false); assert_eq!(instance.get_t1sec_has_hover(), true); assert_eq!(instance.get_t2_has_hover(), false); assert_eq!(instance.get_f1_pos(), -30.0); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 25.0), delta_x: 0.0, delta_y: -30.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_t1_has_hover(), false); assert_eq!(instance.get_t1sec_has_hover(), false); assert_eq!(instance.get_t2_has_hover(), false); @@ -88,6 +90,7 @@ instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPo assert_eq!(instance.get_t2_has_hover(), true); assert_eq!(instance.get_t1_has_hover(), false); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(275.0, 25.0), delta_x: -30.0, delta_y: 0.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_t2_has_hover(), false); assert_eq!(instance.get_t1_has_hover(), false); diff --git a/tests/cases/elements/flickable_in_flickable.slint b/tests/cases/elements/flickable_in_flickable.slint index eef5767c8d6..9ab023b86fd 100644 --- a/tests/cases/elements/flickable_in_flickable.slint +++ b/tests/cases/elements/flickable_in_flickable.slint @@ -16,6 +16,7 @@ TestCase := Window { inner := Flickable { viewport_width: 1500px; + Rectangle { background: @radial-gradient(circle, yellow, blue, red, green); } @@ -126,14 +127,17 @@ assert_eq!(instance.get_outer_y(), old_outer_y); use slint::{LogicalPosition, platform::WindowEvent }; let instance = TestCase::new().unwrap(); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: 0.0, delta_y: -50.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_inner_x(), 0.); assert_eq!(instance.get_outer_y(), 50.); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: -30.0, delta_y: 0.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_inner_x(), 30.); assert_eq!(instance.get_outer_y(), 50.); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(175.0, 175.0), delta_x: 0.0, delta_y: 10.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_inner_x(), 30.); assert_eq!(instance.get_outer_y(), 40.); ``` diff --git a/tests/cases/elements/listview.slint b/tests/cases/elements/listview.slint index 1d84dd1ec47..41aaaea6c25 100644 --- a/tests/cases/elements/listview.slint +++ b/tests/cases/elements/listview.slint @@ -55,6 +55,7 @@ assert_eq(instance.get_listview_viewport_width(), 1500.); // scroll all the way down with the mouse wheel and click on the last item for (int i = 0; i < 10; ++i) { instance.window().dispatch_pointer_scroll_event(slint::LogicalPosition({25.0, 105.0}), 0, -50); + slint_testing::mock_elapsed_time(250); } slint_testing::send_mouse_click(&instance, 5., 441.); assert_eq(instance.get_value(), "Cyan"); @@ -72,6 +73,7 @@ assert_eq!(instance.get_listview_viewport_width(), 1500.); use slint::{LogicalPosition, platform::WindowEvent }; for _ in 0..10 { instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 105.0), delta_x: 0.0, delta_y: -50.0 }); + slint_testing::mock_elapsed_time(250); } slint_testing::send_mouse_click(&instance, 5., 441.); assert_eq!(instance.get_value(), "Cyan"); diff --git a/tests/cases/focus/listview-hidden.slint b/tests/cases/focus/listview-hidden.slint index e5f6f3b2034..cccef5dbddf 100644 --- a/tests/cases/focus/listview-hidden.slint +++ b/tests/cases/focus/listview-hidden.slint @@ -58,6 +58,7 @@ let delta_y = -600.0; assert_eq!(instance.get_viewport_y(), 0.0); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(150.0, 150.0), delta_x: -0.0, delta_y }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_viewport_y(), delta_y); assert_eq!(instance.get_current_item(), 1); slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Backtab)); diff --git a/tests/cases/input/scroll-event.slint b/tests/cases/input/scroll-event.slint index 5df14cf4fcf..1bd2932e9b7 100644 --- a/tests/cases/input/scroll-event.slint +++ b/tests/cases/input/scroll-event.slint @@ -48,6 +48,7 @@ use slint::{LogicalPosition, platform::WindowEvent }; let instance = TestCase::new().unwrap(); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 30.0), delta_x: -3.0, delta_y: -50.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_result(), "ta1{-3,-50 at 5,10}"); assert_eq!(instance.get_offset_y(), 0.0); assert_eq!(instance.get_offset_x(), 0.0); @@ -55,6 +56,7 @@ assert_eq!(instance.get_offset_x(), 0.0); instance.set_result("".into()); instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(155.0, 50.0), delta_x: -30.0, delta_y: -50.0 }); +slint_testing::mock_elapsed_time(250); assert_eq!(instance.get_result(), "ta2{-30,-50 at 35,30}"); assert_eq!(instance.get_offset_x(), 30.0); assert_eq!(instance.get_offset_y(), 50.0);