Skip to main content

freya_components/
global_animated_position.rs

1use std::{
2    collections::HashMap,
3    hash::Hash,
4    sync::Arc,
5    time::Duration,
6};
7
8use dioxus::prelude::*;
9use dioxus_core::AttributeValue;
10use freya_core::custom_attributes::{
11    CustomAttributeValues,
12    NodeReference,
13    NodeReferenceLayout,
14};
15use freya_elements as dioxus_elements;
16use freya_hooks::{
17    use_animation_with_dependencies,
18    AnimDirection,
19    AnimNum,
20    Ease,
21    Function,
22};
23use tokio::sync::watch::channel;
24use torin::prelude::Area;
25
26#[derive(Clone)]
27pub struct GlobalAnimatedPositions<T: Clone + PartialEq + 'static> {
28    pub ids: Signal<HashMap<T, Area>>,
29}
30
31type InitCurrPrevSignals = (
32    AttributeValue,
33    Signal<Option<Area>>,
34    ReadOnlySignal<Option<Area>>,
35    ReadOnlySignal<Option<Area>>,
36);
37
38fn use_node_init_curr_prev<T: Clone + PartialEq + Hash + Eq + 'static>(
39    id: T,
40) -> InitCurrPrevSignals {
41    let (tx, init_signal, curr_signal, prev_signal) = use_hook(|| {
42        let (tx, mut rx) = channel::<NodeReferenceLayout>(NodeReferenceLayout::default());
43        let mut ctx = consume_context::<GlobalAnimatedPositions<T>>();
44        let init_signal = Signal::new(ctx.ids.write().remove(&id));
45        let mut curr_signal = Signal::new(None);
46        let mut prev_signal = Signal::new(None);
47
48        spawn(async move {
49            while rx.changed().await.is_ok() {
50                if *curr_signal.peek() != Some(rx.borrow().clone().area) {
51                    prev_signal.set(curr_signal());
52                    curr_signal.set(Some(rx.borrow().clone().area));
53                    ctx.ids.write().insert(id.clone(), curr_signal().unwrap());
54                }
55            }
56        });
57
58        (Arc::new(tx), init_signal, curr_signal, prev_signal)
59    });
60
61    (
62        AttributeValue::any_value(CustomAttributeValues::Reference(NodeReference(tx))),
63        init_signal,
64        curr_signal.into(),
65        prev_signal.into(),
66    )
67}
68
69#[derive(Props, PartialEq, Clone)]
70pub struct GlobalAnimatedPositionProvider {
71    children: Element,
72}
73
74#[allow(non_snake_case)]
75pub fn GlobalAnimatedPositionProvider<T: Clone + PartialEq + Hash + Eq + 'static>(
76    GlobalAnimatedPositionProvider { children }: GlobalAnimatedPositionProvider,
77) -> Element {
78    use_context_provider(|| GlobalAnimatedPositions::<T> {
79        ids: Signal::default(),
80    });
81
82    children
83}
84
85/// Animate an element position across time and space.
86///
87/// For that, the element must have an unique ID.
88///
89/// It must also be descendant of a [GlobalAnimatedPositionProvider].
90///
91/// # Example
92///
93/// ```no_run
94/// # use freya::prelude::*;
95/// fn app() -> Element {
96///     rsx!(
97///         GlobalAnimatedPositionProvider::<i32> {
98///             GlobalAnimatedPosition {
99///                 id: 0,
100///                 width: "100",
101///                 height: "25",
102///                 label {
103///                     "Click this"
104///                 }
105///             }
106///         }
107///     )
108/// }
109/// ```
110#[component]
111pub fn GlobalAnimatedPosition<T: Clone + PartialEq + Hash + Eq + 'static>(
112    children: Element,
113    width: String,
114    height: String,
115    /// Unique ID to identify this element.
116    id: T,
117    #[props(default = Function::default())] function: Function,
118    #[props(default = Duration::from_millis(250))] duration: Duration,
119    #[props(default = Ease::default())] ease: Ease,
120) -> Element {
121    let mut render_element = use_signal(|| false);
122    let (reference, mut init_size, size, old_size) = use_node_init_curr_prev(id);
123
124    let animations = use_animation_with_dependencies(
125        &(function, duration, ease),
126        move |_conf, (function, duration, ease)| {
127            let old_size = init_size.peek().unwrap_or(old_size().unwrap_or_default());
128            let size = size().unwrap_or_default();
129            (
130                AnimNum::new(size.origin.x, old_size.origin.x)
131                    .duration(duration)
132                    .ease(ease)
133                    .function(function),
134                AnimNum::new(size.origin.y, old_size.origin.y)
135                    .duration(duration)
136                    .ease(ease)
137                    .function(function),
138            )
139        },
140    );
141
142    use_effect(move || {
143        if animations.is_running() {
144            render_element.set(true);
145        }
146    });
147
148    use_effect(move || {
149        let has_size = size.read().is_some();
150        let has_init_size = init_size.read().is_some();
151        let has_old_size = old_size.read().is_some();
152        if has_size && (has_old_size || has_init_size) {
153            animations.run(AnimDirection::Reverse);
154        } else if has_size {
155            render_element.set(true);
156        }
157    });
158
159    use_effect(move || {
160        if animations.has_run_yet() {
161            // Remove the init size when the first animation starts
162            // This way the next time it is animated it will use the prev size
163            init_size.set(None);
164        }
165    });
166
167    let animations = animations.get();
168    let (offset_x, offset_y) = &*animations.read();
169    let offset_x = offset_x.read();
170    let offset_y = offset_y.read();
171
172    rsx!(
173        rect {
174            reference,
175            width: "{width}",
176            height: "{height}",
177            rect {
178                width: "0",
179                height: "0",
180                offset_x: "{offset_x}",
181                offset_y: "{offset_y}",
182                position: "global",
183                if render_element() {
184                    rect {
185                        width: "{size.read().as_ref().unwrap().width()}",
186                        height: "{size.read().as_ref().unwrap().height()}",
187                        {children}
188                    }
189                }
190            }
191        }
192    )
193}
194
195#[cfg(test)]
196mod test {
197    use std::time::Duration;
198
199    use freya::prelude::*;
200    use freya_testing::prelude::*;
201
202    #[tokio::test]
203    pub async fn global_animated_position() {
204        fn global_animated_position_app() -> Element {
205            let mut padding = use_signal(|| (100., 100.));
206
207            rsx!(
208                GlobalAnimatedPositionProvider::<i32> {
209                    rect {
210                        padding: "{padding().0} {padding().1}",
211                        onclick: move |_| {
212                            padding.write().0 += 10.;
213                            padding.write().1 += 10.;
214                        },
215                        GlobalAnimatedPosition {
216                            width: "50",
217                            height: "50",
218                            function: Function::Linear,
219                            id: 0
220                        }
221                    }
222                }
223
224            )
225        }
226
227        let mut utils = launch_test(global_animated_position_app);
228
229        // Disable event loop ticker
230        utils.config().event_loop_ticker = false;
231
232        let root = utils.root();
233        utils.wait_for_update().await;
234        utils.wait_for_update().await;
235
236        let get_positions = || {
237            root.get(0)
238                .get(0)
239                .get(0)
240                .get(0)
241                .layout()
242                .unwrap()
243                .area
244                .origin
245        };
246
247        assert_eq!(get_positions().x, 100.);
248        assert_eq!(get_positions().y, 100.);
249
250        utils.click_cursor((5.0, 5.0)).await;
251        utils.wait_for_update().await;
252        utils.wait_for_update().await;
253        tokio::time::sleep(Duration::from_millis(125)).await;
254        utils.wait_for_update().await;
255        utils.wait_for_update().await;
256
257        assert!(get_positions().x < 106.);
258        assert!(get_positions().x > 105.);
259
260        assert!(get_positions().y < 106.);
261        assert!(get_positions().y > 105.);
262
263        utils.config().event_loop_ticker = true;
264
265        utils.wait_for_update().await;
266        tokio::time::sleep(Duration::from_millis(125)).await;
267        utils.wait_for_update().await;
268
269        assert_eq!(get_positions().x, 110.);
270    }
271}