Skip to main content

i_slint_core/items/
flickable.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore tmax tmin
5//! The implementation details behind the Flickable
6
7//! The `Flickable` item
8
9use super::{
10    Item, ItemConsts, ItemRc, ItemRendererRef, KeyEventResult, PointerEventButton, RenderingResult,
11    VoidArg,
12};
13use crate::animations::Instant;
14use crate::animations::physics_simulation::ConstantDecelerationParameters;
15use crate::input::InternalKeyEvent;
16use crate::input::{
17    FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, MouseEvent, TouchPhase,
18};
19use crate::item_rendering::CachedRenderingData;
20use crate::layout::{LayoutInfo, Orientation};
21use crate::lengths::{
22    LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
23    PointLengths, RectLengths,
24};
25#[cfg(feature = "rtti")]
26use crate::rtti::*;
27use crate::window::WindowAdapter;
28use crate::{Callback, Coord, Property};
29use alloc::boxed::Box;
30use alloc::rc::Rc;
31use const_field_offset::FieldOffsets;
32use core::cell::RefCell;
33use core::pin::Pin;
34use core::time::Duration;
35#[allow(unused)]
36use euclid::num::Ceil;
37use euclid::num::Zero;
38use i_slint_core_macros::*;
39#[allow(unused)]
40use num_traits::Float;
41mod data_ringbuffer;
42use data_ringbuffer::VelocityRingBuffer;
43
44/// Deceleration during the animation. It slows down the initial velocity of the simulation
45/// so that the simulation stops at some point if it didn't reach the limit
46/// The unit is: LogicalPixel/s^2
47const DECELERATION: f32 = 2000.;
48/// Fixed-duration animation used for wheel scrolling, where we don't have enough phase
49/// information to derive a fling velocity.
50/// The unit is: millisecond
51const WHEEL_SCROLL_DURATION: Duration = Duration::from_millis(180);
52/// The maximum duration between a move and a release event to start an animation
53/// If the duration is larger than this value, no animation will be executed because
54/// it is not desired
55const MAX_DURATION: Duration = Duration::from_millis(100);
56
57/// The implementation of the `Flickable` element
58#[repr(C)]
59#[derive(FieldOffsets, Default, SlintElement)]
60#[pin]
61pub struct Flickable {
62    pub viewport_x: Property<LogicalLength>,
63    pub viewport_y: Property<LogicalLength>,
64    pub viewport_width: Property<LogicalLength>,
65    pub viewport_height: Property<LogicalLength>,
66
67    pub interactive: Property<bool>,
68
69    pub flicked: Callback<VoidArg>,
70
71    data: FlickableDataBox,
72
73    /// FIXME: remove this
74    pub cached_rendering_data: CachedRenderingData,
75}
76
77impl Item for Flickable {
78    fn init(self: Pin<&Self>, self_rc: &ItemRc) {
79        self.data.in_bound_change_handler.init_delayed(
80            self_rc.downgrade(),
81            // Binding that returns if the Flickable is out of bounds:
82            |self_weak| {
83                let Some(flick_rc) = self_weak.upgrade() else {
84                    return (false, false);
85                };
86                let Some(flick) = flick_rc.downcast::<Flickable>() else {
87                    return (false, false);
88                };
89                let flick = flick.as_pin_ref();
90                let geo = Self::geometry_without_virtual_keyboard(&flick_rc);
91
92                let zero = LogicalLength::zero();
93                let vpx = flick.viewport_x();
94                let vpy = flick.viewport_y();
95                let x_out_of_bounds =
96                    vpx > zero || vpx < (geo.width_length() - flick.viewport_width()).min(zero);
97                let y_out_of_bounds =
98                    vpy > zero || vpy < (geo.height_length() - flick.viewport_height()).min(zero);
99
100                (x_out_of_bounds, y_out_of_bounds)
101            },
102            // Change event handler that puts the Flickable in bounds if it's not already
103            |self_weak, (x_out_of_bounds, y_out_of_bounds)| {
104                let Some(flick_rc) = self_weak.upgrade() else { return };
105                let Some(flick) = flick_rc.downcast::<Flickable>() else { return };
106                let flick = flick.as_pin_ref();
107                let vpx = flick.viewport_x();
108                let vpy = flick.viewport_y();
109                let p = ensure_in_bound(flick, LogicalPoint::from_lengths(vpx, vpy), &flick_rc);
110
111                let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
112                if *x_out_of_bounds && !x.has_binding() {
113                    x.set(p.x_length());
114                }
115
116                let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
117                if *y_out_of_bounds && !y.has_binding() {
118                    y.set(p.y_length());
119                }
120            },
121        );
122    }
123
124    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
125
126    fn layout_info(
127        self: Pin<&Self>,
128        _orientation: Orientation,
129        _cross_axis_constraint: Coord,
130        _window_adapter: &Rc<dyn WindowAdapter>,
131        _self_rc: &ItemRc,
132    ) -> LayoutInfo {
133        LayoutInfo { stretch: 1., ..LayoutInfo::default() }
134    }
135
136    fn input_event_filter_before_children(
137        self: Pin<&Self>,
138        event: &MouseEvent,
139        window_adapter: &Rc<dyn WindowAdapter>,
140        self_rc: &ItemRc,
141        _: &mut super::MouseCursor,
142    ) -> InputEventFilterResult {
143        if let Some(pos) = event.position() {
144            let geometry = Self::geometry_without_virtual_keyboard(self_rc);
145
146            if (pos.x < 0 as _
147                || pos.y < 0 as _
148                || pos.x_length() > geometry.width_length()
149                || pos.y_length() > geometry.height_length())
150                && self.data.inner.borrow().pressed_mouse_state.is_none()
151            {
152                return InputEventFilterResult::Intercept;
153            }
154        }
155        if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
156            return InputEventFilterResult::ForwardAndIgnore;
157        }
158        self.data.handle_mouse_filter(self, event, window_adapter, self_rc)
159    }
160
161    fn input_event(
162        self: Pin<&Self>,
163        event: &MouseEvent,
164        window_adapter: &Rc<dyn WindowAdapter>,
165        self_rc: &ItemRc,
166        _: &mut super::MouseCursor,
167    ) -> InputEventResult {
168        if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
169            return InputEventResult::EventIgnored;
170        }
171        if let Some(pos) = event.position() {
172            let geometry = Self::geometry_without_virtual_keyboard(self_rc);
173            if matches!(event, MouseEvent::Wheel { .. } | MouseEvent::Pressed { .. })
174                && (pos.x < 0 as _
175                    || pos.y < 0 as _
176                    || pos.x_length() > geometry.width_length()
177                    || pos.y_length() > geometry.height_length())
178            {
179                return InputEventResult::EventIgnored;
180            }
181        }
182
183        self.data.handle_mouse(self, event, window_adapter, self_rc)
184    }
185
186    fn capture_key_event(
187        self: Pin<&Self>,
188        _: &InternalKeyEvent,
189        _window_adapter: &Rc<dyn WindowAdapter>,
190        _self_rc: &ItemRc,
191    ) -> KeyEventResult {
192        KeyEventResult::EventIgnored
193    }
194
195    fn key_event(
196        self: Pin<&Self>,
197        _: &InternalKeyEvent,
198        _window_adapter: &Rc<dyn WindowAdapter>,
199        _self_rc: &ItemRc,
200    ) -> KeyEventResult {
201        KeyEventResult::EventIgnored
202    }
203
204    fn focus_event(
205        self: Pin<&Self>,
206        _: &FocusEvent,
207        _window_adapter: &Rc<dyn WindowAdapter>,
208        _self_rc: &ItemRc,
209    ) -> FocusEventResult {
210        FocusEventResult::FocusIgnored
211    }
212
213    fn render(
214        self: Pin<&Self>,
215        backend: &mut ItemRendererRef,
216        _self_rc: &ItemRc,
217        size: LogicalSize,
218    ) -> RenderingResult {
219        (*backend).combine_clip(
220            LogicalRect::new(LogicalPoint::default(), size),
221            LogicalBorderRadius::zero(),
222            LogicalLength::zero(),
223        );
224        RenderingResult::ContinueRenderingChildren
225    }
226
227    fn bounding_rect(
228        self: core::pin::Pin<&Self>,
229        _window_adapter: &Rc<dyn WindowAdapter>,
230        _self_rc: &ItemRc,
231        geometry: LogicalRect,
232    ) -> LogicalRect {
233        geometry
234    }
235
236    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
237        true
238    }
239}
240
241impl ItemConsts for Flickable {
242    const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
243        Self::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
244}
245
246impl Flickable {
247    fn choose_min_move(
248        current_view_start: Coord, // vx or vy
249        view_len: Coord,           // w or h
250        content_len: Coord,        // vw or vh
251        points: impl Iterator<Item = Coord>,
252    ) -> Coord {
253        // Feasible translations t such that for all p: vx+t <= p <= vx+t+w
254        // -> t in [max_i(p_i - (vx + w)), min_i(p_i - vx)]
255        let zero = 0 as Coord;
256        let mut lower = Coord::MIN;
257        let mut upper = Coord::MAX;
258
259        for p in points {
260            lower = lower.max(p - (current_view_start + view_len));
261            upper = upper.min(p - current_view_start);
262        }
263
264        if lower > upper {
265            // No translation can include all points simultaneously; pick nearest bound direction.
266            // This happens only with NaNs; guard anyway.
267            return zero;
268        }
269
270        // Allowed translation interval due to scroll limits
271        let max_scroll = (content_len - view_len).max(zero);
272        let tmin = -current_view_start; // cannot scroll before 0
273        let tmax = max_scroll - current_view_start; // cannot scroll past max
274
275        let i_min = lower.max(tmin);
276        let i_max = upper.min(tmax);
277
278        if i_min <= i_max {
279            if zero < i_min {
280                i_min
281            } else if zero > i_max {
282                i_max
283            } else {
284                zero
285            }
286        // Intervals disjoint: choose closest allowed translation to feasible interval
287        // either entirely left or right
288        } else if tmax < lower {
289            tmax
290        } else {
291            tmin
292        }
293    }
294
295    /// Scroll the Flickable so that all of the points are visible at the same time (if possible).
296    /// The points have to be in the parent's coordinate space.
297    pub(crate) fn reveal_points(self: Pin<&Self>, self_rc: &ItemRc, pts: &[LogicalPoint]) {
298        if pts.is_empty() {
299            return;
300        }
301
302        // visible viewport size from base Item
303        let geo = Self::geometry_without_virtual_keyboard(self_rc);
304
305        // content extents and current viewport origin (content coords)
306        let vw = Self::FIELD_OFFSETS.viewport_width().apply_pin(self).get().0;
307        let vh = Self::FIELD_OFFSETS.viewport_height().apply_pin(self).get().0;
308        let vx = -Self::FIELD_OFFSETS.viewport_x().apply_pin(self).get().0;
309        let vy = -Self::FIELD_OFFSETS.viewport_y().apply_pin(self).get().0;
310
311        // choose minimal translation along each axis
312        let tx = Self::choose_min_move(vx, geo.width(), vw, pts.iter().map(|p| p.x));
313        let ty = Self::choose_min_move(vy, geo.height(), vh, pts.iter().map(|p| p.y));
314
315        let new_vx = vx + tx;
316        let new_vy = vy + ty;
317
318        Self::FIELD_OFFSETS.viewport_x().apply_pin(self).set(euclid::Length::new(-new_vx));
319        Self::FIELD_OFFSETS.viewport_y().apply_pin(self).set(euclid::Length::new(-new_vy));
320    }
321
322    fn geometry_without_virtual_keyboard(self_rc: &ItemRc) -> LogicalRect {
323        let mut geometry = self_rc.geometry();
324
325        // subtract keyboard rect if needed
326        if let Some(keyboard_rect) = self_rc.window_adapter().and_then(|window_adapter| {
327            window_adapter.window().virtual_keyboard(crate::InternalToken)
328        }) {
329            let keyboard_pos = keyboard_rect.0;
330
331            let self_in_window_coordinates = self_rc.map_to_native_window(geometry.origin);
332            if (keyboard_pos.y as Coord) < (self_in_window_coordinates.y + geometry.height()) {
333                // Keyboard is below the flickable and overlapping
334                geometry.size.height = keyboard_pos.y as Coord - self_in_window_coordinates.y;
335            }
336        }
337        geometry
338    }
339}
340
341#[repr(C)]
342/// Wraps the internal data structure for the Flickable
343pub struct FlickableDataBox(core::ptr::NonNull<FlickableData>);
344
345impl Default for FlickableDataBox {
346    fn default() -> Self {
347        FlickableDataBox(Box::leak(Box::<FlickableData>::default()).into())
348    }
349}
350impl Drop for FlickableDataBox {
351    fn drop(&mut self) {
352        // Safety: the self.0 was constructed from a Box::leak in FlickableDataBox::default
353        drop(unsafe { Box::from_raw(self.0.as_ptr()) });
354    }
355}
356
357impl core::ops::Deref for FlickableDataBox {
358    type Target = FlickableData;
359    fn deref(&self) -> &Self::Target {
360        // Safety: initialized in FlickableDataBox::default
361        unsafe { self.0.as_ref() }
362    }
363}
364
365/// The distance required before it starts flicking if there is another item intercepting the mouse.
366pub(super) const DISTANCE_THRESHOLD: LogicalLength = LogicalLength::new(8 as _);
367/// Time required before we stop caring about child event if the mouse hasn't been moved
368pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
369/// The delay to which press are forwarded to the inner item
370pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
371/// Duration to filter scroll events from children after receiving a scroll event
372/// Note: This needs to be rather long, as that makes it more intuitive when scrolling with the
373/// mouse in concrete steps.
374/// The user can always override this by moving the mouse
375/// The value was tuned by hand, could be adjusted with further user feedback
376pub(super) const SCROLL_FILTER_DURATION: Duration = Duration::from_millis(800);
377/// Short duration for scroll event filtering, used when the end of the flickable is reached.
378pub(super) const SHORT_SCROLL_FILTER_DURATION: Duration =
379    Duration::from_millis(SCROLL_FILTER_DURATION.as_millis() as u64 / 2);
380/// How far the user has to move the mouse to stop filtering scroll event from children after receiving a scroll event
381pub(super) const SCROLL_FILTER_DISTANCE_SQUARED: LogicalLength = LogicalLength::new(4 as _);
382
383#[derive(Debug, PartialEq, Eq, Clone, Copy)]
384enum CaptureEvents {
385    MouseOrTouchScreen,
386    MouseWheel,
387}
388
389#[derive(Default)]
390struct FlickableDataInner {
391    /// The time and position in which the press was made
392    ///
393    /// The position is in the coordinate system of the flickable, not of the viewport.
394    pressed_mouse_state: Option<(Instant, LogicalPoint)>,
395    /// The last mouse position received, used to calculate the delta when flicking with the mouse.
396    ///
397    /// This position is in the coordinate system of the flickable, not of the viewport.
398    last_mouse_position: LogicalPoint,
399    /// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children
400    capture_events: Option<CaptureEvents>,
401    /// Heuristics for filtering scroll events from children after we have scrolled ourselves.
402    /// We want to filter those to prevent the case where the user scrolls with the mouse wheel,
403    /// but the mouse now moves over a child item, and that item captures the scroll event.
404    /// We use two heuristics: First, a timeout after we received a scroll event, and second, if the mouse moves we
405    /// stop filtering scroll event until the next scroll event.
406    last_scroll_event: Option<(Instant, LogicalPoint)>,
407
408    /// Ringbuffer to store the last move deltas. From those data the velocity can be
409    /// calculated required for the animation after the release event
410    velocity_rb: VelocityRingBuffer<5>,
411
412    /// The animation details of the currently running animation for smooth mouse wheel scrolling.
413    /// This allows us to add the missing delta of the animation to the next scroll event if the user scrolls again
414    /// before the animation is finished.
415    running_animation: Option<(Instant, [Option<ConstantDecelerationParameters>; 2])>,
416}
417
418impl FlickableDataInner {
419    fn should_capture_scroll(&self, timeout: Duration, position: LogicalPoint) -> bool {
420        self.last_scroll_event.is_some_and(|(last_time, last_position)| {
421            // Note: Squared length for MCU support, which use i32 coords.
422            crate::animations::current_tick() - last_time < timeout
423                && LogicalLength::new((last_position - position).square_length().abs())
424                    < SCROLL_FILTER_DISTANCE_SQUARED
425        })
426    }
427
428    /// Whether the delta is a scroll in a orthogonal direction than what is allowed by the Flickable
429    #[allow(clippy::nonminimal_bool)] // more readable this way
430    fn is_allowed_scroll_direction(
431        flick: Pin<&Flickable>,
432        delta: LogicalVector,
433        flick_rc: &ItemRc,
434    ) -> bool {
435        let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
436
437        (delta.y != 0 as Coord && flick.viewport_height() > geo.height_length())
438            || (delta.x != 0 as Coord && flick.viewport_width() > geo.width_length())
439    }
440
441    fn process_wheel_event(
442        &mut self,
443        flick: Pin<&Flickable>,
444        mut delta: LogicalVector,
445        position: LogicalPoint,
446        phase: TouchPhase,
447        flick_rc: &ItemRc,
448    ) -> InputEventResult {
449        if phase != TouchPhase::Started
450            && delta != LogicalVector::default()
451            && !Self::is_allowed_scroll_direction(flick, delta, flick_rc)
452        {
453            // Release the capture immediately, this event is not meant for this Flickable.
454            self.capture_events = None;
455            self.last_scroll_event = None;
456            self.running_animation = None;
457            self.velocity_rb = VelocityRingBuffer::default();
458            return InputEventResult::EventIgnored;
459        }
460
461        let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
462        let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
463        let current_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get());
464
465        if self.capture_events.is_none()
466            && matches!(phase, TouchPhase::Moved)
467            && let Some((start_time, [x_simulation, y_simulation])) = &self.running_animation
468        {
469            // If the animation is not finished, we add the remaining animations delta.
470            let animation_duration = crate::animations::current_tick().duration_since(*start_time);
471
472            if let Some(x_simulation) = x_simulation {
473                delta.x += x_simulation.remaining_distance(animation_duration);
474            }
475            if let Some(y_simulation) = y_simulation {
476                delta.y += y_simulation.remaining_distance(animation_duration);
477            }
478        }
479
480        let new_pos = ensure_in_bound(flick, current_pos + delta, flick_rc);
481        delta = new_pos - current_pos;
482
483        if phase != TouchPhase::Ended {
484            viewport_x.remove_binding();
485            viewport_y.remove_binding();
486            self.running_animation = None;
487        }
488
489        match phase {
490            TouchPhase::Cancelled => {
491                viewport_x.set(new_pos.x_length());
492                viewport_y.set(new_pos.y_length());
493                self.last_scroll_event = Some((crate::animations::current_tick(), position));
494            }
495            TouchPhase::Started => {
496                self.velocity_rb = VelocityRingBuffer::default();
497                self.capture_events = Some(CaptureEvents::MouseWheel);
498                self.last_scroll_event = Some((crate::animations::current_tick(), position));
499            }
500            TouchPhase::Moved => {
501                if self.capture_events.is_some_and(|capture| capture == CaptureEvents::MouseWheel) {
502                    // Touchpad case with different phases
503                    self.velocity_rb.push(crate::animations::current_tick(), new_pos - current_pos);
504                    viewport_x.set(new_pos.x_length());
505                    viewport_y.set(new_pos.y_length());
506                } else {
507                    // Mousewheel case with no phase
508                    // Add a short animation that covers the delta for smooth scrolling
509                    //
510                    // Note that this animation must support the viewport_x/_y and width/height
511                    // changing, as e.g. the ListView might resize the viewport if it gets a new size
512                    // estimate.
513                    //
514                    // At the time of writing, in practice this means we must use a physics animation.
515                    let [limit_x, limit_y] = Self::flick_limits(flick_rc, delta);
516
517                    let x_simulation = (delta.x != Coord::default()).then(|| {
518                        let simulation = ConstantDecelerationParameters::new_with_distance(
519                            delta.x as f32,
520                            WHEEL_SCROLL_DURATION.as_secs_f32(),
521                        );
522                        viewport_x.set_physic_animation_value(limit_x, simulation.clone());
523                        simulation
524                    });
525
526                    let y_simulation = (delta.y != Coord::default()).then(|| {
527                        let simulation = ConstantDecelerationParameters::new_with_distance(
528                            delta.y as f32,
529                            WHEEL_SCROLL_DURATION.as_secs_f32(),
530                        );
531                        viewport_y.set_physic_animation_value(limit_y, simulation.clone());
532                        simulation
533                    });
534
535                    if delta.x != 0 as Coord || delta.y != 0 as Coord {
536                        (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
537                    }
538
539                    self.running_animation =
540                        Some((crate::animations::current_tick(), [x_simulation, y_simulation]));
541                }
542                self.last_scroll_event = Some((crate::animations::current_tick(), position));
543            }
544            TouchPhase::Ended => {
545                if self.capture_events.is_some_and(|capture| capture == CaptureEvents::MouseWheel) {
546                    self.animate(flick, flick_rc);
547                }
548                self.capture_events = None;
549                return if self.should_capture_scroll(SHORT_SCROLL_FILTER_DURATION, position) {
550                    InputEventResult::EventAccepted
551                } else {
552                    InputEventResult::EventIgnored
553                };
554            }
555        }
556
557        let flicked = current_pos.x_length() != new_pos.x_length()
558            || current_pos.y_length() != new_pos.y_length();
559        if flicked {
560            (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
561            InputEventResult::EventAccepted
562        } else if self.should_capture_scroll(SHORT_SCROLL_FILTER_DURATION, position) {
563            // After reaching the end, keep accepting the input event for a while longer, then time
564            // out (by not updating the last_scroll_event)
565            InputEventResult::EventAccepted
566        } else {
567            self.last_scroll_event = None;
568            InputEventResult::EventIgnored
569        }
570    }
571
572    fn flick_limits(
573        flick_rc: &ItemRc,
574        flick_velocity: LogicalVector,
575    ) -> [Pin<Box<Property<f32>>>; 2] {
576        let flick_weak = flick_rc.downgrade();
577        let calculate_limits = move || {
578            flick_weak
579                .upgrade()
580                .and_then(|flick_rc| {
581                    flick_rc.downcast::<Flickable>().map(move |flick| (flick_rc, flick))
582                })
583                .map(|(flick_rc, flick)| {
584                    let flick = flick.as_pin_ref();
585                    ensure_in_bound(
586                        flick,
587                        LogicalPoint::from_lengths(
588                            -flick.viewport_width(),
589                            -flick.viewport_height(),
590                        ),
591                        &flick_rc,
592                    )
593                })
594        };
595
596        let limit_x = if flick_velocity.x < 0 as Coord {
597            let property = Box::pin(Property::new(0.0));
598            property.set_binding({
599                let calculate_limits = calculate_limits.clone();
600                move || calculate_limits().map(|limit| limit.x_length().get() as f32).unwrap_or(0.0)
601            });
602            property
603        } else {
604            Box::pin(Property::new(0.0))
605        };
606
607        let limit_y = if flick_velocity.y < 0 as Coord {
608            let property = Box::pin(Property::new(0.0));
609            property.set_binding(move || {
610                calculate_limits().map(|limit| limit.y_length().get() as f32).unwrap_or(0.0)
611            });
612            property
613        } else {
614            Box::pin(Property::new(0.0))
615        };
616
617        [limit_x, limit_y]
618    }
619
620    fn animate(&self, flick: Pin<&Flickable>, flick_rc: &ItemRc) {
621        if let Some(last_time) = self.velocity_rb.last_time() {
622            let mean_velocity = self.velocity_rb.mean_velocity();
623            if self.capture_events.is_some()
624                && mean_velocity.square_length() > 0 as Coord
625                && crate::animations::current_tick().duration_since(last_time) < MAX_DURATION
626            {
627                let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
628                let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
629
630                let [limit_x, limit_y] = Self::flick_limits(flick_rc, mean_velocity);
631
632                {
633                    let simulation =
634                        ConstantDecelerationParameters::new(mean_velocity.x as f32, DECELERATION);
635                    viewport_x.set_physic_animation_value(limit_x, simulation);
636                }
637
638                {
639                    let animation_y =
640                        ConstantDecelerationParameters::new(mean_velocity.y as f32, DECELERATION);
641                    viewport_y.set_physic_animation_value(limit_y, animation_y);
642                }
643
644                if mean_velocity.x != 0 as Coord || mean_velocity.y != 0 as Coord {
645                    (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
646                }
647            }
648        }
649    }
650}
651
652#[derive(Default)]
653pub struct FlickableData {
654    inner: RefCell<FlickableDataInner>,
655    /// Tracker that tracks the property to make sure that the flickable is in bounds
656    in_bound_change_handler: crate::properties::ChangeTracker,
657}
658
659impl FlickableData {
660    fn scroll_delta(
661        window_adapter: &Rc<dyn WindowAdapter>,
662        delta_x: Coord,
663        delta_y: Coord,
664    ) -> LogicalVector {
665        if window_adapter.window().0.context().0.modifiers.get().shift()
666            && !cfg!(target_os = "macos")
667        {
668            // Shift invert coordinate for the purpose of scrolling.
669            // But not on macOs because there the OS already take care of the change
670            LogicalVector::new(delta_y, delta_x)
671        } else {
672            LogicalVector::new(delta_x, delta_y)
673        }
674    }
675
676    fn handle_mouse_filter(
677        &self,
678        flick: Pin<&Flickable>,
679        event: &MouseEvent,
680        window_adapter: &Rc<dyn WindowAdapter>,
681        flick_rc: &ItemRc,
682    ) -> InputEventFilterResult {
683        let mut inner = self.inner.borrow_mut();
684        match event {
685            MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
686                inner.velocity_rb = VelocityRingBuffer::default();
687                inner.pressed_mouse_state = Some((crate::animations::current_tick(), *position));
688                inner.last_mouse_position = *position;
689                let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
690                viewport_x.remove_binding(); // Stop animation by removing the binding
691                let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
692                viewport_y.remove_binding(); // Stop animation by removing the binding
693
694                if inner.capture_events.is_some() {
695                    InputEventFilterResult::Intercept
696                } else {
697                    InputEventFilterResult::DelayForwarding(FORWARD_DELAY.as_millis() as _)
698                }
699            }
700            MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => {
701                inner.pressed_mouse_state = None;
702                if inner.capture_events.is_some() {
703                    InputEventFilterResult::Intercept
704                } else {
705                    InputEventFilterResult::ForwardEvent
706                }
707            }
708            MouseEvent::Moved { position, .. } => {
709                let do_intercept = inner.capture_events.is_some()
710                    || inner.pressed_mouse_state.is_some_and(
711                        |(pressed_time, pressed_mouse_position)| {
712                            let mouse_delta = *position - pressed_mouse_position;
713
714                            crate::animations::current_tick() - pressed_time <= DURATION_THRESHOLD
715                                && self.should_capture_mouse_direction(mouse_delta, flick, flick_rc)
716                        },
717                    );
718                if do_intercept {
719                    InputEventFilterResult::Intercept
720                } else if inner.pressed_mouse_state.is_some() {
721                    InputEventFilterResult::ForwardAndInterceptGrab
722                } else {
723                    InputEventFilterResult::ForwardEvent
724                }
725            }
726            MouseEvent::Wheel { position, delta_x, delta_y, phase } => {
727                match phase {
728                    TouchPhase::Cancelled => {
729                        // Qt sends the Cancelled Phase
730                        // If we recently handled a wheel event, intercept it to prevent children from grabbing
731                        // the scroll event
732                        let delta = Self::scroll_delta(window_adapter, *delta_x, *delta_y);
733                        if FlickableDataInner::is_allowed_scroll_direction(flick, delta, flick_rc)
734                            && inner.should_capture_scroll(SCROLL_FILTER_DURATION, *position)
735                        {
736                            InputEventFilterResult::Intercept
737                        } else {
738                            inner.last_scroll_event = None;
739                            InputEventFilterResult::ForwardEvent
740                        }
741                    }
742                    TouchPhase::Started => InputEventFilterResult::Intercept,
743                    TouchPhase::Moved => {
744                        if inner.capture_events.is_some() {
745                            InputEventFilterResult::Intercept
746                        } else {
747                            // If we recently handled a wheel event, intercept it to prevent children from grabbing
748                            // the scroll event
749                            let delta = Self::scroll_delta(window_adapter, *delta_x, *delta_y);
750                            if FlickableDataInner::is_allowed_scroll_direction(
751                                flick, delta, flick_rc,
752                            ) && inner.should_capture_scroll(SCROLL_FILTER_DURATION, *position)
753                            {
754                                InputEventFilterResult::Intercept
755                            } else {
756                                inner.last_scroll_event = None;
757                                InputEventFilterResult::ForwardEvent
758                            }
759                        }
760                    }
761                    TouchPhase::Ended => {
762                        if inner.capture_events.is_some() {
763                            InputEventFilterResult::Intercept
764                        } else {
765                            InputEventFilterResult::ForwardEvent
766                        }
767                    }
768                }
769            }
770            // Not the left button
771            MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
772                InputEventFilterResult::ForwardAndIgnore
773            }
774            MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
775                InputEventFilterResult::ForwardEvent
776            }
777            MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => {
778                InputEventFilterResult::ForwardAndIgnore
779            }
780        }
781    }
782
783    fn should_capture_mouse_direction(
784        &self,
785        mouse_delta: LogicalVector,
786        flick: Pin<&Flickable>,
787        flick_rc: &ItemRc,
788    ) -> bool {
789        let flickable_geometry = Flickable::geometry_without_virtual_keyboard(flick_rc);
790        let flickable_width = flickable_geometry.width_length();
791        let flickable_height = flickable_geometry.height_length();
792        let viewport_width = flick.viewport_width();
793        let viewport_height = flick.viewport_height();
794        let zero = LogicalLength::zero();
795
796        // We should capture the mouse movement, if the flickable can move in this
797        // axis, and the mouse has moved more than the threshold in this axis.
798        ((viewport_width > flickable_width || flick.viewport_x() != zero)
799            && abs(mouse_delta.x_length()) > DISTANCE_THRESHOLD)
800            || ((viewport_height > flickable_height || flick.viewport_y() != zero)
801                && abs(mouse_delta.y_length()) > DISTANCE_THRESHOLD)
802    }
803
804    fn handle_mouse(
805        &self,
806        flick: Pin<&Flickable>,
807        event: &MouseEvent,
808        window_adapter: &Rc<dyn WindowAdapter>,
809        flick_rc: &ItemRc,
810    ) -> InputEventResult {
811        let mut inner = self.inner.borrow_mut();
812        match event {
813            MouseEvent::Pressed { .. } => {
814                inner.capture_events = Some(CaptureEvents::MouseOrTouchScreen);
815                InputEventResult::GrabMouse
816            }
817            MouseEvent::Exit | MouseEvent::Released { .. } => {
818                if inner.capture_events.is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen) {
819                    let was_capturing = true;
820                    inner.animate(flick, flick_rc);
821                    inner.capture_events = None;
822                    inner.pressed_mouse_state = None;
823                    if was_capturing {
824                        InputEventResult::EventAccepted
825                    } else {
826                        InputEventResult::EventIgnored
827                    }
828                } else if inner.capture_events.is_none() {
829                    inner.pressed_mouse_state = None;
830                    InputEventResult::EventIgnored
831                } else {
832                    InputEventResult::EventIgnored
833                }
834            }
835            MouseEvent::Moved { position, .. } => {
836                // Important constraint: The viewport_y might not be stable, and might jump around
837                // wildly!
838                // This is especially the case if a ListView is involved, which will continuously
839                // update its own viewport_y to keep the current item visible, which can cause the
840                // viewport_y to jump.
841                //
842                // So to correctly calculate the mouse delta, we need to use the position of
843                // the mouse in the flickables coordinate system and never the viewport coordinate
844                // system.
845                if let Some((_pressed_time, _pressed_mouse_position)) = inner.pressed_mouse_state {
846                    let mouse_delta = *position - inner.last_mouse_position;
847                    inner.velocity_rb.push(crate::animations::current_tick(), mouse_delta);
848
849                    let is_capturing = inner
850                        .capture_events
851                        .is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen);
852                    if is_capturing
853                        || self.should_capture_mouse_direction(mouse_delta, flick, flick_rc)
854                    {
855                        // The drag event is meant to move the viewport, set it to the new position
856                        // and start capturing mouse events.
857                        let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
858                        let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
859                        let current_viewport_position =
860                            LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get());
861
862                        // We calculate the new viewport position by adding the mouse delta in the flickable
863                        // coordinate system to the current viewport position.
864                        // Do not rely on the existing viewport position to be stable, as e.g. the
865                        // ListView will continuously update it.
866                        // So we cannot calculate the delta in viewport coordinates.
867                        let new_viewport_position = current_viewport_position + mouse_delta;
868                        let new_viewport_position =
869                            ensure_in_bound(flick, new_viewport_position, flick_rc);
870
871                        viewport_x.set(new_viewport_position.x_length());
872                        viewport_y.set(new_viewport_position.y_length());
873                        if current_viewport_position != new_viewport_position {
874                            (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
875                        }
876
877                        // Only update the mouse position if we are actually applying the delta.
878                        // When the drag starts, there is a short dead zone that is determined by the
879                        // DISTANCE_THRESHOLD. We want to apply that threshold to the
880                        // delta once we've overcome it, so we need to update the position that we
881                        // calculate the delta from only after we've cleared the dead zone and are
882                        // actually moving.
883                        //
884                        // Note: As an alternative to updating the last_mouse_position to the new mouse position,
885                        // we could also update it by the amount that the viewport actually moved.
886                        // This would cause the mouse to stick to a given position in the viewport
887                        // instead of starting to drift if the drag goes into the viewport limits.
888                        // Then this code would need to be:
889                        //
890                        //  inner.last_mouse_position += new_viewport_position - current_viewport_position;
891                        //
892                        // But at least for a touchscreen, the current behavior is more intuitive.
893                        inner.last_mouse_position = *position;
894
895                        inner.capture_events = Some(CaptureEvents::MouseOrTouchScreen);
896
897                        InputEventResult::GrabMouse
898                    } else if abs(mouse_delta.x_length()) > DISTANCE_THRESHOLD
899                        || abs(mouse_delta.y_length()) > DISTANCE_THRESHOLD
900                    {
901                        // drag in a unsupported direction gives up the grab
902                        InputEventResult::EventIgnored
903                    } else {
904                        // the mouse was moved, but not enough to start the drag, we still want to accept further events
905                        // so that we may pass the threshold at some point
906                        InputEventResult::EventAccepted
907                    }
908                } else {
909                    InputEventResult::EventIgnored
910                }
911            }
912            MouseEvent::Wheel { delta_x, delta_y, position, phase } => {
913                let delta = Self::scroll_delta(window_adapter, *delta_x, *delta_y);
914                inner.process_wheel_event(flick, delta, *position, *phase, flick_rc)
915            }
916            MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
917                InputEventResult::EventIgnored
918            }
919            MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => InputEventResult::EventIgnored,
920        }
921    }
922}
923
924fn abs(l: LogicalLength) -> LogicalLength {
925    LogicalLength::new(l.get().abs())
926}
927
928/// Make sure that the point is within the bounds
929fn ensure_in_bound(flick: Pin<&Flickable>, p: LogicalPoint, flick_rc: &ItemRc) -> LogicalPoint {
930    let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
931    let w = geo.width_length();
932    let h = geo.height_length();
933    let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get();
934    let vh = (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get();
935
936    let min = LogicalPoint::from_lengths(w - vw, h - vh);
937    let max = LogicalPoint::default();
938    p.max(min).min(max)
939}
940
941/// # Safety
942/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
943/// hold a FlickableDataBox
944#[cfg(feature = "ffi")]
945#[unsafe(no_mangle)]
946pub unsafe extern "C" fn slint_flickable_data_init(data: *mut FlickableDataBox) {
947    unsafe { core::ptr::write(data, FlickableDataBox::default()) };
948}
949
950/// # Safety
951/// This must be called using a non-null pointer pointing to an initialized FlickableDataBox
952#[cfg(feature = "ffi")]
953#[unsafe(no_mangle)]
954pub unsafe extern "C" fn slint_flickable_data_free(data: *mut FlickableDataBox) {
955    unsafe {
956        core::ptr::drop_in_place(data);
957    }
958}