fret_ui_kit/declarative/
touch_pan_scroll.rs1use std::sync::Arc;
2
3use fret_core::{Point, Px};
4use fret_runtime::Model;
5use fret_ui::action::{OnPointerDown, OnPointerMove, OnPointerUp};
6use fret_ui::scroll::ScrollHandle;
7use fret_ui::{ElementContext, Invalidation, UiHost};
8use fret_ui::{action::OnPointerCancel, element::ScrollAxis};
9
10use super::controllable_state::use_controllable_model;
11
12#[derive(Debug, Clone, Copy)]
13pub struct TouchPanToScrollOptions {
14 pub drag_threshold: Px,
15}
16
17impl Default for TouchPanToScrollOptions {
18 fn default() -> Self {
19 Self {
20 drag_threshold: Px(6.0),
21 }
22 }
23}
24
25#[derive(Debug, Default, Clone, Copy)]
26struct TouchPanToScrollState {
27 pointer_id: Option<fret_core::PointerId>,
28 start: Option<Point>,
29 last: Option<Point>,
30 panning: bool,
31}
32
33fn axis_scrollable(handle: &ScrollHandle, axis: ScrollAxis) -> bool {
34 let max = handle.max_offset();
35 match axis {
36 ScrollAxis::X => max.x.0 > 0.01,
37 ScrollAxis::Y => max.y.0 > 0.01,
38 ScrollAxis::Both => max.x.0 > 0.01 || max.y.0 > 0.01,
39 }
40}
41
42fn apply_pan_delta(handle: &ScrollHandle, axis: ScrollAxis, delta: Point) {
43 let prev = handle.offset();
44 let next = match axis {
45 ScrollAxis::X => Point::new(Px(prev.x.0 - delta.x.0), prev.y),
46 ScrollAxis::Y => Point::new(prev.x, Px(prev.y.0 - delta.y.0)),
47 ScrollAxis::Both => Point::new(Px(prev.x.0 - delta.x.0), Px(prev.y.0 - delta.y.0)),
48 };
49 handle.set_offset(next);
50}
51
52#[track_caller]
59pub fn install_touch_pan_to_scroll<H: UiHost>(
60 cx: &mut ElementContext<'_, H>,
61 axis: ScrollAxis,
62 scroll_handle: ScrollHandle,
63 options: TouchPanToScrollOptions,
64) {
65 let state: Model<TouchPanToScrollState> = use_controllable_model(
66 cx,
67 None::<Model<TouchPanToScrollState>>,
68 TouchPanToScrollState::default,
69 )
70 .model();
71
72 let state_c = state.clone();
73 let handle_c = scroll_handle.clone();
74 let on_down: OnPointerDown = Arc::new(move |host, _action_cx, down| {
75 if down.pointer_type != fret_core::PointerType::Touch {
76 return false;
77 }
78 if !axis_scrollable(&handle_c, axis) {
79 return false;
80 }
81
82 let _ = host.models_mut().update(&state_c, |st| {
83 st.pointer_id = Some(down.pointer_id);
84 st.start = Some(down.position);
85 st.last = Some(down.position);
86 st.panning = false;
87 });
88 false
89 });
90
91 let state_c = state.clone();
92 let handle_c = scroll_handle.clone();
93 let on_move: OnPointerMove = Arc::new(move |host, action_cx, mv| {
94 if mv.pointer_type != fret_core::PointerType::Touch {
95 return false;
96 }
97 if !axis_scrollable(&handle_c, axis) {
98 return false;
99 }
100
101 let mut out = false;
102 let _ = host.models_mut().update(&state_c, |st| {
103 if st.pointer_id != Some(mv.pointer_id) {
104 return;
105 }
106 let Some(prev) = st.last else {
107 st.last = Some(mv.position);
108 return;
109 };
110 let Some(start) = st.start else {
111 st.start = Some(mv.position);
112 st.last = Some(mv.position);
113 return;
114 };
115
116 let total_dx = mv.position.x.0 - start.x.0;
117 let total_dy = mv.position.y.0 - start.y.0;
118 if !st.panning {
119 let dist = (total_dx * total_dx + total_dy * total_dy).sqrt();
120 if dist > options.drag_threshold.0.max(0.0) {
121 st.panning = true;
122 }
123 }
124
125 let dx = mv.position.x.0 - prev.x.0;
126 let dy = mv.position.y.0 - prev.y.0;
127 st.last = Some(mv.position);
128
129 if st.panning {
130 apply_pan_delta(&handle_c, axis, Point::new(Px(dx), Px(dy)));
131 out = true;
132 }
133 });
134
135 if out {
136 host.invalidate(Invalidation::HitTestOnly);
137 host.request_redraw(action_cx.window);
138 }
139 out
140 });
141
142 let state_c = state.clone();
143 let on_up: OnPointerUp = Arc::new(move |host, _action_cx, up| {
144 if up.pointer_type != fret_core::PointerType::Touch {
145 return false;
146 }
147 let _ = host.models_mut().update(&state_c, |st| {
148 if st.pointer_id == Some(up.pointer_id) {
149 *st = TouchPanToScrollState::default();
150 }
151 });
152 false
153 });
154
155 let state_c = state.clone();
156 let on_cancel: OnPointerCancel = Arc::new(move |host, _action_cx, cancel| {
157 if cancel.pointer_type != fret_core::PointerType::Touch {
158 return false;
159 }
160 let _ = host.models_mut().update(&state_c, |st| {
161 if st.pointer_id == Some(cancel.pointer_id) {
162 *st = TouchPanToScrollState::default();
163 }
164 });
165 false
166 });
167
168 cx.pointer_region_add_on_pointer_down(on_down);
169 cx.pointer_region_add_on_pointer_move(on_move);
170 cx.pointer_region_add_on_pointer_up(on_up);
171 cx.pointer_region_on_pointer_cancel(on_cancel);
172}