day_core/anim.rs
1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Ambient animation intent (§8.4). [`with_animation`] sets a thread-local `AnimSpec` for the
5//! duration of a state mutation; the tree's `patch` / layout's `set_frame` read it (via
6//! `Tree::resolve_anim`) and hand it to the backend as *backend-executed* animation intent: Day
7//! passes intent, the toolkit animates. A node-scoped `.animation(anim)` stores its own `AnimSpec`
8//! on the node (`NodeData::implicit_anim`); the ambient one wins when both are present.
9
10use std::cell::Cell;
11
12use day_spec::AnimSpec;
13
14day_reactive::tls_slots! {
15 anim;
16 static CURRENT_ANIM: Cell<Option<AnimSpec>> = const { Cell::new(None) };
17}
18
19/// The ambient animation set by the innermost enclosing [`with_animation`], if any.
20pub fn current_anim() -> Option<AnimSpec> {
21 CURRENT_ANIM.with(|c| c.get())
22}
23
24/// Restores the previous ambient animation on drop (panic-safe).
25struct Restore(Option<AnimSpec>);
26impl Drop for Restore {
27 fn drop(&mut self) {
28 CURRENT_ANIM.with(|c| c.set(self.0));
29 }
30}
31
32/// Run `f` with `spec` as the ambient animation, restoring the previous value afterwards.
33pub(crate) fn with_current_anim<R>(spec: AnimSpec, f: impl FnOnce() -> R) -> R {
34 let prev = CURRENT_ANIM.with(|c| c.replace(Some(spec)));
35 let _restore = Restore(prev);
36 f()
37}
38
39/// Explicitly animate every state change made in `f`, Day's equivalent of SwiftUI's
40/// `withAnimation`. The mutation runs inside a [`day_reactive::batch`]; that batch's synchronous
41/// fixpoint drain (bindings → `patch`, plus the turn-end layout → `set_frame`) executes while
42/// `spec` is ambient, so the resulting native updates carry the animation intent and the toolkit
43/// animates them on its own compositor. Nesting overrides; the previous ambient restores after.
44///
45/// Edge case: if called from *inside* an in-progress drain (rare: mutating within a reactive
46/// effect), the batch defers to the ongoing drain and the intent is not captured; the change then
47/// applies instantly. This matches SwiftUI's transaction boundaries.
48pub fn with_animation<R>(spec: AnimSpec, f: impl FnOnce() -> R) -> R {
49 with_current_anim(spec, || {
50 // Coalesce the writes, then force the drain to run now, while `spec` is still ambient,
51 // rather than deferring to the enclosing batch's close (event dispatch runs handlers inside
52 // a batch, so that close happens after this scope ends and the intent would be lost).
53 let r = day_reactive::batch(f);
54 day_reactive::flush_now();
55 r
56 })
57}