1#![allow(non_snake_case)]
24
25use std::{
26 cell::{Cell, RefCell},
27 rc::Rc,
28};
29
30use cranpose_core::{MutableState, State, remember};
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
130#[track_caller]
133pub fn rememberDraggableState(on_delta: impl Fn(f32) + 'static) -> DraggableState {
134 let state = remember(|| DraggableState::new(|_| {})).with(|state| state.clone());
135 state.update_handler(on_delta);
136 state
137}
138
139#[cfg(test)]
140#[path = "tests/draggable_tests.rs"]
141mod draggable_tests;
142
143#[cfg(test)]
144mod tests {
145 use std::sync::Arc;
146
147 use cranpose_core::{DefaultScheduler, Runtime};
148
149 use super::*;
150
151 fn with_runtime<T>(body: impl FnOnce() -> T) -> T {
152 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
153 body()
154 }
155
156 #[test]
161 fn a_remembered_drag_state_survives_recomposition_and_takes_the_new_handler() {
162 use cranpose_core::{Composition, MemoryApplier, location_key};
163
164 let mut composition = Composition::new(MemoryApplier::new());
165 let seen = Rc::new(RefCell::new(Vec::new()));
166 let handles: Rc<RefCell<Vec<DraggableState>>> = Rc::new(RefCell::new(Vec::new()));
167 let pass = Rc::new(Cell::new(0usize));
168
169 let key = location_key(file!(), line!(), column!());
170 for _ in 0..2 {
171 let seen = Rc::clone(&seen);
172 let handles = Rc::clone(&handles);
173 let pass = Rc::clone(&pass);
174 let mut render = move || {
175 let tag = pass.get();
176 pass.set(tag + 1);
177 let recorder = Rc::clone(&seen);
178 let state = rememberDraggableState(move |delta| {
179 recorder.borrow_mut().push((tag, delta));
180 });
181 handles.borrow_mut().push(state);
182 };
183 composition.render(key, &mut render).expect("render");
184 }
185
186 let handles = handles.borrow();
187 assert_eq!(handles.len(), 2);
188 assert!(
189 handles[0] == handles[1],
190 "a remembered state must survive the slot"
191 );
192
193 handles[1].drag_by(3.0);
194 assert_eq!(
195 *seen.borrow(),
196 vec![(1, 3.0)],
197 "the delta must reach the handler the latest pass supplied"
198 );
199 }
200
201 #[test]
202 fn a_drag_delta_reaches_the_current_handler() {
203 with_runtime(|| {
204 let seen = Rc::new(RefCell::new(Vec::new()));
205 let recorder = Rc::clone(&seen);
206 let state = DraggableState::new(move |delta| recorder.borrow_mut().push(delta));
207 state.drag_by(4.0);
208 state.drag_by(-1.5);
209 assert_eq!(seen.borrow().as_slice(), [4.0, -1.5]);
210 assert_eq!(state.offset(), 2.5);
211 });
212 }
213
214 #[test]
215 fn replacing_the_handler_redirects_later_deltas() {
216 with_runtime(|| {
217 let first = Rc::new(Cell::new(0.0));
218 let second = Rc::new(Cell::new(0.0));
219 let recorder = Rc::clone(&first);
220 let state = DraggableState::new(move |delta| recorder.set(recorder.get() + delta));
221 state.drag_by(2.0);
222 let recorder = Rc::clone(&second);
223 state.update_handler(move |delta| recorder.set(recorder.get() + delta));
224 state.drag_by(3.0);
225 assert_eq!(first.get(), 2.0);
226 assert_eq!(second.get(), 3.0);
227 });
228 }
229
230 #[test]
231 fn a_delta_that_is_not_a_movement_is_not_delivered() {
232 with_runtime(|| {
233 let count = Rc::new(Cell::new(0u32));
234 let recorder = Rc::clone(&count);
235 let state = DraggableState::new(move |_| recorder.set(recorder.get() + 1));
236 state.drag_by(0.0);
237 state.drag_by(f32::NAN);
238 assert_eq!(count.get(), 0);
239 assert_eq!(state.offset(), 0.0);
240 });
241 }
242
243 #[test]
244 fn dragging_is_observable_and_clones_share_it() {
245 with_runtime(|| {
246 let state = DraggableState::new(|_| {});
247 let handle = state.clone();
248 assert!(!handle.is_dragging());
249 state.set_dragging(true);
250 assert!(handle.is_dragging());
251 state.set_dragging(false);
252 assert!(!handle.is_dragging());
253 assert!(state == handle);
254 });
255 }
256}