Skip to main content

dioxus_dnd/
animate.rs

1#![doc = include_str!("../docs/api/animation.md")]
2
3use std::rc::Rc;
4
5use dioxus::html::MountedData;
6use dioxus::prelude::*;
7
8use crate::a11y::use_reduced_motion_css;
9use crate::core::{platform, Point, Rect};
10
11/// FLIP animation phase (render-twice fallback only; the `web` path hands
12/// the whole sequence to the DOM in one synchronous step).
13#[derive(Debug, Clone, Copy, PartialEq, Default)]
14enum FlipPhase {
15    /// At rest (transition armed, no transform).
16    #[default]
17    Rest,
18    /// Rendered at the *old* position via an instant inverse transform.
19    Invert(Point),
20}
21
22/// The inline style of an inverted item: parked at its old position, no
23/// transition. Shared by both paths so they cannot drift.
24fn invert_style(d: Point) -> String {
25    format!(
26        "transform: translate({}px, {}px); transition: none;",
27        d.x, d.y
28    )
29}
30
31/// The inline style of an at-rest item: no transform, transition armed.
32/// Also what [`platform::flip_transform`] leaves on the real element, so the
33/// virtual DOM's view of the attribute stays truthful.
34fn rest_style(duration: f64, easing: &str) -> String {
35    format!("transform: none; transition: transform {duration}ms {easing};")
36}
37
38/// Wraps one list/grid item and glides it to its new position whenever
39/// `epoch` changes. See the module docs for the driving pattern.
40#[component]
41pub fn FlipItem(
42    /// Bump this whenever the surrounding order changes.
43    epoch: usize,
44    /// Transition duration in milliseconds.
45    #[props(default = 200.0)]
46    duration: f64,
47    /// CSS easing function.
48    #[props(default = "ease".to_string())]
49    easing: String,
50    #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
51    children: Element,
52) -> Element {
53    let mounted = use_signal(|| None::<Rc<MountedData>>);
54    let prev = use_signal(|| None::<Rect>);
55    let mut phase = use_signal(FlipPhase::default);
56
57    // First & Last & Invert: on every epoch change, measure the new
58    // position, and if the item moved, run the glide. The synchronous DOM
59    // handoff is preferred; when it isn't available, snap the inverse
60    // transform on through a render instead.
61    use_effect(use_reactive!(|epoch, duration, easing| {
62        let _ = epoch;
63        let Some(m) = mounted.peek().clone() else {
64            return;
65        };
66        let mut prev = prev;
67        spawn(async move {
68            if let Ok(r) = m.get_client_rect().await {
69                let now = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
70                if let Some(old) = *prev.peek() {
71                    let d = Point::new(old.x - now.x, old.y - now.y);
72                    if d.x != 0.0 || d.y != 0.0 {
73                        let handed_off = platform::flip_transform(
74                            &m,
75                            &invert_style(d),
76                            &rest_style(duration, &easing),
77                        );
78                        if !handed_off {
79                            phase.set(FlipPhase::Invert(d));
80                        }
81                    }
82                }
83                prev.set(Some(now));
84            }
85        });
86    }));
87
88    // Play (fallback path only): once the inverted frame has committed,
89    // release the transform; the armed CSS transition glides the item home.
90    // (Effects run after the render commits, giving the browser a painted
91    // "old position" frame.)
92    use_effect(move || {
93        if matches!(phase(), FlipPhase::Invert(_)) {
94            phase.set(FlipPhase::Rest);
95        }
96    });
97
98    let style = match phase() {
99        FlipPhase::Invert(d) => invert_style(d),
100        FlipPhase::Rest => rest_style(duration, &easing),
101    };
102    // The glide is an inline transition; honor prefers-reduced-motion.
103    let reduced_motion_css = use_reduced_motion_css();
104
105    rsx! {
106        {reduced_motion_css}
107        div {
108            style: "{style}",
109            "data-dnd-motion": true,
110            onmounted: move |evt: Event<MountedData>| {
111                let mut mounted = mounted;
112                mounted.set(Some(evt.data()));
113            },
114            ..attributes,
115            {children}
116        }
117    }
118}