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