dioxus_dnd/animate.rs
1//! Drop animations. **Experimental** - this module is the one part of the
2//! crate whose behavior depends on browser paint timing rather than pure
3//! logic; validate it in your target renderer and tune `duration` to taste.
4//!
5//! [`FlipItem`] implements the FLIP technique (First–Last–Invert–Play) for
6//! reorder transitions: when your list order changes, each item measures
7//! where it moved from, renders instantly *back* at its old position via a
8//! transform, then releases the transform with a CSS transition - so tiles
9//! appear to glide to their new slots.
10//!
11//! You drive it with an `epoch` counter: bump it whenever order changes.
12//!
13//! ```text
14//! let mut items = use_signal(|| vec![/* … */]);
15//! let mut epoch = use_signal(|| 0usize);
16//! rsx! {
17//! SortableList {
18//! len: items.read().len(),
19//! render: move |ix: usize| rsx! {
20//! FlipItem { epoch: epoch(), Row { item: items.read()[ix].clone() } }
21//! },
22//! on_sort: move |ev: SortEvent| {
23//! apply_sort(&mut items.write(), ev);
24//! epoch += 1;
25//! },
26//! }
27//! }
28//! ```
29//!
30//! **Drop-settle** (the ghost gliding into the receiving zone on drop) is
31//! built in: set `settle: true` on
32//! [`crate::core::components::DragOverlay`]. **Snap-back on cancel** needs
33//! no Rust at all - it's a CSS recipe: give the overlay's child
34//! `transition: transform 150ms ease` and revert your item's
35//! `data-dragging` styles with a transition.
36
37use std::rc::Rc;
38
39use dioxus::html::MountedData;
40use dioxus::prelude::*;
41
42use crate::a11y::use_reduced_motion_css;
43use crate::core::{Point, Rect};
44
45/// FLIP animation phase.
46#[derive(Debug, Clone, Copy, PartialEq, Default)]
47enum FlipPhase {
48 /// At rest (transition armed, no transform).
49 #[default]
50 Rest,
51 /// Rendered at the *old* position via an instant inverse transform.
52 Invert(Point),
53}
54
55/// Wraps one list/grid item and glides it to its new position whenever
56/// `epoch` changes. See the module docs for the driving pattern.
57#[component]
58pub fn FlipItem(
59 /// Bump this whenever the surrounding order changes.
60 epoch: usize,
61 /// Transition duration in milliseconds.
62 #[props(default = 200.0)]
63 duration: f64,
64 /// CSS easing function.
65 #[props(default = "ease".to_string())]
66 easing: String,
67 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
68 children: Element,
69) -> Element {
70 let mounted = use_signal(|| None::<Rc<MountedData>>);
71 let prev = use_signal(|| None::<Rect>);
72 let mut phase = use_signal(FlipPhase::default);
73
74 // First & Last & Invert: on every epoch change, measure the new
75 // position, and if the item moved, snap an inverse transform on.
76 use_effect(use_reactive!(|epoch| {
77 let _ = epoch;
78 let Some(m) = mounted.peek().clone() else {
79 return;
80 };
81 let mut prev = prev;
82 spawn(async move {
83 if let Ok(r) = m.get_client_rect().await {
84 let now = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
85 if let Some(old) = *prev.peek() {
86 let dx = old.x - now.x;
87 let dy = old.y - now.y;
88 if dx != 0.0 || dy != 0.0 {
89 phase.set(FlipPhase::Invert(Point::new(dx, dy)));
90 }
91 }
92 prev.set(Some(now));
93 }
94 });
95 }));
96
97 // Play: once the inverted frame has committed, release the transform;
98 // the armed CSS transition glides the item home. (Effects run after the
99 // render commits, giving the browser a painted "old position" frame.)
100 use_effect(move || {
101 if matches!(phase(), FlipPhase::Invert(_)) {
102 phase.set(FlipPhase::Rest);
103 }
104 });
105
106 let style = match phase() {
107 FlipPhase::Invert(d) => {
108 format!(
109 "transform: translate({}px, {}px); transition: none;",
110 d.x, d.y
111 )
112 }
113 FlipPhase::Rest => {
114 format!("transform: none; transition: transform {duration}ms {easing};")
115 }
116 };
117 // The glide is an inline transition; honor prefers-reduced-motion.
118 let reduced_motion_css = use_reduced_motion_css();
119
120 rsx! {
121 {reduced_motion_css}
122 div {
123 style: "{style}",
124 "data-dnd-motion": true,
125 onmounted: move |evt: Event<MountedData>| {
126 let mut mounted = mounted;
127 mounted.set(Some(evt.data()));
128 },
129 ..attributes,
130 {children}
131 }
132 }
133}