1#![doc = include_str!("../docs/api/trees.md")]
2
3use std::rc::Rc;
4
5use dioxus::html::MountedData;
6use dioxus::prelude::*;
7
8use crate::core::{
9 use_dnd, use_joined_window, use_parent_zone, use_zone_id, use_zone_registry, DragMode,
10 DropOutcome, Rect, ZoneRecord,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
15pub struct NodeId(pub u64);
16
17impl From<u64> for NodeId {
18 fn from(v: u64) -> Self {
19 Self(v)
20 }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DropIntent {
26 Before,
28 After,
30 Into,
32}
33
34#[derive(Debug, Clone, PartialEq)]
39#[non_exhaustive]
40pub struct TreeDropEvent<T> {
41 pub payload: T,
42 pub target: NodeId,
43 pub intent: DropIntent,
44}
45
46impl<T> TreeDropEvent<T> {
47 pub fn new(payload: T, target: NodeId, intent: DropIntent) -> Self {
49 Self {
50 payload,
51 target,
52 intent,
53 }
54 }
55}
56
57pub fn intent_from_offset(y: f64, row_height: f64) -> DropIntent {
63 let h = row_height.max(1.0);
64 let ratio = (y / h).clamp(0.0, 1.0);
65 if ratio < 0.25 {
66 DropIntent::Before
67 } else if ratio > 0.75 {
68 DropIntent::After
69 } else {
70 DropIntent::Into
71 }
72}
73
74pub fn would_create_cycle(
77 parent_of: impl Fn(NodeId) -> Option<NodeId>,
78 dragged: NodeId,
79 target: NodeId,
80) -> bool {
81 if dragged == target {
82 return true;
83 }
84 let mut cursor = Some(target);
85 for _ in 0..10_000 {
87 match cursor {
88 Some(n) if n == dragged => return true,
89 Some(n) => cursor = parent_of(n),
90 None => return false,
91 }
92 }
93 true
94}
95
96#[component]
112pub fn TreeNodeTarget<T: Clone + PartialEq + 'static>(
113 node: NodeId,
115 #[props(default = 28.0)]
121 row_height: f64,
122 #[props(default)]
124 accepts: Option<Callback<(T, DropIntent), bool>>,
125 on_drop: EventHandler<TreeDropEvent<T>>,
126 #[props(default)]
128 label: Option<String>,
129 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
130 children: Element,
131) -> Element {
132 let dnd = use_dnd::<T>();
133 let joined = use_joined_window::<T>();
134 let mut registry = use_zone_registry::<T>();
135
136 let zone_id = use_zone_id();
138 let parent = use_parent_zone();
139 let registered_accepts = use_callback(move |p: T| match accepts {
142 Some(cb) => {
143 cb.call((p.clone(), DropIntent::Before))
144 || cb.call((p.clone(), DropIntent::After))
145 || cb.call((p, DropIntent::Into))
146 }
147 None => true,
148 });
149 let registered_drop = use_callback(move |o: DropOutcome<T>| {
150 let it = intent_from_offset(o.element.y, row_height);
151 let ok = match accepts {
152 Some(cb) => cb.call((o.payload.clone(), it)),
153 None => true,
154 };
155 if ok {
156 on_drop.call(TreeDropEvent {
157 payload: o.payload,
158 target: node,
159 intent: it,
160 });
161 }
162 });
163 let registered_label = label.clone();
164 let registration = use_hook(move || {
165 registry.register(ZoneRecord {
166 id: zone_id,
167 parent,
168 label: registered_label,
169 on_drop: registered_drop,
170 accepts: Some(registered_accepts),
171 mounted: None,
172 rect: None,
173 })
174 });
175 use_drop(move || {
176 registry.unregister_registration(registration);
177 });
178 let label_for_sync = label.clone();
179 use_effect(use_reactive!(|(label_for_sync)| {
180 registry.sync_label(zone_id, label_for_sync);
181 }));
182 use_effect(use_reactive!(|(parent)| {
183 registry.sync_parent(registration, parent);
184 }));
185
186 let display_intent = move || -> Option<DropIntent> {
189 let over = match joined {
190 Some(joined) => joined.is_over(zone_id),
191 None => dnd.over() == Some(zone_id),
192 };
193 if dnd.dragging()
194 && dnd.proposed_effect() != crate::core::DropEffect::None
195 && dnd.mode() == DragMode::Pointer
196 && over
197 {
198 let r = registry.cached_rect(zone_id)?;
199 let pointer = joined
200 .and_then(|joined| joined.local_pointer())
201 .unwrap_or_else(|| dnd.pointer());
202 return Some(intent_from_offset(pointer.y - r.y, row_height));
203 }
204 None
205 };
206 let intent_str = move || -> Option<&'static str> {
207 match display_intent() {
208 Some(DropIntent::Before) => Some("before"),
209 Some(DropIntent::After) => Some("after"),
210 Some(DropIntent::Into) => Some("into"),
211 None => None,
212 }
213 };
214 let mut attributes = attributes;
215 crate::core::components::protect_attributes(&mut attributes, &["data-intent", "onmounted"]);
216 rsx! {
217 div {
218 "data-intent": intent_str(),
219 onmounted: move |evt: Event<MountedData>| {
220 let m: Rc<MountedData> = evt.data();
221 let mut registry = registry;
222 registry.set_mounted(registration, m.clone());
223 spawn(async move {
224 if let Ok(r) = m.get_client_rect().await {
225 registry.set_rect_if_present(registration, Rect::new(
226 r.origin.x,
227 r.origin.y,
228 r.size.width,
229 r.size.height,
230 ));
231 }
232 });
233 },
234 ..attributes,
235 {children}
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn node_id_from_u64() {
246 assert_eq!(NodeId::from(42), NodeId(42));
247 }
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 intent_bands_use_quarter_boundaries() {
260 assert_eq!(intent_from_offset(24.9, 100.0), DropIntent::Before);
261 assert_eq!(intent_from_offset(25.0, 100.0), DropIntent::Into);
262 assert_eq!(intent_from_offset(75.0, 100.0), DropIntent::Into);
263 assert_eq!(intent_from_offset(75.1, 100.0), DropIntent::After);
264 }
265
266 #[test]
267 fn intent_bands_clamp_out_of_range_offsets() {
268 assert_eq!(intent_from_offset(-20.0, 100.0), DropIntent::Before);
269 assert_eq!(intent_from_offset(120.0, 100.0), DropIntent::After);
270 assert_eq!(intent_from_offset(50.0, -10.0), DropIntent::After);
271 }
272
273 #[test]
274 fn cycle_detection() {
275 let parent = |n: NodeId| match n.0 {
277 3 => Some(NodeId(2)),
278 2 => Some(NodeId(1)),
279 _ => None,
280 };
281 assert!(would_create_cycle(parent, NodeId(1), NodeId(3)));
283 assert!(would_create_cycle(parent, NodeId(2), NodeId(2)));
285 assert!(!would_create_cycle(parent, NodeId(3), NodeId(1)));
287 }
288
289 #[test]
290 fn cycle_detection_handles_missing_parents() {
291 let parent = |n: NodeId| match n.0 {
292 9 => Some(NodeId(8)),
293 _ => None,
294 };
295
296 assert!(!would_create_cycle(parent, NodeId(1), NodeId(9)));
297 assert!(!would_create_cycle(parent, NodeId(9), NodeId(1)));
298 }
299
300 #[test]
301 fn cycle_detection_treats_parent_map_cycles_as_unsafe() {
302 let parent = |n: NodeId| match n.0 {
303 2 => Some(NodeId(3)),
304 3 => Some(NodeId(2)),
305 _ => None,
306 };
307
308 assert!(would_create_cycle(parent, NodeId(1), NodeId(2)));
309 }
310}