diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index 1d58af7d3b9..c192aa0f80d 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -155,6 +155,7 @@ export component Flickable inherits Empty { in property viewport-width; in-out property viewport-x; in-out property viewport-y; + in property smooth-scroll: true; in property interactive: true; callback flicked(); //-default_size_binding:expands_to_parent_geometry diff --git a/internal/compiler/widgets/cosmic/scrollview.slint b/internal/compiler/widgets/cosmic/scrollview.slint index 168602859d5..2515bc0ca61 100644 --- a/internal/compiler/widgets/cosmic/scrollview.slint +++ b/internal/compiler/widgets/cosmic/scrollview.slint @@ -90,6 +90,7 @@ export component ScrollView { in-out property viewport-height <=> flickable.viewport-height; in-out property viewport-x <=> flickable.viewport-x; in-out property viewport-y <=> flickable.viewport-y; + in property smooth-scroll: true; in property vertical-scrollbar-policy <=> vertical-bar.policy; in property horizontal-scrollbar-policy <=> horizontal-bar.policy; in property mouse-drag-pan-enabled <=> flickable.interactive; @@ -108,6 +109,7 @@ export component ScrollView { flickable := Flickable { interactive: false; + smooth-scroll: root.smooth-scroll; viewport-y <=> vertical-bar.value; viewport-x <=> horizontal-bar.value; width: 100%; diff --git a/internal/compiler/widgets/cupertino/scrollview.slint b/internal/compiler/widgets/cupertino/scrollview.slint index 76041c8b26d..04185da7aef 100644 --- a/internal/compiler/widgets/cupertino/scrollview.slint +++ b/internal/compiler/widgets/cupertino/scrollview.slint @@ -98,6 +98,7 @@ export component ScrollView { in-out property viewport-height <=> flickable.viewport-height; in-out property viewport-x <=> flickable.viewport-x; in-out property viewport-y <=> flickable.viewport-y; + in property smooth-scroll: true; in property vertical-scrollbar-policy <=> vertical-bar.policy; in property horizontal-scrollbar-policy <=> horizontal-bar.policy; in property mouse-drag-pan-enabled <=> flickable.interactive; @@ -118,6 +119,7 @@ export component ScrollView { x: 2px; y: 2px; interactive: false; + smooth-scroll: root.smooth-scroll; viewport-y <=> vertical-bar.value; viewport-x <=> horizontal-bar.value; width: parent.width - 4px; diff --git a/internal/compiler/widgets/fluent/scrollview.slint b/internal/compiler/widgets/fluent/scrollview.slint index 75d8c28657e..48df2cfd338 100644 --- a/internal/compiler/widgets/fluent/scrollview.slint +++ b/internal/compiler/widgets/fluent/scrollview.slint @@ -143,6 +143,7 @@ export component ScrollView { in-out property viewport-height <=> flickable.viewport-height; in-out property viewport-x <=> flickable.viewport-x; in-out property viewport-y <=> flickable.viewport-y; + in property smooth-scroll: true; in property vertical-scrollbar-policy <=> vertical-bar.policy; in property horizontal-scrollbar-policy <=> horizontal-bar.policy; in property mouse-drag-pan-enabled <=> flickable.interactive; @@ -163,6 +164,7 @@ export component ScrollView { interactive: false; viewport-y <=> vertical-bar.value; viewport-x <=> horizontal-bar.value; + smooth-scroll: root.smooth-scroll; width: parent.width; height: parent.height; diff --git a/internal/compiler/widgets/material/scrollview.slint b/internal/compiler/widgets/material/scrollview.slint index 23f78514dbd..9fca1850741 100644 --- a/internal/compiler/widgets/material/scrollview.slint +++ b/internal/compiler/widgets/material/scrollview.slint @@ -102,6 +102,7 @@ export component ScrollView { in-out property viewport-height <=> flickable.viewport-height; in-out property viewport-x <=> flickable.viewport-x; in-out property viewport-y <=> flickable.viewport-y; + in property smooth-scroll: true; in property vertical-scrollbar-policy <=> vertical-bar.policy; in property horizontal-scrollbar-policy <=> horizontal-bar.policy; in property mouse-drag-pan-enabled <=> flickable.interactive; @@ -120,6 +121,7 @@ export component ScrollView { y: 0; viewport-y <=> vertical-bar.value; viewport-x <=> horizontal-bar.value; + smooth-scroll: root.smooth-scroll; width: parent.width - vertical-bar.width - 4px; height: parent.height - horizontal-bar.height - 4px; diff --git a/internal/compiler/widgets/qt/internal-scrollview.slint b/internal/compiler/widgets/qt/internal-scrollview.slint index 78a33f43b08..2dc3b112101 100644 --- a/internal/compiler/widgets/qt/internal-scrollview.slint +++ b/internal/compiler/widgets/qt/internal-scrollview.slint @@ -15,6 +15,7 @@ export component InternalScrollView { out property visible-height <=> fli.height; in-out property has-focus <=> native.has-focus; in property enabled <=> native.enabled; + in property smooth-scroll: true; in property vertical-scrollbar-policy <=> native.vertical-scrollbar-policy; in property horizontal-scrollbar-policy <=> native.horizontal-scrollbar-policy; in property mouse-drag-pan-enabled <=> fli.interactive; @@ -49,6 +50,7 @@ export component InternalScrollView { @children interactive: false; + smooth-scroll: root.smooth-scroll; viewport-y <=> native.vertical-value; viewport-x <=> native.horizontal-value; } diff --git a/internal/compiler/widgets/qt/scrollview.slint b/internal/compiler/widgets/qt/scrollview.slint index 120c69d30b9..bdb21dcea30 100644 --- a/internal/compiler/widgets/qt/scrollview.slint +++ b/internal/compiler/widgets/qt/scrollview.slint @@ -12,6 +12,7 @@ export component ScrollView { in-out property viewport-height <=> internal.viewport-height; in-out property viewport-x <=> internal.viewport-x; in-out property viewport-y <=> internal.viewport-y; + in property smooth-scroll: true; in property vertical-scrollbar-policy <=> internal.vertical-scrollbar-policy; in property horizontal-scrollbar-policy <=> internal.horizontal-scrollbar-policy; in property mouse-drag-pan-enabled <=> internal.mouse-drag-pan-enabled; @@ -26,6 +27,7 @@ export component ScrollView { preferred-width: 100%; internal := InternalScrollView { + smooth-scroll: root.smooth-scroll; @children } } diff --git a/internal/core/items/flickable.rs b/internal/core/items/flickable.rs index 2237cbc4186..1a20698330b 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, @@ -48,6 +48,7 @@ pub struct Flickable { pub viewport_height: Property, pub interactive: Property, + pub smooth_scroll: Property, pub flicked: Callback, @@ -242,6 +243,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 +260,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)] @@ -395,7 +407,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,18 +425,50 @@ 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() { + + let old_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get()); + + let smooth_scroll = (Flickable::FIELD_OFFSETS.smooth_scroll).apply_pin(flick).get(); + + if smooth_scroll { + // 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 in embedded `dot` returns `i32` + // but in any other platform it returns `f32` + if delta.dot(remaining_delta) > Default::default() { + delta += remaining_delta; + } + } + } + } + + let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc); + + if smooth_scroll { + inner.smooth_scroll_target = new_pos; + inner.smooth_scroll_time = Some(Instant::now()); + + // Discard previous animation + viewport_x.set_binding(|| euclid::Length::default()); + viewport_y.set_binding(|| euclid::Length::default()); + + viewport_x.set_animated_value(new_pos.x_length(), SMOOTH_SCROLL_ANIM); + viewport_y.set_animated_value(new_pos.y_length(), SMOOTH_SCROLL_ANIM); + } else { + viewport_x.set(new_pos.x_length()); + viewport_y.set(new_pos.y_length()); + } + + if old_pos != new_pos { (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&()); } InputEventResult::EventAccepted @@ -449,24 +493,21 @@ 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() { + let old_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get()); + viewport_x.set_animated_value(final_pos.x_length(), SMOOTH_SCROLL_ANIM); + viewport_y.set_animated_value(final_pos.y_length(), SMOOTH_SCROLL_ANIM); + if old_pos != final_pos { (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&()); } } diff --git a/internal/core/model.rs b/internal/core/model.rs index 4a82017cd87..196003c0c7c 100644 --- a/internal/core/model.rs +++ b/internal/core/model.rs @@ -1281,7 +1281,8 @@ 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); + // XXX: This `set` prevents smooth scroll animated value + // 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..d90bf7536fa 100644 --- a/tests/cases/elements/flickable.slint +++ b/tests/cases/elements/flickable.slint @@ -15,6 +15,7 @@ TestCase := Window { height: parent.height - 20phx; viewport_width: 2100phx; viewport_height: 2100phx; + smooth-scroll: false; flicked => { root.flicked += viewport_x/1phx * 100000 + viewport_y/1phx; 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..d6cfe1045f2 100644 --- a/tests/cases/elements/flickable3.slint +++ b/tests/cases/elements/flickable3.slint @@ -10,6 +10,7 @@ TestCase := Window { x: 0phx; width: 250phx; viewport-height: 800phx; + smooth-scroll: false; t1 := TouchArea { height: 50phx; @@ -27,6 +28,7 @@ TestCase := Window { x: 250phx; width: 250phx; viewport-width: 800phx; + smooth-scroll: false; y: 0; height: 300phx; diff --git a/tests/cases/elements/flickable_in_flickable.slint b/tests/cases/elements/flickable_in_flickable.slint index eef5767c8d6..d427770868f 100644 --- a/tests/cases/elements/flickable_in_flickable.slint +++ b/tests/cases/elements/flickable_in_flickable.slint @@ -13,9 +13,12 @@ TestCase := Window { height: parent.height - 20px; viewport_width: width; viewport_height: 980px; + smooth-scroll: false; inner := Flickable { viewport_width: 1500px; + smooth-scroll: false; + Rectangle { background: @radial-gradient(circle, yellow, blue, red, green); } diff --git a/tests/cases/elements/flickable_stay_in_bounds.slint b/tests/cases/elements/flickable_stay_in_bounds.slint index 7b7b4fc3ac6..25765cda9cc 100644 --- a/tests/cases/elements/flickable_stay_in_bounds.slint +++ b/tests/cases/elements/flickable_stay_in_bounds.slint @@ -13,6 +13,7 @@ export component TestCase inherits Window { height: 300phx; viewport-height: 800phx; viewport-width: 400phx; + smooth-scroll: false; } in-out property fli_width <=> fli.width; diff --git a/tests/cases/elements/listview-millions.slint b/tests/cases/elements/listview-millions.slint index a5c3c4ec37e..9ed3e3de98a 100644 --- a/tests/cases/elements/listview-millions.slint +++ b/tests/cases/elements/listview-millions.slint @@ -14,6 +14,8 @@ export component TestCase inherits Window { in-out property viewport-y <=> lv.viewport-y; lv := ListView { + smooth-scroll: false; + for _[num] in 2130000000: Rectangle { height: 20px; border-width: 1px; diff --git a/tests/cases/elements/listview.slint b/tests/cases/elements/listview.slint index 1d84dd1ec47..9c07f859582 100644 --- a/tests/cases/elements/listview.slint +++ b/tests/cases/elements/listview.slint @@ -17,6 +17,8 @@ TestCase := Window { listview := ListView { + smooth-scroll: false; + for data in [ { text: "Blue", color: #0000ff, bg: #eeeeee}, { text: "Red", color: #ff0000, bg: #eeeeee},