1use std::rc::Rc;
10
11use dioxus::html::MountedData;
12use dioxus::prelude::*;
13
14use crate::core::{
15 element_point, use_dnd, use_zone_id, use_zone_registry, DragMode, DropOutcome, ParentZone,
16 Rect, ZoneRecord,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct NodeId(pub u64);
22
23impl From<u64> for NodeId {
24 fn from(v: u64) -> Self {
25 Self(v)
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DropIntent {
32 Before,
34 After,
36 Into,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct TreeDropEvent<T> {
43 pub payload: T,
44 pub target: NodeId,
45 pub intent: DropIntent,
46}
47
48pub fn intent_from_offset(y: f64, row_height: f64) -> DropIntent {
54 let h = row_height.max(1.0);
55 let ratio = (y / h).clamp(0.0, 1.0);
56 if ratio < 0.25 {
57 DropIntent::Before
58 } else if ratio > 0.75 {
59 DropIntent::After
60 } else {
61 DropIntent::Into
62 }
63}
64
65pub fn would_create_cycle(
68 parent_of: impl Fn(NodeId) -> Option<NodeId>,
69 dragged: NodeId,
70 target: NodeId,
71) -> bool {
72 if dragged == target {
73 return true;
74 }
75 let mut cursor = Some(target);
76 for _ in 0..10_000 {
78 match cursor {
79 Some(n) if n == dragged => return true,
80 Some(n) => cursor = parent_of(n),
81 None => return false,
82 }
83 }
84 true
85}
86
87#[component]
101pub fn TreeNodeTarget<T: Clone + PartialEq + 'static>(
102 node: NodeId,
104 #[props(default = 28.0)]
106 row_height: f64,
107 #[props(default)]
109 accepts: Option<Callback<(T, DropIntent), bool>>,
110 on_drop: EventHandler<TreeDropEvent<T>>,
111 #[props(default)]
113 label: Option<String>,
114 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
115 children: Element,
116) -> Element {
117 let mut dnd = use_dnd::<T>();
118 let mut registry = use_zone_registry::<T>();
119 let mut intent = use_signal(|| None::<DropIntent>);
120
121 let zone_id = use_zone_id();
123 let parent = try_use_context::<ParentZone>().map(|p| p.0);
124 let mounted = use_signal(|| None::<Rc<MountedData>>);
125 let rect = use_signal(|| None::<Rect>);
126 let registered_accepts = accepts.map(|cb| {
129 Callback::new(move |p: T| {
130 cb.call((p.clone(), DropIntent::Before))
131 || cb.call((p.clone(), DropIntent::After))
132 || cb.call((p, DropIntent::Into))
133 })
134 });
135 let registered_drop = Callback::new(move |o: DropOutcome<T>| {
136 let it = intent_from_offset(o.element.y, row_height);
137 let ok = accepts.map_or(true, |cb| cb.call((o.payload.clone(), it)));
138 if ok {
139 on_drop.call(TreeDropEvent {
140 payload: o.payload,
141 target: node,
142 intent: it,
143 });
144 }
145 });
146 use_hook(|| {
147 registry.register(ZoneRecord {
148 id: zone_id,
149 parent,
150 label: label.clone(),
151 on_drop: registered_drop,
152 accepts: registered_accepts,
153 mounted,
154 rect,
155 });
156 });
157 use_drop(move || {
158 registry.unregister(zone_id);
159 });
160
161 let display_intent = move || -> Option<DropIntent> {
165 if let Some(it) = intent() {
166 return Some(it);
167 }
168 if dnd.dragging() && dnd.mode() == DragMode::Pointer && dnd.over() == Some(zone_id) {
169 let r = (*rect.peek())?;
170 return Some(intent_from_offset(dnd.pointer().y - r.y, row_height));
171 }
172 None
173 };
174 let intent_str = move || match display_intent() {
175 Some(DropIntent::Before) => "before",
176 Some(DropIntent::After) => "after",
177 Some(DropIntent::Into) => "into",
178 None => "",
179 };
180
181 rsx! {
182 div {
183 "data-intent": intent_str(),
184 onmounted: move |evt: Event<MountedData>| {
185 let m: Rc<MountedData> = evt.data();
186 let mut mounted = mounted;
187 let mut rect = rect;
188 mounted.set(Some(m.clone()));
189 spawn(async move {
190 if let Ok(r) = m.get_client_rect().await {
191 rect.set(Some(Rect::new(
192 r.origin.x,
193 r.origin.y,
194 r.size.width,
195 r.size.height,
196 )));
197 }
198 });
199 },
200 ondragover: move |evt: DragEvent| {
201 if !dnd.dragging() {
202 return;
203 }
204 let it = intent_from_offset(element_point(&evt).y, row_height);
205 let ok = match (&accepts, dnd.payload()) {
206 (Some(cb), Some(p)) => cb.call((p, it)),
207 (None, Some(_)) => true,
208 _ => false,
209 };
210 if ok {
211 evt.prevent_default();
212 if intent() != Some(it) {
213 intent.set(Some(it));
214 }
215 } else if intent().is_some() {
216 intent.set(None);
217 }
218 },
219 ondragleave: move |_| {
220 intent.set(None);
221 },
222 ondrop: move |evt: DragEvent| {
223 evt.prevent_default();
224 evt.stop_propagation();
225 let it = intent_from_offset(element_point(&evt).y, row_height);
226 intent.set(None);
227 let ok = match (&accepts, dnd.payload()) {
228 (Some(cb), Some(p)) => cb.call((p, it)),
229 (None, Some(_)) => true,
230 _ => false,
231 };
232 if !ok {
233 return;
234 }
235 if let Some((payload, _)) = dnd.take() {
236 on_drop.call(TreeDropEvent { payload, target: node, intent: it });
237 }
238 },
239 ..attributes,
240 {children}
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn intent_bands() {
251 assert_eq!(intent_from_offset(2.0, 28.0), DropIntent::Before);
252 assert_eq!(intent_from_offset(14.0, 28.0), DropIntent::Into);
253 assert_eq!(intent_from_offset(26.0, 28.0), DropIntent::After);
254 assert_eq!(intent_from_offset(0.0, 0.0), DropIntent::Before);
256 }
257
258 #[test]
259 fn cycle_detection() {
260 let parent = |n: NodeId| match n.0 {
262 3 => Some(NodeId(2)),
263 2 => Some(NodeId(1)),
264 _ => None,
265 };
266 assert!(would_create_cycle(parent, NodeId(1), NodeId(3)));
268 assert!(would_create_cycle(parent, NodeId(2), NodeId(2)));
270 assert!(!would_create_cycle(parent, NodeId(3), NodeId(1)));
272 }
273}