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_zone_id, use_zone_registry, DragMode, DropOutcome, ParentZone,
10 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 let mut label_now = use_signal(|| label.clone());
136 let mut accepts_now = use_signal(|| accepts);
137 let mut row_height_now = use_signal(|| row_height);
138 let mut on_drop_now = use_signal(|| on_drop);
139 let mut node_now = use_signal(|| node);
140
141 if *label_now.peek() != label {
142 label_now.set(label.clone());
143 }
144 if *accepts_now.peek() != accepts {
145 accepts_now.set(accepts);
146 }
147 if *row_height_now.peek() != row_height {
148 row_height_now.set(row_height);
149 }
150 if *on_drop_now.peek() != on_drop {
151 on_drop_now.set(on_drop);
152 }
153 if *node_now.peek() != node {
154 node_now.set(node);
155 }
156
157 let zone_id = use_zone_id();
159 let parent = try_use_context::<ParentZone>().map(|p| p.0);
160 let registered_accepts = Callback::new(move |p: T| match *accepts_now.peek() {
163 Some(cb) => {
164 cb.call((p.clone(), DropIntent::Before))
165 || cb.call((p.clone(), DropIntent::After))
166 || cb.call((p, DropIntent::Into))
167 }
168 None => true,
169 });
170 let registered_drop = Callback::new(move |o: DropOutcome<T>| {
171 let it = intent_from_offset(o.element.y, *row_height_now.peek());
172 let ok = match *accepts_now.peek() {
173 Some(cb) => cb.call((o.payload.clone(), it)),
174 None => true,
175 };
176 if ok {
177 on_drop_now.peek().call(TreeDropEvent {
178 payload: o.payload,
179 target: *node_now.peek(),
180 intent: it,
181 });
182 }
183 });
184 let registration = use_hook(move || {
185 registry.register(ZoneRecord {
186 id: zone_id,
187 parent,
188 label: label_now.peek().clone(),
189 on_drop: registered_drop,
190 accepts: Some(registered_accepts),
191 mounted: None,
192 rect: None,
193 })
194 });
195 use_drop(move || {
196 registry.unregister(zone_id);
197 });
198 registry.sync_label(zone_id, label.clone());
201
202 let display_intent = move || -> Option<DropIntent> {
205 let over = match joined {
206 Some(joined) => joined.is_over(zone_id),
207 None => dnd.over() == Some(zone_id),
208 };
209 if dnd.dragging() && dnd.mode() == DragMode::Pointer && over {
210 let r = registry.cached_rect(zone_id)?;
211 let pointer = joined
212 .and_then(|joined| joined.local_pointer())
213 .unwrap_or_else(|| dnd.pointer());
214 return Some(intent_from_offset(pointer.y - r.y, row_height));
215 }
216 None
217 };
218 let intent_str = move || -> Option<&'static str> {
219 match display_intent() {
220 Some(DropIntent::Before) => Some("before"),
221 Some(DropIntent::After) => Some("after"),
222 Some(DropIntent::Into) => Some("into"),
223 None => None,
224 }
225 };
226 rsx! {
227 div {
228 "data-intent": intent_str(),
229 onmounted: move |evt: Event<MountedData>| {
230 let m: Rc<MountedData> = evt.data();
231 let mut registry = registry;
232 registry.set_mounted(registration, m.clone());
233 spawn(async move {
234 if let Ok(r) = m.get_client_rect().await {
235 registry.set_rect_if_present(registration, Rect::new(
236 r.origin.x,
237 r.origin.y,
238 r.size.width,
239 r.size.height,
240 ));
241 }
242 });
243 },
244 ..attributes,
245 {children}
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn node_id_from_u64() {
256 assert_eq!(NodeId::from(42), NodeId(42));
257 }
258
259 #[test]
260 fn intent_bands() {
261 assert_eq!(intent_from_offset(2.0, 28.0), DropIntent::Before);
262 assert_eq!(intent_from_offset(14.0, 28.0), DropIntent::Into);
263 assert_eq!(intent_from_offset(26.0, 28.0), DropIntent::After);
264 assert_eq!(intent_from_offset(0.0, 0.0), DropIntent::Before);
266 }
267
268 #[test]
269 fn intent_bands_use_quarter_boundaries() {
270 assert_eq!(intent_from_offset(24.9, 100.0), DropIntent::Before);
271 assert_eq!(intent_from_offset(25.0, 100.0), DropIntent::Into);
272 assert_eq!(intent_from_offset(75.0, 100.0), DropIntent::Into);
273 assert_eq!(intent_from_offset(75.1, 100.0), DropIntent::After);
274 }
275
276 #[test]
277 fn intent_bands_clamp_out_of_range_offsets() {
278 assert_eq!(intent_from_offset(-20.0, 100.0), DropIntent::Before);
279 assert_eq!(intent_from_offset(120.0, 100.0), DropIntent::After);
280 assert_eq!(intent_from_offset(50.0, -10.0), DropIntent::After);
281 }
282
283 #[test]
284 fn cycle_detection() {
285 let parent = |n: NodeId| match n.0 {
287 3 => Some(NodeId(2)),
288 2 => Some(NodeId(1)),
289 _ => None,
290 };
291 assert!(would_create_cycle(parent, NodeId(1), NodeId(3)));
293 assert!(would_create_cycle(parent, NodeId(2), NodeId(2)));
295 assert!(!would_create_cycle(parent, NodeId(3), NodeId(1)));
297 }
298
299 #[test]
300 fn cycle_detection_handles_missing_parents() {
301 let parent = |n: NodeId| match n.0 {
302 9 => Some(NodeId(8)),
303 _ => None,
304 };
305
306 assert!(!would_create_cycle(parent, NodeId(1), NodeId(9)));
307 assert!(!would_create_cycle(parent, NodeId(9), NodeId(1)));
308 }
309
310 #[test]
311 fn cycle_detection_treats_parent_map_cycles_as_unsafe() {
312 let parent = |n: NodeId| match n.0 {
313 2 => Some(NodeId(3)),
314 3 => Some(NodeId(2)),
315 _ => None,
316 };
317
318 assert!(would_create_cycle(parent, NodeId(1), NodeId(2)));
319 }
320}