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