dioxus-animate 0.3.3

Time-based CSS class manipulation for smooth animations in Dioxus apps
Documentation
use dioxus::{prelude::*, web::WebEventExt};
use std::rc::Rc;

#[derive(Clone, PartialEq)]
pub enum Operation {
    Add(String),
    Remove(String),
    Group(Vec<Operation>),
}

fn operate(patient: &web_sys::Element, operation: &Operation) {
    match operation {
        Operation::Add(class) => {
            let classes = class.split_whitespace().collect::<Vec<&str>>();

            for class in classes {
                let _ = patient.class_list().add_1(class);
            }
        }
        Operation::Remove(class) => {
            let classes = class.split_whitespace().collect::<Vec<&str>>();

            for class in classes {
                let _ = patient.class_list().remove_1(class);
            }
        }
        Operation::Group(operations) => {
            for operation in operations {
                operate(patient, operation);
            }
        }
    }
}

#[macro_export]
macro_rules! use_animate {
    ( $($input:tt)* ) => {{
        let operations = use_signal(|| {
            let mut ops = Vec::new();
            $crate::__parse_animations!(ops, $($input)*);
            ops
        });

        $crate::_use_animate(operations.into())
    }};
}

#[macro_export]
macro_rules! __parse_animations {
    ($ops:ident, $t:expr => add($s:literal), $($rest:tt)*) => {
        $ops.push(($t, $crate::Operation::Add($s.to_string())));
        $crate::__parse_animations!($ops, $($rest)*);
    };

    ($ops:ident, $t:expr => remove($s:literal), $($rest:tt)*) => {
        $ops.push(($t, $crate::Operation::Remove($s.to_string())));
        $crate::__parse_animations!($ops, $($rest)*);
    };

    ($ops:ident, $t:expr => ($($inner:tt)*), $($rest:tt)*) => {
        $ops.push(($t, {
            let mut group_ops = Vec::new();
            $crate::__expand_inner!(group_ops, $($inner)*);
            $crate::Operation::Group(group_ops)
        }));
        $crate::__parse_animations!($ops, $($rest)*);
    };

    ($ops:ident, $t:expr => add($s:literal)) => {
        $ops.push(($t, $crate::Operation::Add($s.to_string())));
    };

    ($ops:ident, $t:expr => remove($s:literal)) => {
        $ops.push(($t, $crate::Operation::Remove($s.to_string())));
    };

    ($ops:ident, $t:expr => ($($inner:tt)*)) => {
        $ops.push(($t, {
            let mut group_ops = Vec::new();
            $crate::__expand_inner!(group_ops, $($inner)*);
            $crate::Operation::Group(group_ops)
        }));
    };

    ($ops:ident,) => {};
}

#[macro_export]
macro_rules! __expand_inner {
    ( $ops:ident, add($s:literal); $($rest:tt)* ) => {
        $ops.push($crate::Operation::Add($s.to_string()));
        $crate::__expand_inner!($ops, $($rest)*);
    };
    ( $ops:ident, remove($s:literal); $($rest:tt)* ) => {
        $ops.push($crate::Operation::Remove($s.to_string()));
        $crate::__expand_inner!($ops, $($rest)*);
    };
    ( $ops:ident, add($s:literal) ) => {
        $ops.push($crate::Operation::Add($s.to_string()));
    };
    ( $ops:ident, remove($s:literal) ) => {
        $ops.push($crate::Operation::Remove($s.to_string()));
    };
    ( $ops:ident, ) => {};
}

pub struct UseAnimate {
    ops: Vec<(u64, Operation)>,
}

impl UseAnimate {
    pub fn start(&self, patient: ReadSignal<Option<Rc<MountedData>>>) {
        let ops = self.ops.clone();

        spawn(async move {
            let Some(element) = patient() else {
                return;
            };

            let mut time_elapsed = 0;

            for (duration, operation) in ops.into_iter() {
                let interval = duration - time_elapsed;

                async_std::task::sleep(instant::Duration::from_millis(interval)).await;
                time_elapsed += interval;

                let web_element = element.as_web_event();
                operate(&web_element, &operation);
            }
        });
    }

    pub fn start_for_id(&self, id: &str) {
        let ops = self.ops.clone();
        use wasm_bindgen::JsCast;

        let element = gloo::utils::document()
            .get_element_by_id(id)
            .and_then(|element| element.dyn_into::<web_sys::Element>().ok())
            .expect("Element not found");

        spawn(async move {
            let mut time_elapsed = 0;

            for (duration, operation) in ops.into_iter() {
                let interval = duration - time_elapsed;

                async_std::task::sleep(instant::Duration::from_millis(interval)).await;
                time_elapsed += interval;

                operate(&element, &operation);
            }
        });
    }
}

pub fn _use_animate(ops: ReadSignal<Vec<(u64, Operation)>>) -> UseAnimate {
    UseAnimate { ops: ops() }
}

pub mod prelude {
    pub use crate::use_animate;
}