use std::{
collections::HashMap,
hash::Hash,
sync::Arc,
time::Duration,
};
use dioxus::prelude::*;
use dioxus_core::AttributeValue;
use freya_common::NodeReferenceLayout;
use freya_elements::elements as dioxus_elements;
use freya_hooks::{
use_animation_with_dependencies,
AnimDirection,
AnimNum,
Ease,
Function,
};
use freya_node_state::{
CustomAttributeValues,
NodeReference,
};
use tokio::sync::watch::channel;
use torin::prelude::Area;
#[derive(Clone)]
pub struct GlobalAnimatedPositions<T: Clone + PartialEq + 'static> {
pub ids: Signal<HashMap<T, Area>>,
}
type InitCurrPrevSignals = (
AttributeValue,
Signal<Option<Area>>,
ReadOnlySignal<Option<Area>>,
ReadOnlySignal<Option<Area>>,
);
fn use_node_init_curr_prev<T: Clone + PartialEq + Hash + Eq + 'static>(
id: T,
) -> InitCurrPrevSignals {
let (tx, init_signal, curr_signal, prev_signal) = use_hook(|| {
let (tx, mut rx) = channel::<NodeReferenceLayout>(NodeReferenceLayout::default());
let mut ctx = consume_context::<GlobalAnimatedPositions<T>>();
let init_signal = Signal::new(ctx.ids.write().remove(&id));
let mut curr_signal = Signal::new(None);
let mut prev_signal = Signal::new(None);
spawn(async move {
while rx.changed().await.is_ok() {
if *curr_signal.peek() != Some(rx.borrow().clone().area) {
prev_signal.set(curr_signal());
curr_signal.set(Some(rx.borrow().clone().area));
ctx.ids.write().insert(id.clone(), curr_signal().unwrap());
}
}
});
(Arc::new(tx), init_signal, curr_signal, prev_signal)
});
(
AttributeValue::any_value(CustomAttributeValues::Reference(NodeReference(tx))),
init_signal,
curr_signal.into(),
prev_signal.into(),
)
}
#[derive(Props, PartialEq, Clone)]
pub struct GlobalAnimatedPositionProvider {
children: Element,
}
#[allow(non_snake_case)]
pub fn GlobalAnimatedPositionProvider<T: Clone + PartialEq + Hash + Eq + 'static>(
GlobalAnimatedPositionProvider { children }: GlobalAnimatedPositionProvider,
) -> Element {
use_context_provider(|| GlobalAnimatedPositions::<T> {
ids: Signal::default(),
});
children
}
#[component]
pub fn GlobalAnimatedPosition<T: Clone + PartialEq + Hash + Eq + 'static>(
children: Element,
width: String,
height: String,
id: T,
#[props(default = Function::default())] function: Function,
#[props(default = Duration::from_millis(250))] duration: Duration,
#[props(default = Ease::default())] ease: Ease,
) -> Element {
let mut render_element = use_signal(|| false);
let (reference, mut init_size, size, old_size) = use_node_init_curr_prev(id);
let animations = use_animation_with_dependencies(
&(function, duration, ease),
move |ctx, (function, duration, ease)| {
let old_size = init_size.peek().unwrap_or(old_size().unwrap_or_default());
let size = size().unwrap_or_default();
(
ctx.with(
AnimNum::new(size.origin.x, old_size.origin.x)
.duration(duration)
.ease(ease)
.function(function),
),
ctx.with(
AnimNum::new(size.origin.y, old_size.origin.y)
.duration(duration)
.ease(ease)
.function(function),
),
)
},
);
use_effect(move || {
if animations.is_running() {
render_element.set(true);
}
});
use_effect(move || {
let has_size = size.read().is_some();
let has_init_size = init_size.read().is_some();
let has_old_size = old_size.read().is_some();
if has_size && (has_old_size || has_init_size) {
animations.run(AnimDirection::Reverse);
} else if has_size {
render_element.set(true);
}
});
use_effect(move || {
if animations.has_run_yet() {
init_size.set(None);
}
});
let (offset_x, offset_y) = animations.get();
let offset_x = offset_x.read().as_f32();
let offset_y = offset_y.read().as_f32();
rsx!(
rect {
reference,
width: "{width}",
height: "{height}",
rect {
width: "0",
height: "0",
offset_x: "{offset_x}",
offset_y: "{offset_y}",
position: "global",
if render_element() {
rect {
width: "{size.read().as_ref().unwrap().width()}",
height: "{size.read().as_ref().unwrap().height()}",
{children}
}
}
}
}
)
}
#[cfg(test)]
mod test {
use std::time::Duration;
use freya::prelude::*;
use freya_testing::prelude::*;
#[tokio::test]
pub async fn animated_position() {
fn animated_position_app() -> Element {
let mut padding = use_signal(|| (100., 100.));
rsx!(
rect {
padding: "{padding().0} {padding().1}",
onclick: move |_| {
padding.write().0 += 10.;
padding.write().1 += 10.;
},
GlobalAnimatedPosition {
width: "50",
height: "50",
function: Function::Linear,
key: 0
}
}
)
}
let mut utils = launch_test(animated_position_app);
utils.config().event_loop_ticker = false;
let root = utils.root();
utils.wait_for_update().await;
utils.wait_for_update().await;
let get_positions = || {
root.get(0)
.get(0)
.get(0)
.get(0)
.layout()
.unwrap()
.area
.origin
};
assert_eq!(get_positions().x, 100.);
assert_eq!(get_positions().y, 100.);
utils.click_cursor((5.0, 5.0)).await;
utils.wait_for_update().await;
utils.wait_for_update().await;
tokio::time::sleep(Duration::from_millis(125)).await;
utils.wait_for_update().await;
utils.wait_for_update().await;
assert!(get_positions().x < 106.);
assert!(get_positions().x > 105.);
assert!(get_positions().y < 106.);
assert!(get_positions().y > 105.);
utils.config().event_loop_ticker = true;
utils.wait_for_update().await;
tokio::time::sleep(Duration::from_millis(125)).await;
utils.wait_for_update().await;
assert_eq!(get_positions().x, 110.);
}
}