dioxus_dnd/debug.rs
1//! **Dev-only** drag-and-drop inspector.
2//!
3//! [`DndDebugOverlay`] draws every zone registered in a provider as a
4//! tinted, labeled outline pinned over the page: acceptance state live
5//! while a drag is in flight (rejecting zones dim and go dashed), the
6//! hovered zone filled as the pointer or keyboard moves, and a status chip
7//! with the registry's view of the world. Everything it shows *is* the
8//! registry - if an outline is missing or misplaced, hit-testing sees
9//! exactly the same wrong thing, which is the point.
10//!
11//! This is a development tool: it renders unstyled debug chrome over your
12//! UI and its output is not localized. Gate it yourself and keep it out of
13//! release builds:
14//!
15//! ```text
16//! DndProvider::<Card> {
17//! if cfg!(debug_assertions) {
18//! DndDebugOverlay::<Card> {}
19//! }
20//! // ... your app ...
21//! }
22//! ```
23
24use dioxus::prelude::*;
25
26use crate::core::{use_dnd, use_zone_registry};
27
28/// Draws every registered zone of one payload world as a tinted outline
29/// (color derived from the zone id, so it's stable across renders), with
30/// the zone's label and id in a tag, live `data-over` highlighting, and
31/// per-zone acceptance state while a drag is in flight. Render one per
32/// provider, anywhere inside it. **Dev-only** - see the module docs.
33///
34/// Click-through by design (`pointer-events: none`), so it never changes
35/// the interaction it inspects. Zones the registry hasn't measured yet
36/// draw no outline; the chip counts them so absence is visible too.
37#[component]
38pub fn DndDebugOverlay<T: Clone + PartialEq + 'static>(
39 /// Internal marker; never set this.
40 #[props(default)]
41 phantom: std::marker::PhantomData<T>,
42) -> Element {
43 let _ = phantom;
44 let dnd = use_dnd::<T>();
45 let registry = use_zone_registry::<T>();
46
47 // The core only measures rects at drag start; an inspector wants
48 // outlines while idle. Re-measure whenever the zone set changes or a
49 // zone's DOM handle arrives (both read here, subscribing this effect);
50 // the rect writes this triggers are *not* read here, so no loop.
51 use_effect(move || {
52 for zone in registry.records() {
53 let _ = zone.mounted.read();
54 }
55 registry.refresh_rects();
56 });
57
58 let payload = dnd.payload();
59 let over = dnd.over();
60 let records = registry.records();
61 let unmeasured = records.iter().filter(|z| z.rect.read().is_none()).count();
62 let status = match (dnd.dragging(), over) {
63 (false, _) => format!("{} zones ({unmeasured} unmeasured) - idle", records.len()),
64 (true, Some(z)) => format!("dragging - over zone {}", z.0),
65 (true, None) => "dragging - over nothing".to_string(),
66 };
67
68 rsx! {
69 div {
70 "data-dnd-debug": "true",
71 style: "position: fixed; inset: 0; pointer-events: none; z-index: 9998; \
72 font-family: ui-monospace, SFMono-Regular, Menlo, monospace;",
73 for record in records {
74 {
75 let id = record.id;
76 let rect = (record.rect)();
77 // Stable per-id tint; the multiplier scatters neighbors
78 // around the wheel.
79 let hue = (id.0.wrapping_mul(47)) % 360;
80 let accepts = payload.as_ref().map(|p| record.accepts_payload(p));
81 let is_over = over == Some(id);
82 let name = record.label.clone().unwrap_or_else(|| "zone".to_string());
83 rsx! {
84 if let Some(r) = rect {
85 div {
86 key: "{id.0}",
87 "data-debug-zone": "{id.0}",
88 "data-over": if is_over { "true" },
89 "data-accepts": accepts.map(|a| if a { "true" } else { "false" }),
90 style: format!(
91 "position: fixed; left: {}px; top: {}px; width: {}px; height: {}px; \
92 box-sizing: border-box; border: 2px {} hsl({hue} 70% 42%); \
93 background: hsl({hue} 70% 42% / {}); opacity: {};",
94 r.x, r.y, r.width, r.height,
95 if accepts == Some(false) { "dashed" } else { "solid" },
96 if is_over { "0.18" } else { "0.04" },
97 if accepts == Some(false) { "0.45" } else { "1" },
98 ),
99 span {
100 style: "position: absolute; top: 0; left: 0; transform: translateY(-100%); \
101 background: hsl({hue} 70% 42%); color: #fff; font-size: 10px; \
102 line-height: 1.6; padding: 0 4px; white-space: nowrap;",
103 "{name} #{id.0}"
104 if accepts == Some(false) { " - rejects" }
105 if is_over { " - over" }
106 }
107 }
108 }
109 }
110 }
111 }
112 div {
113 "data-debug-status": "true",
114 style: "position: fixed; right: 8px; bottom: 8px; background: #1a1a1a; color: #fff; \
115 font-size: 11px; line-height: 1; padding: 6px 8px; border-radius: 6px; \
116 opacity: 0.85;",
117 "{status}"
118 }
119 }
120 }
121}