nightshade_api/web/
pointer_drag.rs1use std::collections::HashMap;
6
7use leptos::prelude::*;
8
9#[derive(Clone)]
12pub struct DragPayload {
13 pub kind: String,
15 pub id: String,
17 pub label: String,
19}
20
21impl DragPayload {
22 pub fn new(kind: impl Into<String>, id: impl Into<String>, label: impl Into<String>) -> Self {
24 Self {
25 kind: kind.into(),
26 id: id.into(),
27 label: label.into(),
28 }
29 }
30}
31
32const DRAG_THRESHOLD: f64 = 4.0;
33
34#[derive(Clone, Copy)]
38pub struct DragState {
39 payload: RwSignal<Option<DragPayload>>,
40 position: RwSignal<(f64, f64)>,
41 over: RwSignal<Option<String>>,
42 zones: StoredValue<HashMap<String, Callback<DragPayload>>>,
43 pending: StoredValue<Option<(DragPayload, f64, f64)>>,
44}
45
46impl DragState {
47 pub fn active(&self) -> bool {
49 self.payload.get().is_some()
50 }
51
52 pub fn arm(&self, payload: DragPayload, x: f64, y: f64) {
54 self.pending.set_value(Some((payload, x, y)));
55 }
56
57 pub fn payload(&self) -> Option<DragPayload> {
59 self.payload.get()
60 }
61
62 pub fn position(&self) -> (f64, f64) {
64 self.position.get()
65 }
66
67 pub fn over(&self) -> Option<String> {
69 self.over.get()
70 }
71
72 pub fn start(&self, payload: DragPayload, x: f64, y: f64) {
74 self.position.set((x, y));
75 self.payload.set(Some(payload));
76 }
77
78 pub fn set_over(&self, id: String) {
80 if self.payload.get_untracked().is_some() {
81 self.over.set(Some(id));
82 }
83 }
84
85 pub fn clear_over(&self, id: &str) {
87 if self.over.get_untracked().as_deref() == Some(id) {
88 self.over.set(None);
89 }
90 }
91
92 fn register(&self, id: String, on_drop: Callback<DragPayload>) {
93 self.zones.update_value(|zones| {
94 zones.insert(id, on_drop);
95 });
96 }
97
98 fn unregister(&self, id: &str) {
99 self.zones.update_value(|zones| {
100 zones.remove(id);
101 });
102 }
103
104 fn finish(&self) {
105 let payload = self.payload.get_untracked();
106 let over = self.over.get_untracked();
107 if let (Some(payload), Some(over)) = (payload, over) {
108 let callback = self.zones.with_value(|zones| zones.get(&over).copied());
109 if let Some(callback) = callback {
110 callback.run(payload);
111 }
112 }
113 self.payload.set(None);
114 self.over.set(None);
115 }
116}
117
118pub fn provide_drag() -> DragState {
122 let state = DragState {
123 payload: RwSignal::new(None),
124 position: RwSignal::new((0.0, 0.0)),
125 over: RwSignal::new(None),
126 zones: StoredValue::new(HashMap::new()),
127 pending: StoredValue::new(None),
128 };
129 provide_context(state);
130 let move_handle = window_event_listener(
131 leptos::ev::pointermove,
132 move |event: web_sys::PointerEvent| {
133 let x = event.client_x() as f64;
134 let y = event.client_y() as f64;
135 if state.payload.get_untracked().is_some() {
136 state.position.set((x, y));
137 } else if let Some((payload, start_x, start_y)) = state.pending.get_value()
138 && (x - start_x).hypot(y - start_y) > DRAG_THRESHOLD
139 {
140 state.pending.set_value(None);
141 state.start(payload, x, y);
142 }
143 },
144 );
145 let up_handle = window_event_listener(leptos::ev::pointerup, move |_| {
146 state.pending.set_value(None);
147 if state.payload.get_untracked().is_some() {
148 state.finish();
149 }
150 });
151 on_cleanup(move || {
152 move_handle.remove();
153 up_handle.remove();
154 });
155 state
156}
157
158pub fn use_drag() -> DragState {
161 use_context::<DragState>().unwrap_or_else(|| DragState {
162 payload: RwSignal::new(None),
163 position: RwSignal::new((0.0, 0.0)),
164 over: RwSignal::new(None),
165 zones: StoredValue::new(HashMap::new()),
166 pending: StoredValue::new(None),
167 })
168}
169
170#[component]
174pub fn DragSource(
175 #[prop(into)] kind: String,
176 #[prop(into)] id: String,
177 #[prop(into, optional)] label: String,
178 #[prop(into, optional)] class: String,
179 children: Children,
180) -> impl IntoView {
181 let drag = use_drag();
182 let payload = StoredValue::new(DragPayload::new(kind, id, label));
183 view! {
184 <div
185 class=format!("nightshade-drag-source {class}")
186 on:pointerdown=move |event: web_sys::PointerEvent| {
187 drag.arm(payload.get_value(), event.client_x() as f64, event.client_y() as f64);
188 }
189 >
190 {children()}
191 </div>
192 }
193}
194
195#[component]
199pub fn DropZone(
200 #[prop(into)] id: String,
201 on_drop: Callback<DragPayload>,
202 #[prop(into, optional)] class: String,
203 children: Children,
204) -> impl IntoView {
205 let drag = use_drag();
206 let id = StoredValue::new(id);
207 drag.register(id.get_value(), on_drop);
208 on_cleanup(move || drag.unregister(&id.get_value()));
209 let is_over = move || drag.over().as_deref() == Some(id.get_value().as_str());
210 view! {
211 <div
212 class=format!("nightshade-drop-zone {class}")
213 class:over=is_over
214 on:pointerenter=move |_| drag.set_over(id.get_value())
215 on:pointermove=move |_| drag.set_over(id.get_value())
216 on:pointerleave=move |_| drag.clear_over(&id.get_value())
217 >
218 {children()}
219 </div>
220 }
221}
222
223#[component]
226pub fn DragLayer() -> impl IntoView {
227 let drag = use_drag();
228 view! {
229 <Show when=move || drag.active() fallback=|| ()>
230 <div
231 class="nightshade-drag-preview"
232 style=move || {
233 let (x, y) = drag.position();
234 format!("left:{}px;top:{}px", x + 10.0, y + 10.0)
235 }
236 >
237 {move || drag.payload().map(|payload| payload.label)}
238 </div>
239 </Show>
240 }
241}