1#![allow(non_snake_case)]
24
25use cranpose_core::{remember, MutableState, State};
26use std::cell::{Cell, RefCell};
27use std::rc::Rc;
28
29pub type DragDeltaHandler = Rc<dyn Fn(f32)>;
31
32struct DraggableStateInner {
33 on_delta: RefCell<DragDeltaHandler>,
34 dragging: MutableState<bool>,
35 offset: Cell<f32>,
39}
40
41#[derive(Clone)]
46pub struct DraggableState {
47 inner: Rc<DraggableStateInner>,
48}
49
50impl PartialEq for DraggableState {
51 fn eq(&self, other: &Self) -> bool {
54 Rc::ptr_eq(&self.inner, &other.inner)
55 }
56}
57
58impl DraggableState {
59 pub fn new(on_delta: impl Fn(f32) + 'static) -> Self {
64 let runtime = cranpose_core::current_runtime_handle()
65 .expect("DraggableState::new requires an active runtime");
66 Self {
67 inner: Rc::new(DraggableStateInner {
68 on_delta: RefCell::new(Rc::new(on_delta)),
69 dragging: MutableState::with_runtime(false, runtime),
70 offset: Cell::new(0.0),
71 }),
72 }
73 }
74
75 pub fn update_handler(&self, on_delta: impl Fn(f32) + 'static) {
80 *self.inner.on_delta.borrow_mut() = Rc::new(on_delta);
81 }
82
83 pub fn is_dragging(&self) -> bool {
87 self.inner.dragging.value()
88 }
89
90 pub fn dragging(&self) -> State<bool> {
92 self.inner.dragging.as_state()
93 }
94
95 pub fn offset(&self) -> f32 {
97 self.inner.offset.get()
98 }
99
100 pub fn drag_by(&self, delta: f32) {
105 if !delta.is_finite() || delta == 0.0 {
106 return;
107 }
108 self.inner.offset.set(self.inner.offset.get() + delta);
109 let handler = Rc::clone(&self.inner.on_delta.borrow());
110 handler(delta);
111 }
112
113 pub(crate) fn identity(&self) -> usize {
117 Rc::as_ptr(&self.inner) as usize
118 }
119
120 pub(crate) fn set_dragging(&self, dragging: bool) {
121 if self.inner.dragging.get_non_reactive() != dragging {
122 self.inner.dragging.set(dragging);
123 }
124 }
125}
126
127pub fn rememberDraggableState(on_delta: impl Fn(f32) + 'static) -> DraggableState {
130 let state = remember(|| DraggableState::new(|_| {})).with(|state| state.clone());
131 state.update_handler(on_delta);
132 state
133}
134
135#[cfg(test)]
136#[path = "tests/draggable_tests.rs"]
137mod draggable_tests;
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use cranpose_core::{DefaultScheduler, Runtime};
143 use std::sync::Arc;
144
145 fn with_runtime<T>(body: impl FnOnce() -> T) -> T {
146 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
147 body()
148 }
149
150 #[test]
155 fn a_remembered_drag_state_survives_recomposition_and_takes_the_new_handler() {
156 use cranpose_core::{location_key, Composition, MemoryApplier};
157
158 let mut composition = Composition::new(MemoryApplier::new());
159 let seen = Rc::new(RefCell::new(Vec::new()));
160 let handles: Rc<RefCell<Vec<DraggableState>>> = Rc::new(RefCell::new(Vec::new()));
161 let pass = Rc::new(Cell::new(0usize));
162
163 let key = location_key(file!(), line!(), column!());
164 for _ in 0..2 {
165 let seen = Rc::clone(&seen);
166 let handles = Rc::clone(&handles);
167 let pass = Rc::clone(&pass);
168 let mut render = move || {
169 let tag = pass.get();
170 pass.set(tag + 1);
171 let recorder = Rc::clone(&seen);
172 let state = rememberDraggableState(move |delta| {
173 recorder.borrow_mut().push((tag, delta));
174 });
175 handles.borrow_mut().push(state);
176 };
177 composition.render(key, &mut render).expect("render");
178 }
179
180 let handles = handles.borrow();
181 assert_eq!(handles.len(), 2);
182 assert!(
183 handles[0] == handles[1],
184 "a remembered state must survive the slot"
185 );
186
187 handles[1].drag_by(3.0);
188 assert_eq!(
189 *seen.borrow(),
190 vec![(1, 3.0)],
191 "the delta must reach the handler the latest pass supplied"
192 );
193 }
194
195 #[test]
196 fn a_drag_delta_reaches_the_current_handler() {
197 with_runtime(|| {
198 let seen = Rc::new(RefCell::new(Vec::new()));
199 let recorder = Rc::clone(&seen);
200 let state = DraggableState::new(move |delta| recorder.borrow_mut().push(delta));
201 state.drag_by(4.0);
202 state.drag_by(-1.5);
203 assert_eq!(seen.borrow().as_slice(), [4.0, -1.5]);
204 assert_eq!(state.offset(), 2.5);
205 });
206 }
207
208 #[test]
209 fn replacing_the_handler_redirects_later_deltas() {
210 with_runtime(|| {
211 let first = Rc::new(Cell::new(0.0));
212 let second = Rc::new(Cell::new(0.0));
213 let recorder = Rc::clone(&first);
214 let state = DraggableState::new(move |delta| recorder.set(recorder.get() + delta));
215 state.drag_by(2.0);
216 let recorder = Rc::clone(&second);
217 state.update_handler(move |delta| recorder.set(recorder.get() + delta));
218 state.drag_by(3.0);
219 assert_eq!(first.get(), 2.0);
220 assert_eq!(second.get(), 3.0);
221 });
222 }
223
224 #[test]
225 fn a_delta_that_is_not_a_movement_is_not_delivered() {
226 with_runtime(|| {
227 let count = Rc::new(Cell::new(0u32));
228 let recorder = Rc::clone(&count);
229 let state = DraggableState::new(move |_| recorder.set(recorder.get() + 1));
230 state.drag_by(0.0);
231 state.drag_by(f32::NAN);
232 assert_eq!(count.get(), 0);
233 assert_eq!(state.offset(), 0.0);
234 });
235 }
236
237 #[test]
238 fn dragging_is_observable_and_clones_share_it() {
239 with_runtime(|| {
240 let state = DraggableState::new(|_| {});
241 let handle = state.clone();
242 assert!(!handle.is_dragging());
243 state.set_dragging(true);
244 assert!(handle.is_dragging());
245 state.set_dragging(false);
246 assert!(!handle.is_dragging());
247 assert!(state == handle);
248 });
249 }
250}