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;
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#[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 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 |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 |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)]
205pub 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 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 unsafe { self.0.as_ref() }
225 }
226}
227
228pub(super) const DISTANCE_THRESHOLD: LogicalLength = LogicalLength::new(8 as _);
230pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
232pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
234
235#[derive(Default, Debug)]
236struct FlickableDataInner {
237 pressed_pos: LogicalPoint,
239 pressed_time: Option<Instant>,
240 pressed_viewport_pos: LogicalPoint,
241 capture_events: bool,
243}
244
245#[derive(Default, Debug)]
246pub struct FlickableData {
247 inner: RefCell<FlickableDataInner>,
248 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 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 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 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 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; inner.pressed_time = None;
452 }
453}
454
455fn abs(l: LogicalLength) -> LogicalLength {
456 LogicalLength::new(l.get().abs())
457}
458
459fn 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#[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#[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}