Skip to main content

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//! **Snap-back on cancel** needs no Rust at all - it's a CSS recipe. Pointer
31//! drags via `Draggable` use your `DragOverlay`; give the overlay's child
32//! `transition: transform 150ms ease` and render it conditionally on
33//! `dnd.dragging()` - reverting your item's `data-dragging` styles with a
34//! transition produces the settle effect.
35
36use std::rc::Rc;
37
38use dioxus::html::MountedData;
39use dioxus::prelude::*;
40
41use crate::a11y::use_reduced_motion_css;
42use crate::core::{Point, Rect};
43
44/// FLIP animation phase.
45#[derive(Debug, Clone, Copy, PartialEq, Default)]
46enum FlipPhase {
47    /// At rest (transition armed, no transform).
48    #[default]
49    Rest,
50    /// Rendered at the *old* position via an instant inverse transform.
51    Invert(Point),
52}
53
54/// Wraps one list/grid item and glides it to its new position whenever
55/// `epoch` changes. See the module docs for the driving pattern.
56#[component]
57pub fn FlipItem(
58    /// Bump this whenever the surrounding order changes.
59    epoch: usize,
60    /// Transition duration in milliseconds.
61    #[props(default = 200.0)]
62    duration: f64,
63    /// CSS easing function.
64    #[props(default = "ease".to_string())]
65    easing: String,
66    #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
67    children: Element,
68) -> Element {
69    let mounted = use_signal(|| None::<Rc<MountedData>>);
70    let prev = use_signal(|| None::<Rect>);
71    let mut phase = use_signal(FlipPhase::default);
72
73    // First & Last & Invert: on every epoch change, measure the new
74    // position, and if the item moved, snap an inverse transform on.
75    use_effect(use_reactive!(|epoch| {
76        let _ = epoch;
77        let Some(m) = mounted.peek().clone() else {
78            return;
79        };
80        let mut prev = prev;
81        spawn(async move {
82            if let Ok(r) = m.get_client_rect().await {
83                let now = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
84                if let Some(old) = *prev.peek() {
85                    let dx = old.x - now.x;
86                    let dy = old.y - now.y;
87                    if dx != 0.0 || dy != 0.0 {
88                        phase.set(FlipPhase::Invert(Point::new(dx, dy)));
89                    }
90                }
91                prev.set(Some(now));
92            }
93        });
94    }));
95
96    // Play: once the inverted frame has committed, release the transform;
97    // the armed CSS transition glides the item home. (Effects run after the
98    // render commits, giving the browser a painted "old position" frame.)
99    use_effect(move || {
100        if matches!(phase(), FlipPhase::Invert(_)) {
101            phase.set(FlipPhase::Rest);
102        }
103    });
104
105    let style = match phase() {
106        FlipPhase::Invert(d) => {
107            format!(
108                "transform: translate({}px, {}px); transition: none;",
109                d.x, d.y
110            )
111        }
112        FlipPhase::Rest => {
113            format!("transform: none; transition: transform {duration}ms {easing};")
114        }
115    };
116    // The glide is an inline transition; honor prefers-reduced-motion.
117    let reduced_motion_css = use_reduced_motion_css();
118
119    rsx! {
120        {reduced_motion_css}
121        div {
122            style: "{style}",
123            "data-dnd-motion": true,
124            onmounted: move |evt: Event<MountedData>| {
125                let mut mounted = mounted;
126                mounted.set(Some(evt.data()));
127            },
128            ..attributes,
129            {children}
130        }
131    }
132}