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