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 capture_events: bool,
253}
254
255#[derive(Default, Debug)]
256pub struct FlickableData {
257 inner: RefCell<FlickableDataInner>,
258 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 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 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 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 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 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; inner.pressed_time = None;
476 }
477}
478
479fn abs(l: LogicalLength) -> LogicalLength {
480 LogicalLength::new(l.get().abs())
481}
482
483fn 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#[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#[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}