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 = Self::geometry_without_virtual_keyboard(&flick_rc);
70
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::geometry_without_virtual_keyboard(self_rc);
115
116 if (pos.x < 0 as _
117 || pos.y < 0 as _
118 || pos.x_length() > geometry.width_length()
119 || pos.y_length() > geometry.height_length())
120 && self.data.inner.borrow().pressed_time.is_none()
121 {
122 return InputEventFilterResult::Intercept;
123 }
124 }
125 if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
126 return InputEventFilterResult::ForwardAndIgnore;
127 }
128 self.data.handle_mouse_filter(self, event, self_rc)
129 }
130
131 fn input_event(
132 self: Pin<&Self>,
133 event: &MouseEvent,
134 window_adapter: &Rc<dyn WindowAdapter>,
135 self_rc: &ItemRc,
136 ) -> InputEventResult {
137 if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
138 return InputEventResult::EventIgnored;
139 }
140 if let Some(pos) = event.position() {
141 let geometry = Self::geometry_without_virtual_keyboard(self_rc);
142 if matches!(event, MouseEvent::Wheel { .. } | MouseEvent::Pressed { .. })
143 && (pos.x < 0 as _
144 || pos.y < 0 as _
145 || pos.x_length() > geometry.width_length()
146 || pos.y_length() > geometry.height_length())
147 {
148 return InputEventResult::EventIgnored;
149 }
150 }
151
152 self.data.handle_mouse(self, event, window_adapter, self_rc)
153 }
154
155 fn capture_key_event(
156 self: Pin<&Self>,
157 _: &KeyEvent,
158 _window_adapter: &Rc<dyn WindowAdapter>,
159 _self_rc: &ItemRc,
160 ) -> KeyEventResult {
161 KeyEventResult::EventIgnored
162 }
163
164 fn key_event(
165 self: Pin<&Self>,
166 _: &KeyEvent,
167 _window_adapter: &Rc<dyn WindowAdapter>,
168 _self_rc: &ItemRc,
169 ) -> KeyEventResult {
170 KeyEventResult::EventIgnored
171 }
172
173 fn focus_event(
174 self: Pin<&Self>,
175 _: &FocusEvent,
176 _window_adapter: &Rc<dyn WindowAdapter>,
177 _self_rc: &ItemRc,
178 ) -> FocusEventResult {
179 FocusEventResult::FocusIgnored
180 }
181
182 fn render(
183 self: Pin<&Self>,
184 backend: &mut ItemRendererRef,
185 _self_rc: &ItemRc,
186 size: LogicalSize,
187 ) -> RenderingResult {
188 (*backend).combine_clip(
189 LogicalRect::new(LogicalPoint::default(), size),
190 LogicalBorderRadius::zero(),
191 LogicalLength::zero(),
192 );
193 RenderingResult::ContinueRenderingChildren
194 }
195
196 fn bounding_rect(
197 self: core::pin::Pin<&Self>,
198 _window_adapter: &Rc<dyn WindowAdapter>,
199 _self_rc: &ItemRc,
200 geometry: LogicalRect,
201 ) -> LogicalRect {
202 geometry
203 }
204
205 fn clips_children(self: core::pin::Pin<&Self>) -> bool {
206 true
207 }
208}
209
210impl ItemConsts for Flickable {
211 const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
212 Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
213}
214
215impl Flickable {
216 fn choose_min_move(
217 current_view_start: Coord, view_len: Coord, content_len: Coord, points: impl Iterator<Item = Coord>,
221 ) -> Coord {
222 let zero = 0 as Coord;
225 let mut lower = Coord::MIN;
226 let mut upper = Coord::MAX;
227
228 for p in points {
229 lower = lower.max(p - (current_view_start + view_len));
230 upper = upper.min(p - current_view_start);
231 }
232
233 if lower > upper {
234 return zero;
237 }
238
239 let max_scroll = (content_len - view_len).max(zero);
241 let tmin = -current_view_start; let tmax = max_scroll - current_view_start; let i_min = lower.max(tmin);
245 let i_max = upper.min(tmax);
246
247 if i_min <= i_max {
248 if zero < i_min {
249 i_min
250 } else if zero > i_max {
251 i_max
252 } else {
253 zero
254 }
255 } else if tmax < lower {
258 tmax
259 } else {
260 tmin
261 }
262 }
263
264 pub(crate) fn reveal_points(self: Pin<&Self>, self_rc: &ItemRc, pts: &[LogicalPoint]) {
267 if pts.is_empty() {
268 return;
269 }
270
271 let geo = Self::geometry_without_virtual_keyboard(self_rc);
273
274 let vw = Self::FIELD_OFFSETS.viewport_width.apply_pin(self).get().0;
276 let vh = Self::FIELD_OFFSETS.viewport_height.apply_pin(self).get().0;
277 let vx = -Self::FIELD_OFFSETS.viewport_x.apply_pin(self).get().0;
278 let vy = -Self::FIELD_OFFSETS.viewport_y.apply_pin(self).get().0;
279
280 let tx = Self::choose_min_move(vx, geo.width(), vw, pts.iter().map(|p| p.x));
282 let ty = Self::choose_min_move(vy, geo.height(), vh, pts.iter().map(|p| p.y));
283
284 let new_vx = vx + tx;
285 let new_vy = vy + ty;
286
287 Self::FIELD_OFFSETS.viewport_x.apply_pin(self).set(euclid::Length::new(-new_vx));
288 Self::FIELD_OFFSETS.viewport_y.apply_pin(self).set(euclid::Length::new(-new_vy));
289 }
290
291 fn geometry_without_virtual_keyboard(self_rc: &ItemRc) -> LogicalRect {
292 let mut geometry = self_rc.geometry();
293
294 if let Some(keyboard_rect) = self_rc.window_adapter().and_then(|window_adapter| {
296 window_adapter.window().virtual_keyboard(crate::InternalToken)
297 }) {
298 let keyboard_top_left = self_rc.map_from_window(keyboard_rect.0.to_euclid());
299 if keyboard_top_left.y > geometry.origin.y {
300 geometry.size.height = keyboard_top_left.y - geometry.origin.y;
301 }
302 }
303 geometry
304 }
305}
306
307#[repr(C)]
308pub struct FlickableDataBox(core::ptr::NonNull<FlickableData>);
310
311impl Default for FlickableDataBox {
312 fn default() -> Self {
313 FlickableDataBox(Box::leak(Box::<FlickableData>::default()).into())
314 }
315}
316impl Drop for FlickableDataBox {
317 fn drop(&mut self) {
318 drop(unsafe { Box::from_raw(self.0.as_ptr()) });
320 }
321}
322
323impl core::ops::Deref for FlickableDataBox {
324 type Target = FlickableData;
325 fn deref(&self) -> &Self::Target {
326 unsafe { self.0.as_ref() }
328 }
329}
330
331pub(super) const DISTANCE_THRESHOLD: LogicalLength = LogicalLength::new(8 as _);
333pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
335pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
337
338#[derive(Default, Debug)]
339struct FlickableDataInner {
340 pressed_pos: LogicalPoint,
342 pressed_time: Option<Instant>,
343 pressed_viewport_pos: LogicalPoint,
344 pressed_viewport_size: LogicalSize,
345 capture_events: bool,
347}
348
349#[derive(Default, Debug)]
350pub struct FlickableData {
351 inner: RefCell<FlickableDataInner>,
352 in_bound_change_handler: crate::properties::ChangeTracker,
354}
355
356impl FlickableData {
357 fn handle_mouse_filter(
358 &self,
359 flick: Pin<&Flickable>,
360 event: &MouseEvent,
361 flick_rc: &ItemRc,
362 ) -> InputEventFilterResult {
363 let mut inner = self.inner.borrow_mut();
364 match event {
365 MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
366 inner.pressed_pos = *position;
367 inner.pressed_time = Some(crate::animations::current_tick());
368 inner.pressed_viewport_pos = LogicalPoint::from_lengths(
369 (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
370 (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
371 );
372 inner.pressed_viewport_size = LogicalSize::from_lengths(
373 (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get(),
374 (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get(),
375 );
376 if inner.capture_events {
377 InputEventFilterResult::Intercept
378 } else {
379 InputEventFilterResult::DelayForwarding(FORWARD_DELAY.as_millis() as _)
380 }
381 }
382 MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => {
383 let was_capturing = inner.capture_events;
384 Self::mouse_released(&mut inner, flick, event, flick_rc);
385 if was_capturing {
386 InputEventFilterResult::Intercept
387 } else {
388 InputEventFilterResult::ForwardEvent
389 }
390 }
391 MouseEvent::Moved { position, .. } => {
392 let do_intercept = inner.capture_events
393 || inner.pressed_time.is_some_and(|pressed_time| {
394 if crate::animations::current_tick() - pressed_time > DURATION_THRESHOLD {
395 return false;
396 }
397 let diff = *position - inner.pressed_pos;
400 let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
401 let w = geo.width_length();
402 let h = geo.height_length();
403 let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
404 let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
405 let x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get();
406 let y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get();
407 let zero = LogicalLength::zero();
408 ((vw > w || x != zero) && abs(diff.x_length()) > DISTANCE_THRESHOLD)
409 || ((vh > h || y != zero) && abs(diff.y_length()) > DISTANCE_THRESHOLD)
410 });
411 if do_intercept {
412 InputEventFilterResult::Intercept
413 } else if inner.pressed_time.is_some() {
414 InputEventFilterResult::ForwardAndInterceptGrab
415 } else {
416 InputEventFilterResult::ForwardEvent
417 }
418 }
419 MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardEvent,
420 MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
422 InputEventFilterResult::ForwardAndIgnore
423 }
424 MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
425 InputEventFilterResult::ForwardAndIgnore
426 }
427 }
428 }
429
430 fn handle_mouse(
431 &self,
432 flick: Pin<&Flickable>,
433 event: &MouseEvent,
434 window_adapter: &Rc<dyn WindowAdapter>,
435 flick_rc: &ItemRc,
436 ) -> InputEventResult {
437 let mut inner = self.inner.borrow_mut();
438 match event {
439 MouseEvent::Pressed { .. } => {
440 inner.capture_events = true;
441 InputEventResult::GrabMouse
442 }
443 MouseEvent::Exit | MouseEvent::Released { .. } => {
444 let was_capturing = inner.capture_events;
445 Self::mouse_released(&mut inner, flick, event, flick_rc);
446 if was_capturing {
447 InputEventResult::EventAccepted
448 } else {
449 InputEventResult::EventIgnored
450 }
451 }
452 MouseEvent::Moved { position, .. } => {
453 if inner.pressed_time.is_some() {
454 let current_viewport_size = LogicalSize::from_lengths(
455 (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get(),
456 (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get(),
457 );
458
459 if current_viewport_size != inner.pressed_viewport_size {
464 inner.pressed_viewport_size = current_viewport_size;
465
466 inner.pressed_viewport_pos = LogicalPoint::from_lengths(
467 (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
468 (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
469 );
470
471 inner.pressed_pos = *position;
472 };
473
474 let new_pos = inner.pressed_viewport_pos + (*position - inner.pressed_pos);
475
476 let x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
477 let y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
478 let should_capture = || {
479 let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
480 let w = geo.width_length();
481 let h = geo.height_length();
482 let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
483 let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
484 let zero = LogicalLength::zero();
485 ((vw > w || x.get() != zero)
486 && abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD)
487 || ((vh > h || y.get() != zero)
488 && abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD)
489 };
490
491 if inner.capture_events || should_capture() {
492 let new_pos = ensure_in_bound(flick, new_pos, flick_rc);
493
494 let old_pos = (x.get(), y.get());
495 x.set(new_pos.x_length());
496 y.set(new_pos.y_length());
497 if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
498 (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
499 }
500
501 inner.capture_events = true;
502 InputEventResult::GrabMouse
503 } else if abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD
504 || abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD
505 {
506 InputEventResult::EventIgnored
508 } else {
509 InputEventResult::EventAccepted
510 }
511 } else {
512 inner.capture_events = false;
513 InputEventResult::EventIgnored
514 }
515 }
516 MouseEvent::Wheel { delta_x, delta_y, .. } => {
517 let delta = if window_adapter.window().0.modifiers.get().shift()
518 && !cfg!(target_os = "macos")
519 {
520 LogicalVector::new(*delta_y, *delta_x)
522 } else {
523 LogicalVector::new(*delta_x, *delta_y)
524 };
525
526 let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
527
528 if (delta.x == 0 as Coord && flick.viewport_height() <= geo.height_length())
529 || (delta.y == 0 as Coord && flick.viewport_width() <= geo.width_length())
530 {
531 return InputEventResult::EventIgnored;
533 }
534
535 let old_pos = LogicalPoint::from_lengths(
536 (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick).get(),
537 (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick).get(),
538 );
539 let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc);
540
541 let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
542 let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
543 let old_pos = (viewport_x.get(), viewport_y.get());
544 viewport_x.set(new_pos.x_length());
545 viewport_y.set(new_pos.y_length());
546 if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
547 (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
548 }
549 InputEventResult::EventAccepted
550 }
551 MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
552 }
553 }
554
555 fn mouse_released(
556 inner: &mut FlickableDataInner,
557 flick: Pin<&Flickable>,
558 event: &MouseEvent,
559 flick_rc: &ItemRc,
560 ) {
561 if let (Some(pressed_time), Some(pos)) = (inner.pressed_time, event.position()) {
562 let dist = (pos - inner.pressed_pos).cast::<f32>();
563
564 let millis = (crate::animations::current_tick() - pressed_time).as_millis();
565 if inner.capture_events
566 && dist.square_length() > (DISTANCE_THRESHOLD.get() * DISTANCE_THRESHOLD.get()) as _
567 && millis > 1
568 {
569 let speed = dist / (millis as f32);
570
571 let duration = 250;
572 let final_pos = ensure_in_bound(
573 flick,
574 (inner.pressed_viewport_pos.cast() + dist + speed * (duration as f32)).cast(),
575 flick_rc,
576 );
577 let anim = PropertyAnimation {
578 duration,
579 easing: EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]),
580 ..PropertyAnimation::default()
581 };
582
583 let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x).apply_pin(flick);
584 let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y).apply_pin(flick);
585 let old_pos = (viewport_x.get(), viewport_y.get());
586 viewport_x.set_animated_value(final_pos.x_length(), anim.clone());
587 viewport_y.set_animated_value(final_pos.y_length(), anim);
588 if old_pos.0 != final_pos.x_length() || old_pos.1 != final_pos.y_length() {
589 (Flickable::FIELD_OFFSETS.flicked).apply_pin(flick).call(&());
590 }
591 }
592 }
593 inner.capture_events = false; inner.pressed_time = None;
595 }
596}
597
598fn abs(l: LogicalLength) -> LogicalLength {
599 LogicalLength::new(l.get().abs())
600}
601
602fn ensure_in_bound(flick: Pin<&Flickable>, p: LogicalPoint, flick_rc: &ItemRc) -> LogicalPoint {
604 let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
605 let w = geo.width_length();
606 let h = geo.height_length();
607 let vw = (Flickable::FIELD_OFFSETS.viewport_width).apply_pin(flick).get();
608 let vh = (Flickable::FIELD_OFFSETS.viewport_height).apply_pin(flick).get();
609
610 let min = LogicalPoint::from_lengths(w - vw, h - vh);
611 let max = LogicalPoint::default();
612 p.max(min).min(max)
613}
614
615#[cfg(feature = "ffi")]
619#[unsafe(no_mangle)]
620pub unsafe extern "C" fn slint_flickable_data_init(data: *mut FlickableDataBox) {
621 unsafe { core::ptr::write(data, FlickableDataBox::default()) };
622}
623
624#[cfg(feature = "ffi")]
627#[unsafe(no_mangle)]
628pub unsafe extern "C" fn slint_flickable_data_free(data: *mut FlickableDataBox) {
629 unsafe {
630 core::ptr::drop_in_place(data);
631 }
632}