use dioxus_core::ScopeState;
use dioxus_hooks::{use_memo, use_state, UseFutureDep, UseState};
use freya_engine::prelude::Color;
use freya_node_state::Parse;
use tokio::time::Instant;
use uuid::Uuid;
use crate::{use_platform, Animation, TransitionAnimation, UsePlatform};
#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Transition {
Size(f64, f64),
Color(Color, Color),
}
impl Transition {
pub fn new_size(start: f64, end: f64) -> Self {
Self::Size(start, end)
}
pub fn new_color(start: &str, end: &str) -> Self {
let start = Color::parse(start).unwrap();
let end = Color::parse(end).unwrap();
Self::Color(start, end)
}
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub enum TransitionState {
Size(f64),
Color(Color),
}
impl From<&Transition> for TransitionState {
fn from(value: &Transition) -> Self {
match *value {
Transition::Size(start, _) => Self::Size(start),
Transition::Color(start, _) => Self::Color(start),
}
}
}
impl TransitionState {
pub fn set_value(&mut self, animate: &Transition, value: f64) {
match (self, animate) {
(Self::Size(current), Transition::Size(start, end)) => {
let road = *end - *start;
let walked = (road / 100.0) * value;
*current = walked;
}
(Self::Color(current), Transition::Color(start, end)) => {
let apply_index = |v: u8, d: u8, value: f64| -> u8 {
let road = if d > v { d - v } else { v - d };
let walked = (road as f64 / 100.0) * value;
if d > v {
v + walked.round() as u8
} else {
v - walked.round() as u8
}
};
let r = apply_index(start.r(), end.r(), value);
let g = apply_index(start.g(), end.g(), value);
let b = apply_index(start.b(), end.b(), value);
*current = Color::from_rgb(r, g, b)
}
_ => {}
}
}
pub fn clear(&mut self, animate: &Transition) {
match (self, animate) {
(Self::Size(current), Transition::Size(start, _)) => {
*current = *start;
}
(Self::Color(current), Transition::Color(start, _)) => {
*current = *start;
}
_ => {}
}
}
pub fn as_size(&self) -> f64 {
self.to_size().unwrap()
}
pub fn as_color(&self) -> String {
self.to_color().unwrap()
}
pub fn to_size(&self) -> Option<f64> {
match self {
Self::Size(current) => Some(*current),
_ => None,
}
}
pub fn to_color(&self) -> Option<String> {
match self {
Self::Color(current) => Some(format!(
"rgb({}, {}, {})",
current.r(),
current.g(),
current.b()
)),
_ => None,
}
}
pub fn to_raw_color(&self) -> Option<Color> {
match self {
Self::Color(current) => Some(*current),
_ => None,
}
}
}
#[derive(Clone)]
pub struct TransitionsManager<'a> {
transitions: &'a Vec<Transition>,
transitions_storage: &'a UseState<Vec<TransitionState>>,
transition_animation: TransitionAnimation,
current_animation_id: &'a UseState<Option<Uuid>>,
cx: &'a ScopeState,
platform: UsePlatform,
}
impl<'a> TransitionsManager<'a> {
pub fn reverse(&self) {
self.clear();
let animation = self.transition_animation.to_animation(100.0..=0.0);
self.run_with_animation(animation);
}
pub fn start(&self) {
self.clear();
let animation = self.transition_animation.to_animation(0.0..=100.0);
self.run_with_animation(animation);
}
fn run_with_animation(&self, mut animation: Animation) {
let animation_id = Uuid::new_v4();
let platform = self.platform.clone();
let mut ticker = platform.new_ticker();
let transitions = self.transitions.clone();
let transitions_storage = self.transitions_storage.clone();
let current_animation_id = self.current_animation_id.clone();
current_animation_id.set(Some(animation_id));
self.cx.spawn(async move {
platform.request_animation_frame();
let mut index = 0;
let mut prev_frame = Instant::now();
loop {
ticker.tick().await;
platform.request_animation_frame();
if *current_animation_id.current() == Some(animation_id) {
if animation.is_finished() {
current_animation_id.set(None);
break;
}
index += prev_frame.elapsed().as_millis() as i32;
let value = animation.move_value(index);
transitions_storage.with_mut(|storage| {
for (i, storage) in storage.iter_mut().enumerate() {
if let Some(conf) = transitions.get(i) {
storage.set_value(conf, value);
}
}
});
prev_frame = Instant::now();
} else {
break;
}
}
});
}
pub fn clear(&self) {
self.current_animation_id.set(None);
self.transitions_storage.with_mut(|storage| {
for (i, storage) in storage.iter_mut().enumerate() {
if let Some(conf) = self.transitions.get(i) {
storage.clear(conf);
}
}
})
}
pub fn is_animating(&self) -> bool {
self.current_animation_id.is_some()
}
pub fn is_at_start(&self) -> bool {
if let Some(storage) = self.get(0) {
let anim = self.transitions[0];
match anim {
Transition::Size(start, _) => start == storage.to_size().unwrap_or(start),
Transition::Color(start, _) => start == storage.to_raw_color().unwrap_or(start),
}
} else {
true
}
}
pub fn get(&self, index: usize) -> Option<TransitionState> {
self.transitions_storage.current().get(index).copied()
}
}
pub fn use_animation_transition<D>(
cx: &ScopeState,
transition: TransitionAnimation,
dependencies: D,
mut init: impl Fn(D::Out) -> Vec<Transition>,
) -> TransitionsManager
where
D: UseFutureDep,
{
let current_animation_id = use_state(cx, || None);
let transitions = use_memo(cx, dependencies.clone(), &mut init);
let transitions_storage = use_state(cx, || animations_map(transitions));
let platform = use_platform(cx);
let _ = use_memo(cx, dependencies, {
let storage_setter = transitions_storage.setter();
move |v| {
storage_setter(animations_map(&init(v)));
}
});
TransitionsManager {
current_animation_id,
transitions,
transitions_storage,
cx,
transition_animation: transition,
platform,
}
}
fn animations_map(animations: &[Transition]) -> Vec<TransitionState> {
animations
.iter()
.map(TransitionState::from)
.collect::<Vec<TransitionState>>()
}
#[cfg(test)]
mod test {
use std::time::Duration;
use crate::{use_animation_transition, Transition, TransitionAnimation};
use freya::prelude::*;
use freya_testing::launch_test;
use tokio::time::sleep;
#[tokio::test]
pub async fn track_progress() {
fn use_animation_transition_app(cx: Scope) -> Element {
let animation =
use_animation_transition(cx, TransitionAnimation::new_linear(50), (), |_| {
vec![Transition::new_size(0.0, 100.0)]
});
let progress = animation.get(0).unwrap().as_size();
let _ = use_memo(cx, (), move |_| {
animation.start();
});
render!(rect {
width: "{progress}",
})
}
let mut utils = launch_test(use_animation_transition_app);
utils.config().enable_ticker(false);
utils.wait_for_update().await;
assert_eq!(utils.root().get(0).layout().unwrap().width(), 0.0);
sleep(Duration::from_millis(15)).await;
utils.wait_for_update().await;
let width = utils.root().get(0).layout().unwrap().width();
assert!(width > 0.0);
utils.config().enable_ticker(true);
utils.wait_for_update().await;
let width = utils.root().get(0).layout().unwrap().width();
assert_eq!(width, 100.0);
}
}