use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use js_sys::Reflect;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::{window, Element};
const TX_ID_KEY: &str = "__pp_tx_id";
#[derive(Clone, Copy, PartialEq, Eq)]
enum Phase {
Idle,
Entering,
Leaving,
}
struct State {
enter: Vec<String>,
enter_start: Vec<String>,
enter_end: Vec<String>,
leave: Vec<String>,
leave_start: Vec<String>,
leave_end: Vec<String>,
epoch: u64,
phase: Phase,
pending_timer: Option<i32>,
}
impl State {
fn any(&self) -> bool {
!self.enter.is_empty()
|| !self.enter_start.is_empty()
|| !self.enter_end.is_empty()
|| !self.leave.is_empty()
|| !self.leave_start.is_empty()
|| !self.leave_end.is_empty()
}
fn all_classes(&self) -> impl Iterator<Item = &String> {
self.enter
.iter()
.chain(self.enter_start.iter())
.chain(self.enter_end.iter())
.chain(self.leave.iter())
.chain(self.leave_start.iter())
.chain(self.leave_end.iter())
}
}
thread_local! {
static TX: RefCell<HashMap<u64, Rc<RefCell<State>>>> =
RefCell::new(HashMap::new());
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
static DISABLED: Cell<bool> = const { Cell::new(false) };
}
pub fn set_disabled(v: bool) {
DISABLED.with(|c| c.set(v));
}
pub fn is_disabled() -> bool {
DISABLED.with(|c| c.get())
}
fn get_or_init(el: &Element) -> Option<Rc<RefCell<State>>> {
if let Some(v) = Reflect::get(el.as_ref(), &TX_ID_KEY.into())
.ok()
.and_then(|v| v.as_f64())
{
let id = v as u64;
return TX.with(|m| m.borrow().get(&id).cloned());
}
expand_preset_shorthand(el);
let state = parse_attrs(el);
if !state.any() {
return None;
}
let id = NEXT_ID.with(|c| {
let v = c.get();
c.set(v + 1);
v
});
let rc = Rc::new(RefCell::new(state));
TX.with(|m| m.borrow_mut().insert(id, rc.clone()));
let _ = Reflect::set(
el.as_ref(),
&TX_ID_KEY.into(),
&JsValue::from_f64(id as f64),
);
Some(rc)
}
fn expand_preset_shorthand(el: &Element) {
let sym = el.get_attribute("pp-transition");
let in_attr = el.get_attribute("pp-transition:in");
let out_attr = el.get_attribute("pp-transition:out");
if sym.is_none() && in_attr.is_none() && out_attr.is_none() {
return;
}
let symmetric = sym.as_deref().unwrap_or("");
let in_name = in_attr.as_deref().unwrap_or(symmetric);
let out_name = out_attr.as_deref().unwrap_or(symmetric);
let has = |name: &str| el.has_attribute(name);
if has("pp-transition:enter")
&& has("pp-transition:enter-start")
&& has("pp-transition:enter-end")
&& has("pp-transition:leave")
&& has("pp-transition:leave-start")
&& has("pp-transition:leave-end")
{
return;
}
crate::animate::apply_preset(el, in_name, out_name);
}
fn parse_attrs(el: &Element) -> State {
fn split(s: &str) -> Vec<String> {
s.split_whitespace().map(str::to_string).collect()
}
let get = |name: &str| {
el.get_attribute(name)
.map(|s| split(&s))
.unwrap_or_default()
};
State {
enter: get("pp-transition:enter"),
enter_start: get("pp-transition:enter-start"),
enter_end: get("pp-transition:enter-end"),
leave: get("pp-transition:leave"),
leave_start: get("pp-transition:leave-start"),
leave_end: get("pp-transition:leave-end"),
epoch: 0,
phase: Phase::Idle,
pending_timer: None,
}
}
fn cancel(state: &mut State, el: &Element) {
let cl = el.class_list();
for c in state.all_classes() {
let _ = cl.remove_1(c);
}
if let Some(h) = state.pending_timer.take() {
if let Some(w) = window() {
w.clear_timeout_with_handle(h);
}
}
state.epoch = state.epoch.wrapping_add(1);
state.phase = Phase::Idle;
}
pub fn is_leaving(el: &Element) -> bool {
match get_or_init(el) {
Some(rc) => rc.borrow().phase == Phase::Leaving,
None => false,
}
}
pub fn enter<F: FnOnce() + 'static>(el: &Element, on_done: F) {
if is_disabled() {
on_done();
return;
}
if crate::animate::motion::effective_for(el)
== crate::animate::motion::MotionPreference::Reduced
{
on_done();
return;
}
let rc = match get_or_init(el) {
Some(r) => r,
None => {
on_done();
return;
}
};
{
let mut s = rc.borrow_mut();
cancel(&mut s, el);
s.phase = Phase::Entering;
}
let epoch = rc.borrow().epoch;
let cl = el.class_list();
{
let s = rc.borrow();
for c in &s.enter_start {
let _ = cl.add_1(c);
}
}
let _ = el.client_width();
let el_cap = el.clone();
let rc_cap = rc.clone();
let on_done_cell = std::rc::Rc::new(std::cell::RefCell::new(Some(on_done)));
crate::tick::next_frame(move || {
if rc_cap.borrow().epoch != epoch {
return;
}
let cl = el_cap.class_list();
{
let s = rc_cap.borrow();
for c in &s.enter_start {
let _ = cl.remove_1(c);
}
for c in &s.enter {
let _ = cl.add_1(c);
}
for c in &s.enter_end {
let _ = cl.add_1(c);
}
}
let el_for_end = el_cap.clone();
let rc_for_end = rc_cap.clone();
let on_done_cell = on_done_cell.clone();
schedule_end(&el_cap, rc_cap.clone(), epoch, move || {
let _ = el_for_end;
let mut s = rc_for_end.borrow_mut();
s.phase = Phase::Idle;
drop(s);
if let Some(cb) = on_done_cell.borrow_mut().take() {
cb();
}
});
});
}
pub fn leave<F: FnOnce() + 'static>(el: &Element, on_done: F) {
if is_disabled() {
on_done();
return;
}
if crate::animate::motion::effective_for(el)
== crate::animate::motion::MotionPreference::Reduced
{
on_done();
return;
}
let rc = match get_or_init(el) {
Some(r) => r,
None => {
on_done();
return;
}
};
{
let mut s = rc.borrow_mut();
cancel(&mut s, el);
s.phase = Phase::Leaving;
}
let epoch = rc.borrow().epoch;
let cl = el.class_list();
{
let s = rc.borrow();
for c in &s.leave_start {
let _ = cl.add_1(c);
}
}
let _ = el.client_width();
let el_cap = el.clone();
let rc_cap = rc.clone();
let on_done_cell = std::rc::Rc::new(std::cell::RefCell::new(Some(on_done)));
crate::tick::next_frame(move || {
if rc_cap.borrow().epoch != epoch {
return;
}
let cl = el_cap.class_list();
{
let s = rc_cap.borrow();
for c in &s.leave_start {
let _ = cl.remove_1(c);
}
for c in &s.leave {
let _ = cl.add_1(c);
}
for c in &s.leave_end {
let _ = cl.add_1(c);
}
}
let el_for_end = el_cap.clone();
let rc_for_end = rc_cap.clone();
let on_done_cell = on_done_cell.clone();
schedule_end(&el_cap, rc_cap.clone(), epoch, move || {
let _ = el_for_end;
let mut s = rc_for_end.borrow_mut();
s.phase = Phase::Idle;
drop(s);
if let Some(cb) = on_done_cell.borrow_mut().take() {
cb();
}
});
});
}
fn schedule_end<F: FnOnce() + 'static>(
el: &Element,
rc: Rc<RefCell<State>>,
epoch: u64,
on_done: F,
) {
let duration = computed_duration_ms(el);
if duration <= 0.0 {
on_done();
return;
}
let rc_cap = rc.clone();
let closure = Closure::once(Box::new(move || {
let current_epoch = rc_cap.borrow().epoch;
if current_epoch != epoch {
return;
}
rc_cap.borrow_mut().pending_timer = None;
on_done();
}) as Box<dyn FnOnce()>);
if let Some(w) = window() {
if let Ok(handle) = w.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
(duration + 20.0) as i32,
) {
rc.borrow_mut().pending_timer = Some(handle);
}
}
closure.forget();
}
fn computed_duration_ms(el: &Element) -> f64 {
let Some(w) = window() else { return 0.0 };
let Ok(Some(cs)) = w.get_computed_style(el) else {
return 0.0;
};
let dur = cs
.get_property_value("transition-duration")
.unwrap_or_default();
let delay = cs
.get_property_value("transition-delay")
.unwrap_or_default();
parse_duration(&dur) + parse_duration(&delay)
}
fn parse_duration(s: &str) -> f64 {
s.split(',')
.map(|seg| {
let t = seg.trim();
if let Some(n) = t.strip_suffix("ms") {
n.trim().parse::<f64>().unwrap_or(0.0)
} else if let Some(n) = t.strip_suffix('s') {
n.trim().parse::<f64>().unwrap_or(0.0) * 1000.0
} else {
0.0
}
})
.fold(0.0_f64, f64::max)
}
const ATTR_SELECTOR: &str = "[pp-transition], [pp-transition\\:in], [pp-transition\\:out], \
[pp-transition\\:enter], [pp-transition\\:enter-start], [pp-transition\\:enter-end], \
[pp-transition\\:leave], [pp-transition\\:leave-start], [pp-transition\\:leave-end]";
fn has_any_transition_attr(el: &Element) -> bool {
el.has_attribute("pp-transition")
|| el.has_attribute("pp-transition:in")
|| el.has_attribute("pp-transition:out")
|| el.has_attribute("pp-transition:enter")
|| el.has_attribute("pp-transition:enter-start")
|| el.has_attribute("pp-transition:enter-end")
|| el.has_attribute("pp-transition:leave")
|| el.has_attribute("pp-transition:leave-start")
|| el.has_attribute("pp-transition:leave-end")
}
pub fn has_transition_in_subtree(root: &Element) -> bool {
if has_any_transition_attr(root) {
return true;
}
root.query_selector(ATTR_SELECTOR).ok().flatten().is_some()
}
fn collect_animated(root: &Element) -> Vec<Element> {
use wasm_bindgen::JsCast;
let mut out = Vec::new();
if has_any_transition_attr(root) {
out.push(root.clone());
}
if let Ok(list) = root.query_selector_all(ATTR_SELECTOR) {
for i in 0..list.length() {
if let Some(node) = list.item(i) {
if let Ok(el) = node.dyn_into::<Element>() {
out.push(el);
}
}
}
}
out
}
pub fn enter_subtree<F: FnOnce() + 'static>(root: &Element, on_done: F) {
enter_subtree_dyn(root, Box::new(on_done));
}
fn enter_subtree_dyn(root: &Element, on_done: Box<dyn FnOnce() + 'static>) {
let elems = collect_animated(root);
if elems.is_empty() {
on_done();
return;
}
let remaining = Rc::new(Cell::new(elems.len()));
let on_done_cell = Rc::new(RefCell::new(Some(on_done)));
for el in elems {
let remaining = remaining.clone();
let on_done_cell = on_done_cell.clone();
enter(&el, move || {
let n = remaining.get().saturating_sub(1);
remaining.set(n);
if n == 0 {
if let Some(cb) = on_done_cell.borrow_mut().take() {
cb();
}
}
});
}
}
pub fn enter_subtrees_sequenced(clones: &[Element], stagger_ms: u32) {
struct Pending {
el: Element,
rc: Rc<RefCell<State>>,
epoch: u64,
}
let mut batch: Vec<Vec<Pending>> = Vec::with_capacity(clones.len());
for root in clones {
let mut per_clone: Vec<Pending> = Vec::new();
for el in collect_animated(root) {
if is_disabled() {
continue;
}
if crate::animate::motion::effective_for(&el)
== crate::animate::motion::MotionPreference::Reduced
{
continue;
}
let Some(rc) = get_or_init(&el) else { continue };
{
let mut s = rc.borrow_mut();
cancel(&mut s, &el);
s.phase = Phase::Entering;
}
let epoch = rc.borrow().epoch;
let cl = el.class_list();
{
let s = rc.borrow();
for c in &s.enter_start {
let _ = cl.add_1(c);
}
}
per_clone.push(Pending { el, rc, epoch });
}
batch.push(per_clone);
}
if let Some(first_clone) = batch.iter().flatten().next() {
let _ = first_clone.el.client_width();
}
crate::tick::next_frame(move || {
for (i, per_clone) in batch.into_iter().enumerate() {
let delay = (i as u32).saturating_mul(stagger_ms);
let finish = move || {
for Pending { el, rc, epoch } in per_clone {
if rc.borrow().epoch != epoch {
continue;
}
let cl = el.class_list();
{
let s = rc.borrow();
for c in &s.enter_start {
let _ = cl.remove_1(c);
}
for c in &s.enter {
let _ = cl.add_1(c);
}
for c in &s.enter_end {
let _ = cl.add_1(c);
}
}
let rc_for_end = rc.clone();
schedule_end(&el, rc.clone(), epoch, move || {
let mut s = rc_for_end.borrow_mut();
s.phase = Phase::Idle;
});
}
};
if delay == 0 {
finish();
} else if let Some(window) = window() {
let cb = Closure::once_into_js(finish);
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.unchecked_ref(),
delay as i32,
);
}
}
});
}
pub fn enter_subtree_staggered<F: FnOnce() + 'static>(root: &Element, stagger_ms: u32, on_done: F) {
let elems = collect_animated(root);
if elems.is_empty() {
on_done();
return;
}
let remaining = Rc::new(Cell::new(elems.len()));
let on_done_cell = Rc::new(RefCell::new(Some(on_done)));
for (i, el) in elems.into_iter().enumerate() {
let remaining = remaining.clone();
let on_done_cell = on_done_cell.clone();
let delay = (i as u32).saturating_mul(stagger_ms);
let fire = move || {
enter(&el, move || {
let n = remaining.get().saturating_sub(1);
remaining.set(n);
if n == 0 {
if let Some(cb) = on_done_cell.borrow_mut().take() {
cb();
}
}
});
};
if delay == 0 {
fire();
} else {
let cb = wasm_bindgen::closure::Closure::once_into_js(fire);
if let Some(w) = web_sys::window() {
let _ = w.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.unchecked_ref(),
delay as i32,
);
}
}
}
}
pub fn leave_subtree<F: FnOnce() + 'static>(root: &Element, on_done: F) {
leave_subtree_dyn(root, Box::new(on_done));
}
fn leave_subtree_dyn(root: &Element, on_done: Box<dyn FnOnce() + 'static>) {
let elems = collect_animated(root);
if elems.is_empty() {
on_done();
return;
}
let remaining = Rc::new(Cell::new(elems.len()));
let on_done_cell = Rc::new(RefCell::new(Some(on_done)));
const SAFETY_MS: i32 = 1000;
let remaining_for_safety = remaining.clone();
let on_done_for_safety = on_done_cell.clone();
let safety = Closure::once_into_js(move || {
if remaining_for_safety.get() > 0 {
if let Some(cb) = on_done_for_safety.borrow_mut().take() {
cb();
}
}
});
if let Some(w) = window() {
let _ = w.set_timeout_with_callback_and_timeout_and_arguments_0(
safety.unchecked_ref(),
SAFETY_MS,
);
}
for el in elems {
let remaining = remaining.clone();
let on_done_cell = on_done_cell.clone();
leave(&el, move || {
let n = remaining.get().saturating_sub(1);
remaining.set(n);
if n == 0 {
if let Some(cb) = on_done_cell.borrow_mut().take() {
cb();
}
}
});
}
}
pub fn is_subtree_leaving(root: &Element) -> bool {
collect_animated(root).iter().any(is_leaving)
}
pub fn release(el: &Element) {
let Some(id) = Reflect::get(el.as_ref(), &TX_ID_KEY.into())
.ok()
.and_then(|v| v.as_f64())
else {
return;
};
let id = id as u64;
TX.with(|m| {
if let Some(rc) = m.borrow_mut().remove(&id) {
if let Some(h) = rc.borrow_mut().pending_timer.take() {
if let Some(w) = window() {
w.clear_timeout_with_handle(h);
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::parse_duration;
#[test]
fn parses_ms() {
assert_eq!(parse_duration("300ms"), 300.0);
}
#[test]
fn parses_fractional_seconds() {
assert_eq!(parse_duration("0.3s"), 300.0);
}
#[test]
fn empty_is_zero() {
assert_eq!(parse_duration(""), 0.0);
}
#[test]
fn takes_first_of_list() {
assert_eq!(parse_duration("300ms, 200ms"), 300.0);
}
}