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::{EasingCurve, Instant};
13use crate::input::{
14    FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent, MouseEvent,
15};
16use crate::item_rendering::CachedRenderingData;
17use crate::items::PropertyAnimation;
18use crate::layout::{LayoutInfo, Orientation};
19use crate::lengths::{
20    LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
21    PointLengths, RectLengths,
22};
23#[cfg(feature = "rtti")]
24use crate::rtti::*;
25use crate::window::WindowAdapter;
26use crate::{Callback, Coord, Property};
27use alloc::boxed::Box;
28use alloc::rc::Rc;
29use const_field_offset::FieldOffsets;
30use core::cell::RefCell;
31use core::pin::Pin;
32use core::time::Duration;
33#[allow(unused)]
34use euclid::num::Ceil;
35use euclid::num::Zero;
36use i_slint_core_macros::*;
37#[allow(unused)]
38use num_traits::Float;
39
40/// The implementation of the `Flickable` element
41#[repr(C)]
42#[derive(FieldOffsets, Default, SlintElement)]
43#[pin]
44pub struct Flickable {
45    pub viewport_x: Property<LogicalLength>,
46    pub viewport_y: Property<LogicalLength>,
47    pub viewport_width: Property<LogicalLength>,
48    pub viewport_height: Property<LogicalLength>,
49
50    pub interactive: Property<bool>,
51
52    pub flicked: Callback<VoidArg>,
53
54    data: FlickableDataBox,
55
56    /// FIXME: remove this
57    pub cached_rendering_data: CachedRenderingData,
58}
59
60impl Item for Flickable {
61    fn init(self: Pin<&Self>, self_rc: &ItemRc) {
62        self.data.in_bound_change_handler.init_delayed(
63            self_rc.downgrade(),
64            // Binding that returns if the Flickable is out of bounds:
65            |self_weak| {
66                let Some(flick_rc) = self_weak.upgrade() else { return false };
67                let Some(flick) = flick_rc.downcast::<Flickable>() else { return false };
68                let flick = flick.as_pin_ref();
69                let geo = flick_rc.geometry();
70                let zero = LogicalLength::zero();
71                let vpx = flick.viewport_x();
72                if vpx > zero || vpx < (geo.width_length() - flick.viewport_width()).min(zero) {
73                    return true;
74                }
75                let vpy = flick.viewport_y();
76                if vpy > zero || vpy < (geo.height_length() - flick.viewport_height()).min(zero) {
77                    return true;
78                }
79                false
80            },
81            // Change event handler that puts the Flickable in bounds if it's not already
82            |self_weak, out_of_bound| {
83                let Some(flick_rc) = self_weak.upgrade() else { return };
84                let Some(flick) = flick_rc.downcast::<Flickable>() else { return };
85                let flick = flick.as_pin_ref();
86                if *out_of_bound {
87                    let vpx = flick.viewport_x();
88                    let vpy = flick.viewport_y();
89                    let p = ensure_in_bound(flick, LogicalPoint::from_lengths(vpx, vpy), &flick_rc);
90                    (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).set(p.x_length());
91                    (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).set(p.y_length());
92                }
93            },
94        );
95    }
96
97    fn layout_info(
98        self: Pin<&Self>,
99        _orientation: Orientation,
100        _window_adapter: &Rc<dyn WindowAdapter>,
101        _self_rc: &ItemRc,
102    ) -> LayoutInfo {
103        LayoutInfo { stretch: 1., ..LayoutInfo::default() }
104    }
105
106    fn input_event_filter_before_children(
107        self: Pin<&Self>,
108        event: &MouseEvent,
109        _window_adapter: &Rc<dyn WindowAdapter>,
110        self_rc: &ItemRc,
111    ) -> InputEventFilterResult {
112        if let Some(pos) = event.position() {
113            let geometry = self_rc.geometry();
114            if pos.x < 0 as _
115                || pos.y < 0 as _
116                || pos.x_length() > geometry.width_length()
117                || pos.y_length() > geometry.height_length()
118            {
119                if !self.data.inner.borrow().pressed_time.is_some() {
120                    return InputEventFilterResult::Intercept;
121                }
122            }
123        }
124        if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
125            return InputEventFilterResult::ForwardAndIgnore;
126        }
127        self.data.handle_mouse_filter(self, event, self_rc)
128    }
129
130    fn input_event(
131        self: Pin<&Self>,
132        event: &MouseEvent,
133        window_adapter: &Rc<dyn WindowAdapter>,
134        self_rc: &ItemRc,
135    ) -> InputEventResult {
136        if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
137            return InputEventResult::EventIgnored;
138        }
139        if let Some(pos) = event.position() {
140            let geometry = self_rc.geometry();
141            if matches!(event, MouseEvent::Wheel { .. } | MouseEvent::Pressed { .. })
142                && (pos.x < 0 as _
143                    || pos.y < 0 as _
144                    || pos.x_length() > geometry.width_length()
145                    || pos.y_length() > geometry.height_length())
146            {
147                return InputEventResult::EventIgnored;
148            }
149        }
150
151        self.data.handle_mouse(self, event, window_adapter, self_rc)
152    }
153
154    fn capture_key_event(
155        self: Pin<&Self>,
156        _: &KeyEvent,
157        _window_adapter: &Rc<dyn WindowAdapter>,
158        _self_rc: &ItemRc,
159    ) -> KeyEventResult {
160        KeyEventResult::EventIgnored
161    }
162
163    fn key_event(
164        self: Pin<&Self>,
165        _: &KeyEvent,
166        _window_adapter: &Rc<dyn WindowAdapter>,
167        _self_rc: &ItemRc,
168    ) -> KeyEventResult {
169        KeyEventResult::EventIgnored
170    }
171
172    fn focus_event(
173        self: Pin<&Self>,
174        _: &FocusEvent,
175        _window_adapter: &Rc<dyn WindowAdapter>,
176        _self_rc: &ItemRc,
177    ) -> FocusEventResult {
178        FocusEventResult::FocusIgnored
179    }
180
181    fn render(
182        self: Pin<&Self>,
183        backend: &mut ItemRendererRef,
184        _self_rc: &ItemRc,
185        size: LogicalSize,
186    ) -> RenderingResult {
187        (*backend).combine_clip(
188            LogicalRect::new(LogicalPoint::default(), size),
189            LogicalBorderRadius::zero(),
190            LogicalLength::zero(),
191        );
192        RenderingResult::ContinueRenderingChildren
193    }
194
195    fn bounding_rect(
196        self: core::pin::Pin<&Self>,
197        _window_adapter: &Rc<dyn WindowAdapter>,
198        _self_rc: &ItemRc,
199        geometry: LogicalRect,
200    ) -> LogicalRect {
201        geometry
202    }
203
204    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
205        true
206    }
207}
208
209impl ItemConsts for Flickable {
210    const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
211        Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
212}
213
214#[repr(C)]
215/// Wraps the internal data structure for the Flickable
216pub struct FlickableDataBox(core::ptr::NonNull<FlickableData>);
217
218impl Default for FlickableDataBox {
219    fn default() -> Self {
220        FlickableDataBox(Box::leak(Box::<FlickableData>::default()).into())
221    }
222}
223impl Drop for FlickableDataBox {
224    fn drop(&mut self) {
225        // Safety: the self.0 was constructed from a Box::leak in FlickableDataBox::default
226        drop(unsafe { Box::from_raw(self.0.as_ptr()) });
227    }
228}
229
230impl core::ops::Deref for FlickableDataBox {
231    type Target = FlickableData;
232    fn deref(&self) -> &Self::Target {
233        // Safety: initialized in FlickableDataBox::default
234        unsafe { self.0.as_ref() }
235    }
236}
237
238/// The distance required before it starts flicking if there is another item intercepting the mouse.
239pub(super) const DISTANCE_THRESHOLD: LogicalLength = LogicalLength::new(8 as _);
240/// Time required before we stop caring about child event if the mouse hasn't been moved
241pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
242/// The delay to which press are forwarded to the inner item
243pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
244
245#[derive(Default, Debug)]
246struct FlickableDataInner {
247    /// The position in which the press was made
248    pressed_pos: LogicalPoint,
249    pressed_time: Option<Instant>,
250    pressed_viewport_pos: LogicalPoint,
251    pressed_viewport_size: LogicalSize,
252    /// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children
253    capture_events: bool,
254}
255
256#[derive(Default, Debug)]
257pub struct FlickableData {
258    inner: RefCell<FlickableDataInner>,
259    /// Tracker that tracks the property to make sure that the flickable is in bounds
260    in_bound_change_handler: crate::properties::ChangeTracker,
261}
262
263impl FlickableData {
264    fn handle_mouse_filter(
265        &self,
266        flick: Pin<&Flickable>,
267        event: &MouseEvent,
268        flick_rc: &ItemRc,
269    ) -> InputEventFilterResult {
270        let mut inner = self.inner.borrow_mut();
271        match event {
272            MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
273                inner.pressed_pos = *position;
274                inner.pressed_time = Some(crate::animations::current_tick());
275                inner.pressed_viewport_pos = LogicalPoint::from_lengths(
276                    (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
277                    (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
278                );
279                inner.pressed_viewport_size = LogicalSize::from_lengths(
280                    (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get(),
281                    (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get(),
282                );
283                if inner.capture_events {
284                    InputEventFilterResult::Intercept
285                } else {
286                    InputEventFilterResult::DelayForwarding(FORWARD_DELAY.as_millis() as _)
287                }
288            }
289            MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => {
290                let was_capturing = inner.capture_events;
291                Self::mouse_released(&mut inner, flick, event, flick_rc);
292                if was_capturing {
293                    InputEventFilterResult::Intercept
294                } else {
295                    InputEventFilterResult::ForwardEvent
296                }
297            }
298            MouseEvent::Moved { position } => {
299                let do_intercept = inner.capture_events
300                    || inner.pressed_time.is_some_and(|pressed_time| {
301                        if crate::animations::current_tick() - pressed_time > DURATION_THRESHOLD {
302                            return false;
303                        }
304                        // Check if the mouse was moved more than the DISTANCE_THRESHOLD in a
305                        // direction in which the flickable can flick
306                        let diff = *position - inner.pressed_pos;
307                        let geo = flick_rc.geometry();
308                        let w = geo.width_length();
309                        let h = geo.height_length();
310                        let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
311                        let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
312                        let x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get();
313                        let y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get();
314                        let zero = LogicalLength::zero();
315                        ((vw > w || x != zero) && abs(diff.x_length()) > DISTANCE_THRESHOLD)
316                            || ((vh > h || y != zero) && abs(diff.y_length()) > DISTANCE_THRESHOLD)
317                    });
318                if do_intercept {
319                    InputEventFilterResult::Intercept
320                } else if inner.pressed_time.is_some() {
321                    InputEventFilterResult::ForwardAndInterceptGrab
322                } else {
323                    InputEventFilterResult::ForwardEvent
324                }
325            }
326            MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardEvent,
327            // Not the left button
328            MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
329                InputEventFilterResult::ForwardAndIgnore
330            }
331            MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
332                InputEventFilterResult::ForwardAndIgnore
333            }
334        }
335    }
336
337    fn handle_mouse(
338        &self,
339        flick: Pin<&Flickable>,
340        event: &MouseEvent,
341        window_adapter: &Rc<dyn WindowAdapter>,
342        flick_rc: &ItemRc,
343    ) -> InputEventResult {
344        let mut inner = self.inner.borrow_mut();
345        match event {
346            MouseEvent::Pressed { .. } => {
347                inner.capture_events = true;
348                InputEventResult::GrabMouse
349            }
350            MouseEvent::Exit | MouseEvent::Released { .. } => {
351                let was_capturing = inner.capture_events;
352                Self::mouse_released(&mut inner, flick, event, flick_rc);
353                if was_capturing {
354                    InputEventResult::EventAccepted
355                } else {
356                    InputEventResult::EventIgnored
357                }
358            }
359            MouseEvent::Moved { position } => {
360                if inner.pressed_time.is_some() {
361                    let current_viewport_size = LogicalSize::from_lengths(
362                        (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get(),
363                        (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get(),
364                    );
365
366                    // Update reference points when the size of the viewport changes to
367                    // avoid 'jumping' during scrolling.
368                    // This happens when height estimate of a ListView changes after
369                    // new items are loaded.
370                    if current_viewport_size != inner.pressed_viewport_size {
371                        inner.pressed_viewport_size = current_viewport_size;
372
373                        inner.pressed_viewport_pos = LogicalPoint::from_lengths(
374                            (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
375                            (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
376                        );
377
378                        inner.pressed_pos = *position;
379                    };
380
381                    let new_pos = inner.pressed_viewport_pos + (*position - inner.pressed_pos);
382
383                    let x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
384                    let y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
385                    let should_capture = || {
386                        let geo = flick_rc.geometry();
387                        let w = geo.width_length();
388                        let h = geo.height_length();
389                        let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
390                        let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
391                        let zero = LogicalLength::zero();
392                        ((vw > w || x.get() != zero)
393                            && abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD)
394                            || ((vh > h || y.get() != zero)
395                                && abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD)
396                    };
397
398                    if inner.capture_events || should_capture() {
399                        let new_pos = ensure_in_bound(flick, new_pos, flick_rc);
400
401                        let old_pos = (x.get(), y.get());
402                        x.set(new_pos.x_length());
403                        y.set(new_pos.y_length());
404                        if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
405                            (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
406                        }
407
408                        inner.capture_events = true;
409                        InputEventResult::GrabMouse
410                    } else if abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD
411                        || abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD
412                    {
413                        // drag in a unsupported direction gives up the grab
414                        InputEventResult::EventIgnored
415                    } else {
416                        InputEventResult::EventAccepted
417                    }
418                } else {
419                    inner.capture_events = false;
420                    InputEventResult::EventIgnored
421                }
422            }
423            MouseEvent::Wheel { delta_x, delta_y, .. } => {
424                let delta = if window_adapter.window().0.modifiers.get().shift()
425                    && !cfg!(target_os = "macos")
426                {
427                    // Shift invert coordinate for the purpose of scrolling. But not on macOs because there the OS already take care of the change
428                    LogicalVector::new(*delta_y, *delta_x)
429                } else {
430                    LogicalVector::new(*delta_x, *delta_y)
431                };
432
433                let geo = flick_rc.geometry();
434
435                if (delta.x == 0 as Coord && flick.viewport_height() <= geo.height_length())
436                    || (delta.y == 0 as Coord && flick.viewport_width() <= geo.width_length())
437                {
438                    // Scroll in a orthogonal direction than what is allowed by the flickable
439                    return InputEventResult::EventIgnored;
440                }
441
442                let old_pos = LogicalPoint::from_lengths(
443                    (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
444                    (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
445                );
446                let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc);
447
448                let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
449                let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
450                let old_pos = (viewport_x.get(), viewport_y.get());
451                viewport_x.set(new_pos.x_length());
452                viewport_y.set(new_pos.y_length());
453                if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
454                    (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
455                }
456                InputEventResult::EventAccepted
457            }
458            MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
459        }
460    }
461
462    fn mouse_released(
463        inner: &mut FlickableDataInner,
464        flick: Pin<&Flickable>,
465        event: &MouseEvent,
466        flick_rc: &ItemRc,
467    ) {
468        if let (Some(pressed_time), Some(pos)) = (inner.pressed_time, event.position()) {
469            let dist = (pos - inner.pressed_pos).cast::<f32>();
470
471            let millis = (crate::animations::current_tick() - pressed_time).as_millis();
472            if inner.capture_events
473                && dist.square_length() > (DISTANCE_THRESHOLD.get() * DISTANCE_THRESHOLD.get()) as _
474                && millis > 1
475            {
476                let speed = dist / (millis as f32);
477
478                let duration = 250;
479                let final_pos = ensure_in_bound(
480                    flick,
481                    (inner.pressed_viewport_pos.cast() + dist + speed * (duration as f32)).cast(),
482                    flick_rc,
483                );
484                let anim = PropertyAnimation {
485                    duration,
486                    easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]),
487                    ..PropertyAnimation::default()
488                };
489
490                let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
491                let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
492                let old_pos = (viewport_x.get(), viewport_y.get());
493                viewport_x.set_animated_value(final_pos.x_length(), anim.clone());
494                viewport_y.set_animated_value(final_pos.y_length(), anim);
495                if old_pos.0 != final_pos.x_length() || old_pos.1 != final_pos.y_length() {
496                    (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
497                }
498            }
499        }
500        inner.capture_events = false; // FIXME: should only be set to false once the flick animation is over
501        inner.pressed_time = None;
502    }
503}
504
505fn abs(l: LogicalLength) -> LogicalLength {
506    LogicalLength::new(l.get().abs())
507}
508
509/// Make sure that the point is within the bounds
510fn ensure_in_bound(flick: Pin<&Flickable>, p: LogicalPoint, flick_rc: &ItemRc) -> LogicalPoint {
511    let geo = flick_rc.geometry();
512    let w = geo.width_length();
513    let h = geo.height_length();
514    let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
515    let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
516
517    let min = LogicalPoint::from_lengths(w - vw, h - vh);
518    let max = LogicalPoint::default();
519    p.max(min).min(max)
520}
521
522/// # Safety
523/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
524/// hold a FlickableDataBox
525#[cfg(feature = "ffi")]
526#[unsafe(no_mangle)]
527pub unsafe extern "C" fn slint_flickable_data_init(data: *mut FlickableDataBox) {
528    core::ptr::write(data, FlickableDataBox::default());
529}
530
531/// # Safety
532/// This must be called using a non-null pointer pointing to an initialized FlickableDataBox
533#[cfg(feature = "ffi")]
534#[unsafe(no_mangle)]
535pub unsafe extern "C" fn slint_flickable_data_free(data: *mut FlickableDataBox) {
536    core::ptr::drop_in_place(data);
537}