1use std::rc::Rc;
10
11use dioxus::html::MountedData;
12use dioxus::prelude::*;
13
14use crate::core::{
15 use_dnd, use_zone_id, use_zone_registry, DragMode, DropOutcome, ParentZone, Rect, ZoneRecord,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct NodeId(pub u64);
21
22impl From<u64> for NodeId {
23 fn from(v: u64) -> Self {
24 Self(v)
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DropIntent {
31 Before,
33 After,
35 Into,
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct TreeDropEvent<T> {
42 pub payload: T,
43 pub target: NodeId,
44 pub intent: DropIntent,
45}
46
47pub fn intent_from_offset(y: f64, row_height: f64) -> DropIntent {
53 let h = row_height.max(1.0);
54 let ratio = (y / h).clamp(0.0, 1.0);
55 if ratio < 0.25 {
56 DropIntent::Before
57 } else if ratio > 0.75 {
58 DropIntent::After
59 } else {
60 DropIntent::Into
61 }
62}
63
64pub fn would_create_cycle(
67 parent_of: impl Fn(NodeId) -> Option<NodeId>,
68 dragged: NodeId,
69 target: NodeId,
70) -> bool {
71 if dragged == target {
72 return true;
73 }
74 let mut cursor = Some(target);
75 for _ in 0..10_000 {
77 match cursor {
78 Some(n) if n == dragged => return true,
79 Some(n) => cursor = parent_of(n),
80 None => return false,
81 }
82 }
83 true
84}
85
86#[component]
102pub fn TreeNodeTarget<T: Clone + PartialEq + 'static>(
103 node: NodeId,
105 #[props(default = 28.0)]
111 row_height: f64,
112 #[props(default)]
114 accepts: Option<Callback<(T, DropIntent), bool>>,
115 on_drop: EventHandler<TreeDropEvent<T>>,
116 #[props(default)]
118 label: Option<String>,
119 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
120 children: Element,
121) -> Element {
122 let dnd = use_dnd::<T>();
123 let mut registry = use_zone_registry::<T>();
124 let mut label_now = use_signal(|| label.clone());
125 let mut accepts_now = use_signal(|| accepts);
126 let mut row_height_now = use_signal(|| row_height);
127 let mut on_drop_now = use_signal(|| on_drop);
128 let mut node_now = use_signal(|| node);
129
130 if *label_now.peek() != label {
131 label_now.set(label.clone());
132 }
133 if *accepts_now.peek() != accepts {
134 accepts_now.set(accepts);
135 }
136 if *row_height_now.peek() != row_height {
137 row_height_now.set(row_height);
138 }
139 if *on_drop_now.peek() != on_drop {
140 on_drop_now.set(on_drop);
141 }
142 if *node_now.peek() != node {
143 node_now.set(node);
144 }
145
146 let zone_id = use_zone_id();
148 let parent = try_use_context::<ParentZone>().map(|p| p.0);
149 let mounted = use_signal(|| None::<Rc<MountedData>>);
150 let rect = use_signal(|| None::<Rect>);
151 let registered_accepts = Callback::new(move |p: T| match *accepts_now.peek() {
154 Some(cb) => {
155 cb.call((p.clone(), DropIntent::Before))
156 || cb.call((p.clone(), DropIntent::After))
157 || cb.call((p, DropIntent::Into))
158 }
159 None => true,
160 });
161 let registered_drop = Callback::new(move |o: DropOutcome<T>| {
162 let it = intent_from_offset(o.element.y, *row_height_now.peek());
163 let ok = match *accepts_now.peek() {
164 Some(cb) => cb.call((o.payload.clone(), it)),
165 None => true,
166 };
167 if ok {
168 on_drop_now.peek().call(TreeDropEvent {
169 payload: o.payload,
170 target: *node_now.peek(),
171 intent: it,
172 });
173 }
174 });
175 use_hook(move || {
176 registry.register(ZoneRecord {
177 id: zone_id,
178 parent,
179 label: label_now.peek().clone(),
180 on_drop: registered_drop,
181 accepts: Some(registered_accepts),
182 mounted,
183 rect,
184 })
185 });
186 use_drop(move || {
187 registry.unregister(zone_id);
188 });
189 registry.sync_label(zone_id, label.clone());
192
193 let display_intent = move || -> Option<DropIntent> {
196 if dnd.dragging() && dnd.mode() == DragMode::Pointer && dnd.over() == Some(zone_id) {
197 let r = (*rect.peek())?;
198 return Some(intent_from_offset(dnd.pointer().y - r.y, row_height));
199 }
200 None
201 };
202 let intent_str = move || -> Option<&'static str> {
203 match display_intent() {
204 Some(DropIntent::Before) => Some("before"),
205 Some(DropIntent::After) => Some("after"),
206 Some(DropIntent::Into) => Some("into"),
207 None => None,
208 }
209 };
210 rsx! {
211 div {
212 "data-intent": intent_str(),
213 onmounted: move |evt: Event<MountedData>| {
214 let m: Rc<MountedData> = evt.data();
215 let mut mounted = mounted;
216 let mut rect = rect;
217 mounted.set(Some(m.clone()));
218 spawn(async move {
219 if let Ok(r) = m.get_client_rect().await {
220 rect.set(Some(Rect::new(
221 r.origin.x,
222 r.origin.y,
223 r.size.width,
224 r.size.height,
225 )));
226 }
227 });
228 },
229 ..attributes,
230 {children}
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn node_id_from_u64() {
241 assert_eq!(NodeId::from(42), NodeId(42));
242 }
243
244 #[test]
245 fn intent_bands() {
246 assert_eq!(intent_from_offset(2.0, 28.0), DropIntent::Before);
247 assert_eq!(intent_from_offset(14.0, 28.0), DropIntent::Into);
248 assert_eq!(intent_from_offset(26.0, 28.0), DropIntent::After);
249 assert_eq!(intent_from_offset(0.0, 0.0), DropIntent::Before);
251 }
252
253 #[test]
254 fn intent_bands_use_quarter_boundaries() {
255 assert_eq!(intent_from_offset(24.9, 100.0), DropIntent::Before);
256 assert_eq!(intent_from_offset(25.0, 100.0), DropIntent::Into);
257 assert_eq!(intent_from_offset(75.0, 100.0), DropIntent::Into);
258 assert_eq!(intent_from_offset(75.1, 100.0), DropIntent::After);
259 }
260
261 #[test]
262 fn intent_bands_clamp_out_of_range_offsets() {
263 assert_eq!(intent_from_offset(-20.0, 100.0), DropIntent::Before);
264 assert_eq!(intent_from_offset(120.0, 100.0), DropIntent::After);
265 assert_eq!(intent_from_offset(50.0, -10.0), DropIntent::After);
266 }
267
268 #[test]
269 fn cycle_detection() {
270 let parent = |n: NodeId| match n.0 {
272 3 => Some(NodeId(2)),
273 2 => Some(NodeId(1)),
274 _ => None,
275 };
276 assert!(would_create_cycle(parent, NodeId(1), NodeId(3)));
278 assert!(would_create_cycle(parent, NodeId(2), NodeId(2)));
280 assert!(!would_create_cycle(parent, NodeId(3), NodeId(1)));
282 }
283
284 #[test]
285 fn cycle_detection_handles_missing_parents() {
286 let parent = |n: NodeId| match n.0 {
287 9 => Some(NodeId(8)),
288 _ => None,
289 };
290
291 assert!(!would_create_cycle(parent, NodeId(1), NodeId(9)));
292 assert!(!would_create_cycle(parent, NodeId(9), NodeId(1)));
293 }
294
295 #[test]
296 fn cycle_detection_treats_parent_map_cycles_as_unsafe() {
297 let parent = |n: NodeId| match n.0 {
298 2 => Some(NodeId(3)),
299 3 => Some(NodeId(2)),
300 _ => None,
301 };
302
303 assert!(would_create_cycle(parent, NodeId(1), NodeId(2)));
304 }
305}