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