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