use dioxus::html::MountedData;
use dioxus::prelude::*;
use std::rc::Rc;
use crate::core::hooks::{
use_bridge_world, use_dnd, use_zone_id, use_zone_registry, BridgeGeometry,
};
use crate::core::registry::{ZonePolicy, ZoneRecord};
use crate::core::types::{edge_of, DragMode, DropOutcome, EdgeSet, Rect, ZoneId};
use crate::core::world::use_joined_window;
use crate::core::{DropEffects, DropQuery};
#[derive(Clone, Copy, PartialEq)]
pub struct ParentZone(pub ZoneId);
#[derive(Clone, Copy, PartialEq)]
struct LiveParentZone(Signal<ZoneId>);
#[doc(hidden)]
pub fn use_parent_zone() -> Option<ZoneId> {
let live = try_use_context::<LiveParentZone>();
let fixed = try_use_context::<ParentZone>();
live.map(|parent| *parent.0.read())
.or_else(|| fixed.map(|parent| parent.0))
}
#[doc(hidden)]
#[component]
pub fn BridgeParentZoneBoundary(zone_id: ZoneId, children: Element) -> Element {
let mut live = use_signal(|| zone_id);
use_effect(use_reactive!(|(zone_id)| {
if *live.peek() != zone_id {
live.set(zone_id);
}
}));
provide_context(LiveParentZone(live));
provide_context(ParentZone(zone_id));
rsx! { {children} }
}
#[component]
fn FixedParentZoneBoundary(zone_id: ZoneId, children: Element) -> Element {
provide_context(ParentZone(zone_id));
rsx! { {children} }
}
#[component]
pub fn DropZone<T: Clone + PartialEq + 'static>(
#[props(default)]
id: Option<ZoneId>,
#[props(default)]
label: Option<String>,
#[props(default)]
accepts: Option<Callback<T, bool>>,
#[props(default)]
accepts_query: Option<Callback<DropQuery<T>, bool>>,
#[props(default)]
allowed_effects: DropEffects,
#[props(default)]
edge: Option<EdgeSet>,
on_drop: EventHandler<DropOutcome<T>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let auto_id = use_zone_id();
let zone_id = id.unwrap_or(auto_id);
let parent = use_parent_zone();
rsx! {
for (keyed_zone_id, keyed_parent) in [(zone_id, parent)] {
DropZoneInstance::<T> {
key: "{keyed_zone_id.0}:{keyed_parent:?}",
zone_id: keyed_zone_id,
parent: keyed_parent,
provide_parent: true,
label: label.clone(),
accepts,
accepts_query,
allowed_effects,
edge,
on_drop,
attributes: attributes.clone(),
{children.clone()}
}
}
}
}
#[component]
fn DropZoneInstance<T: Clone + PartialEq + 'static>(
zone_id: ZoneId,
parent: Option<ZoneId>,
provide_parent: bool,
label: Option<String>,
accepts: Option<Callback<T, bool>>,
accepts_query: Option<Callback<DropQuery<T>, bool>>,
allowed_effects: DropEffects,
edge: Option<EdgeSet>,
on_drop: EventHandler<DropOutcome<T>>,
attributes: Vec<Attribute>,
children: Element,
) -> Element {
let dnd = use_dnd::<T>();
let joined = use_joined_window::<T>();
let mut registry = use_zone_registry::<T>();
let registered_label = label.clone();
let registration = use_hook(|| {
registry.register_with_policy(
ZoneRecord {
id: zone_id,
parent,
label: registered_label.clone(),
on_drop: Callback::new(move |outcome: DropOutcome<T>| on_drop.call(outcome)),
accepts,
mounted: None,
rect: None,
},
ZonePolicy {
accepts_query,
allowed_effects,
edge,
},
)
});
use_drop(move || {
registry.unregister_registration(registration);
});
let label_for_sync = label.clone();
use_effect(use_reactive!(|(label_for_sync)| {
registry.sync_label(zone_id, label_for_sync);
}));
use_effect(use_reactive!(|(parent)| {
registry.sync_parent(registration, parent);
}));
use_effect(use_reactive!(|(
accepts,
accepts_query,
allowed_effects,
edge,
)| {
registry.sync_policy(
registration,
accepts,
ZonePolicy {
accepts_query,
allowed_effects,
edge,
},
);
}));
let acceptable = move || -> bool {
let Some(payload) = dnd.payload() else {
return false;
};
let query = super::delivery::drop_query(&dnd, payload.clone(), dnd.proposed_effect());
query.proposed_effect != crate::core::DropEffect::None
&& accepts.is_none_or(|callback| callback.call(payload.clone()))
&& accepts_query.is_none_or(|callback| callback.call(query.clone()))
&& allowed_effects.negotiate(query.proposed_effect).is_some()
};
let is_over = move || match joined {
Some(joined) => joined.is_over(zone_id),
None => dnd.over() == Some(zone_id),
};
let live_edge = move || -> Option<&'static str> {
let set = edge?;
if !is_over() || dnd.mode() != DragMode::Pointer || !acceptable() {
return None;
}
let r = registry.cached_rect(zone_id)?;
let pointer = joined
.and_then(|joined| joined.local_pointer())
.unwrap_or_else(|| dnd.pointer());
Some(edge_of(pointer, r, set).as_str())
};
let mut attributes = attributes;
super::protect_attributes(
&mut attributes,
&["data-active", "data-over", "data-edge", "onmounted"],
);
let content = if provide_parent {
rsx! {
FixedParentZoneBoundary { zone_id, {children} }
}
} else {
children
};
rsx! {
div {
"data-active": if dnd.dragging() && acceptable() { "true" },
"data-over": if is_over() && acceptable() { "true" },
"data-edge": live_edge(),
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
let mut registry = registry;
registry.set_mounted(registration, m.clone());
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
registry.set_rect_if_present(registration, Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
));
}
});
},
..attributes,
{content}
}
}
}
#[component]
pub(crate) fn FlatDropZone<T: Clone + PartialEq + 'static>(
zone_id: ZoneId,
#[props(default)] label: Option<String>,
#[props(default)] accepts_query: Option<Callback<DropQuery<T>, bool>>,
#[props(default)] edge: Option<EdgeSet>,
on_drop: EventHandler<DropOutcome<T>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let parent = use_parent_zone();
rsx! {
for (keyed_zone_id, keyed_parent) in [(zone_id, parent)] {
DropZoneInstance::<T> {
key: "{keyed_zone_id.0}:{keyed_parent:?}",
zone_id: keyed_zone_id,
parent: keyed_parent,
provide_parent: false,
label: label.clone(),
accepts: None,
accepts_query,
allowed_effects: DropEffects::default(),
edge,
on_drop,
attributes: attributes.clone(),
{children.clone()}
}
}
}
}
#[component]
pub fn BridgeDropZone<A: Clone + PartialEq + 'static, B: Clone + PartialEq + 'static>(
#[props(default)]
id: Option<ZoneId>,
#[props(default)]
label: Option<String>,
#[props(default)]
accepts_a: Option<Callback<A, bool>>,
#[props(default)]
accepts_b: Option<Callback<B, bool>>,
on_drop_a: EventHandler<DropOutcome<A>>,
on_drop_b: EventHandler<DropOutcome<B>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let auto_id = use_zone_id();
let zone_id = id.unwrap_or(auto_id);
let parent = use_parent_zone();
rsx! {
for (keyed_zone_id, keyed_parent) in [(zone_id, parent)] {
BridgeDropZoneInstance::<A, B> {
key: "{keyed_zone_id.0}:{keyed_parent:?}",
zone_id: keyed_zone_id,
parent: keyed_parent,
label: label.clone(),
accepts_a,
accepts_b,
on_drop_a,
on_drop_b,
attributes: attributes.clone(),
{children.clone()}
}
}
}
}
#[component]
fn BridgeDropZoneInstance<A: Clone + PartialEq + 'static, B: Clone + PartialEq + 'static>(
zone_id: ZoneId,
parent: Option<ZoneId>,
label: Option<String>,
accepts_a: Option<Callback<A, bool>>,
accepts_b: Option<Callback<B, bool>>,
on_drop_a: EventHandler<DropOutcome<A>>,
on_drop_b: EventHandler<DropOutcome<B>>,
attributes: Vec<Attribute>,
children: Element,
) -> Element {
provide_context(ParentZone(zone_id));
let geometry = use_hook(BridgeGeometry::default);
let a = use_bridge_world::<A>(
zone_id,
parent,
label.clone(),
accepts_a,
on_drop_a,
geometry.clone(),
);
let b = use_bridge_world::<B>(
zone_id,
parent,
label,
accepts_b,
on_drop_b,
geometry.clone(),
);
let mut attributes = attributes;
super::protect_attributes(&mut attributes, &["data-active", "data-over", "onmounted"]);
rsx! {
div {
"data-active": if a.active || b.active { "true" },
"data-over": if a.over || b.over { "true" },
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
geometry.set_mounted(&m);
let geometry = geometry.clone();
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
let rect = Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
);
geometry.set_rect_if_present(rect);
}
});
},
..attributes,
{children}
}
}
}
#[macro_export]
macro_rules! bridge_drop_zone {
(
$(#[$meta:meta])*
$vis:vis $name:ident {
$( ($ty:ty, $accepts:ident, $on_drop:ident) ),+ $(,)?
}
) => {
$(#[$meta])*
#[::dioxus::prelude::component]
#[allow(non_snake_case)]
$vis fn $name(
#[props(default)]
id: ::std::option::Option<$crate::core::ZoneId>,
#[props(default)]
label: ::std::option::Option<::std::string::String>,
$(
#[props(default)]
$accepts: ::std::option::Option<::dioxus::prelude::Callback<$ty, bool>>,
$on_drop: ::dioxus::prelude::EventHandler<$crate::core::DropOutcome<$ty>>,
)+
#[props(extends = div, extends = GlobalAttributes)]
attributes: ::std::vec::Vec<::dioxus::prelude::Attribute>,
children: ::dioxus::prelude::Element,
) -> ::dioxus::prelude::Element {
use ::dioxus::prelude::*;
let auto_id = $crate::core::use_zone_id();
let zone_id = id.unwrap_or(auto_id);
let parent = $crate::core::use_parent_zone();
let geometry = use_hook($crate::core::BridgeGeometry::default);
let mut attributes = attributes;
attributes.retain(|attribute| {
!matches!(attribute.name, "data-active" | "data-over" | "onmounted")
});
let mut active = false;
let mut over = false;
$(
let world = $crate::core::use_bridge_world::<$ty>(
zone_id,
parent,
label.clone(),
$accepts,
$on_drop,
geometry.clone(),
);
active |= world.active;
over |= world.over;
)+
rsx! {
$crate::core::BridgeParentZoneBoundary {
key: "{zone_id.0}",
zone_id,
div {
"data-active": if active { "true" },
"data-over": if over { "true" },
onmounted: move |evt: Event<::dioxus::html::MountedData>| {
let m = evt.data();
geometry.set_mounted(&m);
let geometry = geometry.clone();
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
let rect = $crate::core::Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
);
geometry.set_rect_if_present(rect);
}
});
},
..attributes,
{children}
}
}
}
}
};
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use super::*;
use crate::core::{DndProvider, DragMode, DropEffect, Point};
#[component]
fn ProposedEffectProbe() -> Element {
let mut dnd = use_dnd::<u8>();
use_hook(move || {
dnd.start(
7,
None,
Point::new(10.0, 10.0),
Point::default(),
DropEffect::Move,
DragMode::Pointer,
);
dnd.set_proposed_effect(DropEffect::Copy);
});
rsx! {
DropZone::<u8> {
allowed_effects: DropEffects::COPY,
accepts_query: move |query: DropQuery<u8>| {
query.proposed_effect == DropEffect::Copy
},
on_drop: move |_| {},
"copy target"
}
}
}
fn proposed_effect_app() -> Element {
rsx! {
DndProvider::<u8> { ProposedEffectProbe {} }
}
}
#[test]
fn active_state_uses_the_live_proposed_effect() {
let mut dom = VirtualDom::new(proposed_effect_app);
dom.rebuild_in_place();
let html = dioxus_ssr::render(&dom);
assert!(
html.contains(r#"data-active="true""#),
"copy target stayed dark: {html}"
);
}
#[derive(Clone, Props)]
struct DynamicIdProps {
phase: Rc<Cell<bool>>,
}
impl PartialEq for DynamicIdProps {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.phase, &other.phase)
}
}
fn dynamic_id_app(props: DynamicIdProps) -> Element {
let id = if props.phase.get() {
ZoneId(2)
} else {
ZoneId(1)
};
rsx! {
DndProvider::<u8> {
DropZone::<u8> {
id,
on_drop: move |_| {},
DynamicIdProbe { expected: id }
}
}
}
}
#[component]
fn DynamicIdProbe(expected: ZoneId) -> Element {
let registry = use_zone_registry::<u8>();
let stale = if expected == ZoneId(1) {
ZoneId(2)
} else {
ZoneId(1)
};
assert!(registry.contains(expected));
assert!(!registry.contains(stale));
rsx! { div {} }
}
#[test]
fn changing_an_explicit_id_replaces_the_registered_instance() {
let phase = Rc::new(Cell::new(false));
let mut dom = VirtualDom::new_with_props(
dynamic_id_app,
DynamicIdProps {
phase: phase.clone(),
},
);
dom.rebuild_in_place();
phase.set(true);
dom.mark_dirty(ScopeId::APP);
dom.render_immediate(&mut dioxus::core::NoOpMutations);
}
}