freya_components/
global_animated_position.rs1use 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#[component]
111pub fn GlobalAnimatedPosition<T: Clone + PartialEq + Hash + Eq + 'static>(
112 children: Element,
113 width: String,
114 height: String,
115 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 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 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}