use dioxus::html::MountedData;
use dioxus::prelude::*;
use std::{cell::Cell, rc::Rc};
use crate::core::hooks::{use_dnd, SettleFlag};
use crate::core::types::{DragId, DragMode, Point, Rect};
use crate::core::world::WorldMembership;
use super::merge_style_invariant_last;
pub(crate) fn overlay_style(pos: Point) -> String {
format!(
"position: fixed; left: {}px; top: {}px; pointer-events: none; z-index: 9999;",
pos.x, pos.y
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct SettleGlide {
generation: u64,
delta: Point,
}
fn glide_for_generation(
glide: Option<SettleGlide>,
generation: Option<u64>,
) -> Option<SettleGlide> {
glide.filter(|glide| Some(glide.generation) == generation)
}
fn overlay_generation_key(generation: Option<u64>) -> String {
generation.map_or_else(
|| "drag".to_string(),
|generation| format!("settle-{generation}"),
)
}
fn cleanup_generation(still_armed: bool, owned: Option<u64>, live: Option<u64>) -> Option<u64> {
if still_armed {
live.or(owned)
} else {
owned
}
}
#[component]
pub fn DragOverlay<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
#[props(default)]
settle: bool,
#[props(default = 200.0)]
duration: f64,
#[props(default = "ease".to_string())]
easing: String,
#[props(default)]
match_source: bool,
#[props(default)]
on_settled: Option<EventHandler<()>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let _ = phantom;
let mut dnd = use_dnd::<T>();
let membership = try_use_context::<WorldMembership<T>>().and_then(|m| m.0);
let settle_token = move || match membership {
Some(joined) => joined.world.settle_token(joined.key),
None => dnd.settling().map(|_| 0),
};
let owned_settle_generation = use_hook(|| Rc::new(Cell::new(None::<u64>)));
let render_settle_generation = settle_token();
owned_settle_generation.set(render_settle_generation);
let settle_capability = use_hook(|| DragId::auto().0);
let flag = try_use_context::<SettleFlag<T>>();
use_hook(move || {
if settle {
if let Some(flag) = flag {
flag.arm(settle_capability);
}
}
});
let effect_owned_settle_generation = Rc::clone(&owned_settle_generation);
use_effect(use_reactive!(|settle| {
if settle {
if let Some(f) = flag {
f.arm(settle_capability);
}
} else if flag.is_some_and(|flag| flag.release(settle_capability)) && dnd.alive() {
match membership {
Some(joined) => {
if let Some(generation) = cleanup_generation(
true,
effect_owned_settle_generation.get(),
joined.world.peek_settle_token(joined.key),
) {
joined
.world
.finish_settle_generation(joined.key, generation);
}
}
None => dnd.finish_settle(),
}
}
}));
use_drop(move || {
let still_armed = flag.is_some_and(|flag| flag.release(settle_capability));
if still_armed && dnd.alive() {
match membership {
Some(joined) => {
let generation = cleanup_generation(
true,
owned_settle_generation.get(),
joined.world.peek_settle_token(joined.key),
);
if let Some(generation) = generation {
joined
.world
.finish_settle_generation(joined.key, generation);
}
}
None => dnd.finish_settle(),
}
}
});
let mut node = use_signal(|| None::<(Option<u64>, Rc<MountedData>)>);
let mut glide = use_signal(|| None::<SettleGlide>);
let reduced_motion_css = crate::a11y::use_reduced_motion_css();
let mut settled = move |generation: u64| {
let finished = match membership {
Some(joined) => joined
.world
.finish_settle_generation(joined.key, generation),
None => {
let was_settling = dnd.settling().is_some();
dnd.finish_settle();
was_settling
}
};
if finished {
if let Some(h) = &on_settled {
h.call(());
}
}
};
let mut from = use_signal(|| None::<Rect>);
let mut measuring = use_signal(|| None::<u64>);
let mut measured_generation = use_signal(|| None::<u64>);
use_effect(move || {
let token = settle_token();
match (dnd.settling(), token) {
(Some(to), Some(generation)) if settle => {
if *measured_generation.peek() != Some(generation) {
measured_generation.set(Some(generation));
from.set(None);
glide.set(None);
}
if let Some(f) = *from.peek() {
let d = to.center() - f.center();
let playing = glide_for_generation(*glide.peek(), Some(generation)).is_some();
if d.x.abs() < 1.0 && d.y.abs() < 1.0 && !playing {
settled(generation);
} else {
glide.set(Some(SettleGlide {
generation,
delta: d,
}));
}
return;
}
if *measuring.peek() == Some(generation) {
return;
}
let mounted_node = node.read().clone();
let Some((node_generation, m)) = mounted_node else {
if dnd.mode() == DragMode::Keyboard {
settled(generation);
}
return;
};
if node_generation != Some(generation) {
return;
}
measuring.set(Some(generation));
spawn(async move {
let r = m.get_client_rect().await;
if *measuring.peek() == Some(generation) {
measuring.set(None);
}
if settle_token() != Some(generation) {
return;
}
let Ok(r) = r else {
settled(generation);
return;
};
let f = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
from.set(Some(f));
let Some(to) = dnd.settling() else {
settled(generation);
return;
};
let d = to.center() - f.center();
if d.x.abs() < 1.0 && d.y.abs() < 1.0 {
settled(generation);
} else {
glide.set(Some(SettleGlide {
generation,
delta: d,
}));
}
});
}
_ => {
if glide.peek().is_some() {
glide.set(None);
}
if from.peek().is_some() {
from.set(None);
}
if measured_generation.peek().is_some() {
measured_generation.set(None);
}
if measuring.peek().is_some() {
measuring.set(None);
}
}
}
});
let settle_generation = render_settle_generation;
let settling = settle && settle_generation.is_some();
if !dnd.dragging() && !settling {
return rsx! {};
}
if dnd.mode() == DragMode::Keyboard {
return rsx! {};
}
if match_source && dnd.dragging() && dnd.source_rect().is_none() {
return rsx! {};
}
let (anchor, scale_ratio) = match membership {
Some(j) => match j.present_overlay(dnd.pointer(), dnd.grab(), settling) {
Some(placed) => placed,
None => return rsx! {},
},
None => (dnd.pointer() - dnd.grab(), 1.0),
};
let size = match_source
.then(|| dnd.source_rect())
.flatten()
.map(|r| {
format!(
" width: {}px; height: {}px; box-sizing: border-box;",
r.width * scale_ratio,
r.height * scale_ratio
)
})
.unwrap_or_default();
let played_glide = glide_for_generation(glide(), settle_generation);
let functional = if settling {
let transform = match played_glide {
Some(glide) => format!("translate({}px, {}px)", glide.delta.x, glide.delta.y),
None => "none".to_string(),
};
format!(
"{}{size} transform: {transform}; transition: transform {duration}ms {easing};",
overlay_style(anchor),
)
} else {
format!("{}{size}", overlay_style(anchor))
};
let overlay_key = overlay_generation_key(settle_generation);
let mut attributes = attributes;
super::protect_attributes(
&mut attributes,
&["data-dnd-motion", "onmounted", "ontransitionend"],
);
let mut invariant_properties = vec!["position", "left", "top", "pointer-events", "z-index"];
if match_source {
invariant_properties.extend(["width", "height", "box-sizing"]);
}
if settling {
invariant_properties.extend(["transform", "transition"]);
}
let style = merge_style_invariant_last(&mut attributes, &functional, &invariant_properties);
rsx! {
for node_key in [overlay_key] {
div {
key: "{node_key}",
style: style.clone(),
"data-dnd-motion": if settle { "true" },
onmounted: move |evt: Event<MountedData>| {
if settle_token() == settle_generation {
node.set(Some((settle_generation, evt.data())));
}
},
ontransitionend: move |_| {
if let Some(glide) = played_glide {
if settle_token() == Some(glide.generation) {
settled(glide.generation);
}
}
},
..attributes.clone(),
{children.clone()}
}
}
{reduced_motion_css}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Props)]
struct DynamicSettleProps {
enabled: Rc<Cell<bool>>,
captured: Rc<Cell<Option<SettleFlag<u8>>>>,
}
impl PartialEq for DynamicSettleProps {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.enabled, &other.enabled) && Rc::ptr_eq(&self.captured, &other.captured)
}
}
#[allow(non_snake_case)]
fn SettleFlagProbe(props: DynamicSettleProps) -> Element {
let mut dnd = use_dnd::<u8>();
props.captured.set(try_use_context::<SettleFlag<u8>>());
use_hook(move || {
dnd.start(
1,
None,
Point::new(10.0, 10.0),
Point::default(),
crate::core::DropEffect::Move,
DragMode::Pointer,
);
});
rsx! {}
}
fn dynamic_settle_app(props: DynamicSettleProps) -> Element {
let enabled = props.enabled.get();
rsx! {
crate::core::DndProvider::<u8> {
SettleFlagProbe { enabled: props.enabled, captured: props.captured }
DragOverlay::<u8> { settle: enabled, "ghost" }
}
}
}
fn flush_effects(dom: &mut VirtualDom) {
for _ in 0..3 {
dom.process_events();
dom.render_immediate(&mut dioxus::core::NoOpMutations);
}
}
#[test]
fn settle_prop_arms_releases_and_renders_motion_css_dynamically() {
let enabled = Rc::new(Cell::new(false));
let captured = Rc::new(Cell::new(None));
let mut dom = VirtualDom::new_with_props(
dynamic_settle_app,
DynamicSettleProps {
enabled: enabled.clone(),
captured: captured.clone(),
},
);
dom.rebuild_in_place();
flush_effects(&mut dom);
assert!(!dom.in_runtime(|| captured.get().unwrap().is_armed()));
let html = dioxus_ssr::render(&dom);
assert!(html.contains("prefers-reduced-motion"));
assert!(!html.contains(r#"data-dnd-motion="true""#));
enabled.set(true);
dom.mark_dirty(ScopeId::APP);
flush_effects(&mut dom);
assert!(dom.in_runtime(|| captured.get().unwrap().is_armed()));
let html = dioxus_ssr::render(&dom);
assert!(html.contains("prefers-reduced-motion"));
assert!(html.contains(r#"data-dnd-motion="true""#), "{html}");
enabled.set(false);
dom.mark_dirty(ScopeId::APP);
flush_effects(&mut dom);
assert!(!dom.in_runtime(|| captured.get().unwrap().is_armed()));
let html = dioxus_ssr::render(&dom);
assert!(html.contains("prefers-reduced-motion"));
assert!(!html.contains(r#"data-dnd-motion="true""#));
}
#[test]
fn stale_glide_is_not_relabelled_as_its_successor() {
let stale = SettleGlide {
generation: 7,
delta: Point::new(10.0, 20.0),
};
assert_eq!(glide_for_generation(Some(stale), Some(7)), Some(stale));
assert_eq!(glide_for_generation(Some(stale), Some(8)), None);
assert_ne!(
overlay_generation_key(Some(7)),
overlay_generation_key(Some(8))
);
assert_eq!(cleanup_generation(false, Some(7), Some(8)), Some(7));
assert_eq!(cleanup_generation(true, None, Some(8)), Some(8));
assert_eq!(cleanup_generation(true, Some(7), Some(8)), Some(8));
}
}
#[component]
pub fn SettleSlot<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
active: bool,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let _ = phantom;
let mut dnd = use_dnd::<T>();
let membership = try_use_context::<WorldMembership<T>>().and_then(|m| m.0);
let mut node = use_signal(|| None::<Rc<MountedData>>);
let settle_token = move || match membership {
Some(joined) => joined.world.settle_token(joined.key),
None => dnd.settling().map(|_| 0),
};
let retarget = move |m: Rc<MountedData>, generation: u64| {
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
if settle_token() == Some(generation) {
dnd.retarget_settle(Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
));
}
}
});
};
use_effect(use_reactive!(|active| {
if active {
if let (Some(m), Some(generation)) = (node.peek().clone(), settle_token()) {
retarget(m, generation);
}
}
}));
let hidden = active && settle_token().is_some();
let mut attributes = attributes;
super::protect_attributes(&mut attributes, &["data-settling", "onmounted"]);
let style = merge_style_invariant_last(
&mut attributes,
if hidden {
"visibility: hidden;"
} else {
"visibility: visible;"
},
&["visibility"],
);
rsx! {
div {
style: style,
"data-settling": if hidden { "true" },
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
node.set(Some(m.clone()));
if active {
if let Some(generation) = settle_token() {
retarget(m, generation);
}
}
},
..attributes,
{children}
}
}
}