Skip to main content

dioxus_dnd/
debug.rs

1#![doc = include_str!("../docs/api/debugging.md")]
2
3use dioxus::prelude::*;
4
5use crate::core::components::drop_query;
6use crate::core::{use_dnd, use_joined_window, use_zone_registry};
7
8/// Draws every registered zone of one payload world as a tinted outline
9/// (color derived from the zone id, so it's stable across renders), with
10/// the zone's label and id in a tag, live `data-over` highlighting, and
11/// per-zone acceptance state while a drag is in flight. Render one per
12/// provider, anywhere inside it. **Dev-only** - see the module docs.
13///
14/// Click-through by design (`pointer-events: none`), so it never changes
15/// the interaction it inspects. Zones the registry hasn't measured yet
16/// draw no outline; the chip counts them so absence is visible too.
17#[component]
18pub fn DndDebugOverlay<T: Clone + PartialEq + 'static>(
19    /// Internal marker; never set this.
20    #[props(default)]
21    phantom: std::marker::PhantomData<T>,
22) -> Element {
23    let _ = phantom;
24    let dnd = use_dnd::<T>();
25    let joined = use_joined_window::<T>();
26    let registry = use_zone_registry::<T>();
27
28    // The core only measures rects at drag start; an inspector wants
29    // outlines while idle. Re-measure whenever the zone set changes or a
30    // zone's DOM handle arrives. The registry exposes a separate revision
31    // for those events so the rect writes this triggers cannot loop.
32    use_effect(move || {
33        registry.track_mounts();
34        registry.refresh_rects();
35    });
36
37    let payload = dnd.payload();
38    let over = dnd.over();
39    let records = registry.records();
40    let unmeasured = records.iter().filter(|z| z.rect.is_none()).count();
41    let status = match (dnd.dragging(), over) {
42        (false, _) => format!("{} zones ({unmeasured} unmeasured) - idle", records.len()),
43        (true, Some(z)) => format!("dragging - over zone {}", z.0),
44        (true, None) => "dragging - over nothing".to_string(),
45    };
46
47    rsx! {
48        div {
49            "data-dnd-debug": "true",
50            style: "position: fixed; inset: 0; pointer-events: none; z-index: 9998; \
51                    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;",
52            for record in records {
53                {
54                    let id = record.id;
55                    let rect = record.cached_rect();
56                    // Stable per-id tint; the multiplier scatters neighbors
57                    // around the wheel.
58                    let hue = (id.0.wrapping_mul(47)) % 360;
59                    let accepts = payload.as_ref().map(|payload| {
60                        let query = drop_query(&dnd, payload.clone(), dnd.proposed_effect());
61                        registry.negotiate_zone(id, &query).is_some()
62                    });
63                    let is_over = match joined {
64                        Some(joined) => joined.is_over(id),
65                        None => over == Some(id),
66                    };
67                    let name = record.label.clone().unwrap_or_else(|| "zone".to_string());
68                    rsx! {
69                        if let Some(r) = rect {
70                            div {
71                                key: "{id.0}",
72                                "data-debug-zone": "{id.0}",
73                                "data-over": if is_over { "true" },
74                                "data-accepts": accepts.map(|a| if a { "true" } else { "false" }),
75                                style: format!(
76                                    "position: fixed; left: {}px; top: {}px; width: {}px; height: {}px; \
77                                     box-sizing: border-box; border: 2px {} hsl({hue} 70% 42%); \
78                                     background: hsl({hue} 70% 42% / {}); opacity: {};",
79                                    r.x, r.y, r.width, r.height,
80                                    if accepts == Some(false) { "dashed" } else { "solid" },
81                                    if is_over { "0.18" } else { "0.04" },
82                                    if accepts == Some(false) { "0.45" } else { "1" },
83                                ),
84                                span {
85                                    style: "position: absolute; top: 0; left: 0; transform: translateY(-100%); \
86                                            background: hsl({hue} 70% 42%); color: #fff; font-size: 10px; \
87                                            line-height: 1.6; padding: 0 4px; white-space: nowrap;",
88                                    "{name} #{id.0}"
89                                    if accepts == Some(false) { " - rejects" }
90                                    if is_over { " - over" }
91                                }
92                            }
93                        }
94                    }
95                }
96            }
97            div {
98                "data-debug-status": "true",
99                style: "position: fixed; right: 8px; bottom: 8px; background: #1a1a1a; color: #fff; \
100                        font-size: 11px; line-height: 1; padding: 6px 8px; border-radius: 6px; \
101                        opacity: 0.85;",
102                "{status}"
103            }
104        }
105    }
106}