Repository: slint-ui/slint (language: rust)

Issue:
Slow scrolling on flickable is hard (when there is no toucharea inside) 
### Bug Description

I just noticed on out target devices with resistive and capacitive touch, that slow scrolling fails most of the time. Slow scrolling will likely happen because we have a small screen. I think the cursor speed is still reasonable.

The behaviour can be improved when there is a `TouchArea` inside. Check out the code below and the attached video. The column on the right has `TouchArea`s.


[Scrolling.webm](https://github.com/user-attachments/assets/dc09422f-237e-4835-91f1-1e32565b602a)

I also took a short look how firefox and chrome handle scrolling. They both have not problem with slow scrolling. Imo chrome handles touch scroll in the best way (I think safari is similiar). What I like about it is, that chrome directly sends the pressed event to an underlying button and if you move the cursor over some threshold, then it starts scrolling. It seems there is also no timer involved, as you can wait a few seconds and then start scrolling.



### Reproducible Code (if applicable)

```slint
export component Example inherits Window {
    width: 300px;
    height: 500px;

    HorizontalLayout {
    spacing: 20px;
        Flickable {
            VerticalLayout {
                for i in 20: Rectangle {
                    background: @linear-gradient(0deg, red, green);
                    height: 52px;
                }
            }
        }
        Flickable {
            VerticalLayout {
                for i in 20: Rectangle {
                    TouchArea {}
                    background: @linear-gradient(0deg, red, green);
                    height: 52px;
                }
                
            }
        }
    }
}
```


### Environment Details

- Slint Version: 1.8.0



### Product Impact

Critical, as scrolling fails most of the time.


Discussion / hints:
Thanks for flilling a bug.

I can reproduce the problem.

The core of the issue is that we cancel the grab if we get a move event that is below the distance threshold in https://github.com/slint-ui/slint/blob/d1be6f69fd9c772de67066edda671ee0e062cde2/internal/core/items/flickable.rs#L324
If we simply replace Ignored with Accepted in this line, that solves the problem. But it then exposes another problem in the test when we have Flickable inside Flickable: Since we no longer ignoring event on other direction, we don't process delayed event in the children.

Fix patch:
diff --git a/internal/core/items/flickable.rs b/internal/core/items/flickable.rs
index c0a682a4922..5199f62b583 100644
--- a/internal/core/items/flickable.rs
+++ b/internal/core/items/flickable.rs
@@ -320,8 +320,13 @@ impl FlickableData {
 
                         inner.capture_events = true;
                         InputEventResult::GrabMouse
-                    } else {
+                    } else if abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD
+                        || abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD
+                    {
+                        // drag in a unsupported direction gives up the grab
                         InputEventResult::EventIgnored
+                    } else {
+                        InputEventResult::EventAccepted
                     }
                 } else {
                     inner.capture_events = false;


Test patch:
diff --git a/tests/cases/elements/flickable.slint b/tests/cases/elements/flickable.slint
index e236d1fc705..97715026ea0 100644
--- a/tests/cases/elements/flickable.slint
+++ b/tests/cases/elements/flickable.slint
@@ -286,5 +286,49 @@ assert_eq!(instance.get_offset_x(), 0.);
 assert_eq!(instance.get_offset_y(), 0.);
 ```
 
+```rust
+// Same as first test, but wait a bit before moving the mouse, and move only a tiny  ()
+use slint::{platform::WindowEvent, platform::PointerEventButton, LogicalPosition};
+let instance = TestCase::new().unwrap();
+instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(300.0, 100.0) });
+slint_testing::mock_elapsed_time(5000);
+instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(300.0, 100.0), button: PointerEventButton::Left });
+assert_eq!(instance.get_offset_x(), 0.);
+assert_eq!(instance.get_offset_y(), 0.);
+slint_testing::mock_elapsed_time(500);
+instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(299.0, 99.0) });
+slint_testing::mock_elapsed_time(500);
+assert_eq!(instance.get_offset_x(), 0.);
+assert_eq!(instance.get_offset_y(), 0.);
+instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(200.0, 50.0) });
+assert_eq!(instance.get_offset_x(), 100.);
+assert_eq!(instance.get_offset_y(), 50.);
+slint_testing::mock_elapsed_time(200);
+assert_eq!(instance.get_offset_x(), 100.);
+assert_eq!(instance.get_offset_y(), 50.);
+instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(100.0, 50.0) });
+assert_eq!(instance.get_offset_x(), 200.);
+assert_eq!(instance.get_offset_y(), 50.);
+instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(100.0, 50.0), button: PointerEventButton::Left });
+// Start of the animation, the position is still unchanged
+assert_eq!(instance.get_offset_x(), 200.);
+assert_eq!(instance.get_offset_y(), 50.);
+slint_testing::mock_elapsed_time(50);
+// middle of the animation
+// NOTE: the value were changed compared to the first test because the timing is different
+assert!(instance.get_offset_x() > 201.);
+assert!(instance.get_offset_y() > 41.);
+assert!(instance.get_offset_x() < 290.);
+assert!(instance.get_offset_y() < 70.);
+
+// end of the animation
+slint_testing::mock_elapsed_time(300);
+
+assert!(!instance.get_inner_ta_pressed());
+assert!(!instance.get_inner_ta_has_hover());
+assert_eq!(instance.get_clicked(), 0);
+assert_eq!(instance.get_double_clicked(), 0);
+```
+
 */