Skip to main content

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}