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