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    /// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children
252    capture_events: bool,
253}
254
255#[derive(Default, Debug)]
256pub struct FlickableData {
257    inner: RefCell<FlickableDataInner>,
258    /// Tracker that tracks the property to make sure that the flickable is in bounds
259    in_bound_change_handler: crate::properties::ChangeTracker,
260}
261
262impl FlickableData {
263    fn handle_mouse_filter(
264        &self,
265        flick: Pin<&Flickable>,
266        event: &MouseEvent,
267        flick_rc: &ItemRc,
268    ) -> InputEventFilterResult {
269        let mut inner = self.inner.borrow_mut();
270        match event {
271            MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
272                inner.pressed_pos = *position;
273                inner.pressed_time = Some(crate::animations::current_tick());
274                inner.pressed_viewport_pos = LogicalPoint::from_lengths(
275                    (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
276                    (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
277                );
278                if inner.capture_events {
279                    InputEventFilterResult::Intercept
280                } else {
281                    InputEventFilterResult::DelayForwarding(FORWARD_DELAY.as_millis() as _)
282                }
283            }
284            MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => {
285                let was_capturing = inner.capture_events;
286                Self::mouse_released(&mut inner, flick, event, flick_rc);
287                if was_capturing {
288                    InputEventFilterResult::Intercept
289                } else {
290                    InputEventFilterResult::ForwardEvent
291                }
292            }
293            MouseEvent::Moved { position } => {
294                let do_intercept = inner.capture_events
295                    || inner.pressed_time.is_some_and(|pressed_time| {
296                        if crate::animations::current_tick() - pressed_time > DURATION_THRESHOLD {
297                            return false;
298                        }
299                        // Check if the mouse was moved more than the DISTANCE_THRESHOLD in a
300                        // direction in which the flickable can flick
301                        let diff = *position - inner.pressed_pos;
302                        let geo = flick_rc.geometry();
303                        let w = geo.width_length();
304                        let h = geo.height_length();
305                        let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
306                        let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
307                        let x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get();
308                        let y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get();
309                        let zero = LogicalLength::zero();
310                        ((vw > w || x != zero) && abs(diff.x_length()) > DISTANCE_THRESHOLD)
311                            || ((vh > h || y != zero) && abs(diff.y_length()) > DISTANCE_THRESHOLD)
312                    });
313                if do_intercept {
314                    InputEventFilterResult::Intercept
315                } else if inner.pressed_time.is_some() {
316                    InputEventFilterResult::ForwardAndInterceptGrab
317                } else {
318                    InputEventFilterResult::ForwardEvent
319                }
320            }
321            MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardEvent,
322            // Not the left button
323            MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
324                InputEventFilterResult::ForwardAndIgnore
325            }
326            MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
327                InputEventFilterResult::ForwardAndIgnore
328            }
329        }
330    }
331
332    fn handle_mouse(
333        &self,
334        flick: Pin<&Flickable>,
335        event: &MouseEvent,
336        window_adapter: &Rc<dyn WindowAdapter>,
337        flick_rc: &ItemRc,
338    ) -> InputEventResult {
339        let mut inner = self.inner.borrow_mut();
340        match event {
341            MouseEvent::Pressed { .. } => {
342                inner.capture_events = true;
343                InputEventResult::GrabMouse
344            }
345            MouseEvent::Exit | MouseEvent::Released { .. } => {
346                let was_capturing = inner.capture_events;
347                Self::mouse_released(&mut inner, flick, event, flick_rc);
348                if was_capturing {
349                    InputEventResult::EventAccepted
350                } else {
351                    InputEventResult::EventIgnored
352                }
353            }
354            MouseEvent::Moved { position } => {
355                if inner.pressed_time.is_some() {
356                    let new_pos = inner.pressed_viewport_pos + (*position - inner.pressed_pos);
357                    let x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
358                    let y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
359                    let should_capture = || {
360                        let geo = flick_rc.geometry();
361                        let w = geo.width_length();
362                        let h = geo.height_length();
363                        let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
364                        let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
365                        let zero = LogicalLength::zero();
366                        ((vw > w || x.get() != zero)
367                            && abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD)
368                            || ((vh > h || y.get() != zero)
369                                && abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD)
370                    };
371
372                    if inner.capture_events || should_capture() {
373                        let new_pos = ensure_in_bound(flick, new_pos, flick_rc);
374
375                        let old_pos = (x.get(), y.get());
376                        x.set(new_pos.x_length());
377                        y.set(new_pos.y_length());
378                        if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
379                            (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
380                        }
381
382                        inner.capture_events = true;
383                        InputEventResult::GrabMouse
384                    } else if abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD
385                        || abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD
386                    {
387                        // drag in a unsupported direction gives up the grab
388                        InputEventResult::EventIgnored
389                    } else {
390                        InputEventResult::EventAccepted
391                    }
392                } else {
393                    inner.capture_events = false;
394                    InputEventResult::EventIgnored
395                }
396            }
397            MouseEvent::Wheel { delta_x, delta_y, .. } => {
398                let delta = if window_adapter.window().0.modifiers.get().shift()
399                    && !cfg!(target_os = "macos")
400                {
401                    // Shift invert coordinate for the purpose of scrolling. But not on macOs because there the OS already take care of the change
402                    LogicalVector::new(*delta_y, *delta_x)
403                } else {
404                    LogicalVector::new(*delta_x, *delta_y)
405                };
406
407                let geo = flick_rc.geometry();
408
409                if (delta.x == 0 as Coord && flick.viewport_height() <= geo.height_length())
410                    || (delta.y == 0 as Coord && flick.viewport_width() <= geo.width_length())
411                {
412                    // Scroll in a orthogonal direction than what is allowed by the flickable
413                    return InputEventResult::EventIgnored;
414                }
415
416                let old_pos = LogicalPoint::from_lengths(
417                    (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
418                    (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
419                );
420                let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc);
421
422                let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
423                let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
424                let old_pos = (viewport_x.get(), viewport_y.get());
425                viewport_x.set(new_pos.x_length());
426                viewport_y.set(new_pos.y_length());
427                if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
428                    (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
429                }
430                InputEventResult::EventAccepted
431            }
432            MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
433        }
434    }
435
436    fn mouse_released(
437        inner: &mut FlickableDataInner,
438        flick: Pin<&Flickable>,
439        event: &MouseEvent,
440        flick_rc: &ItemRc,
441    ) {
442        if let (Some(pressed_time), Some(pos)) = (inner.pressed_time, event.position()) {
443            let dist = (pos - inner.pressed_pos).cast::<f32>();
444
445            let millis = (crate::animations::current_tick() - pressed_time).as_millis();
446            if inner.capture_events
447                && dist.square_length() > (DISTANCE_THRESHOLD.get() * DISTANCE_THRESHOLD.get()) as _
448                && millis > 1
449            {
450                let speed = dist / (millis as f32);
451
452                let duration = 250;
453                let final_pos = ensure_in_bound(
454                    flick,
455                    (inner.pressed_viewport_pos.cast() + dist + speed * (duration as f32)).cast(),
456                    flick_rc,
457                );
458                let anim = PropertyAnimation {
459                    duration,
460                    easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]),
461                    ..PropertyAnimation::default()
462                };
463
464                let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
465                let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
466                let old_pos = (viewport_x.get(), viewport_y.get());
467                viewport_x.set_animated_value(final_pos.x_length(), anim.clone());
468                viewport_y.set_animated_value(final_pos.y_length(), anim);
469                if old_pos.0 != final_pos.x_length() || old_pos.1 != final_pos.y_length() {
470                    (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
471                }
472            }
473        }
474        inner.capture_events = false; // FIXME: should only be set to false once the flick animation is over
475        inner.pressed_time = None;
476    }
477}
478
479fn abs(l: LogicalLength) -> LogicalLength {
480    LogicalLength::new(l.get().abs())
481}
482
483/// Make sure that the point is within the bounds
484fn ensure_in_bound(flick: Pin<&Flickable>, p: LogicalPoint, flick_rc: &ItemRc) -> LogicalPoint {
485    let geo = flick_rc.geometry();
486    let w = geo.width_length();
487    let h = geo.height_length();
488    let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
489    let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
490
491    let min = LogicalPoint::from_lengths(w - vw, h - vh);
492    let max = LogicalPoint::default();
493    p.max(min).min(max)
494}
495
496/// # Safety
497/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
498/// hold a FlickableDataBox
499#[cfg(feature = "ffi")]
500#[unsafe(no_mangle)]
501pub unsafe extern "C" fn slint_flickable_data_init(data: *mut FlickableDataBox) {
502    core::ptr::write(data, FlickableDataBox::default());
503}
504
505/// # Safety
506/// This must be called using a non-null pointer pointing to an initialized FlickableDataBox
507#[cfg(feature = "ffi")]
508#[unsafe(no_mangle)]
509pub unsafe extern "C" fn slint_flickable_data_free(data: *mut FlickableDataBox) {
510    core::ptr::drop_in_place(data);
511}