1use 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#[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 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 |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 |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)]
215pub 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 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 unsafe { self.0.as_ref() }
235 }
236}
237
238pub(super) const DISTANCE_THRESHOLD: LogicalLength = LogicalLength::new(8 as _);
240pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
242pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
244
245#[derive(Default, Debug)]
246struct FlickableDataInner {
247 pressed_pos: LogicalPoint,
249 pressed_time: Option<Instant>,
250 pressed_viewport_pos: LogicalPoint,
251 pressed_viewport_size: LogicalSize,
252 capture_events: bool,
254}
255
256#[derive(Default, Debug)]
257pub struct FlickableData {
258 inner: RefCell<FlickableDataInner>,
259 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 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 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 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 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 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 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; inner.pressed_time = None;
502 }
503}
504
505fn abs(l: LogicalLength) -> LogicalLength {
506 LogicalLength::new(l.get().abs())
507}
508
509fn 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#[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#[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}