Skip to main content

compose_rust/
animation.rs

1//! Animations the Renderer plays on its own frame clock, and the platform's reduced motion
2//! setting.
3//!
4//! An animation is described once and played to its end by the Renderer. Nothing crosses
5//! the boundary while it plays unless it asked for events, and the value it shows at any
6//! moment is never sent back: animation state is the Renderer's.
7//!
8//! [`start_animation`] and [`control_animation`] queue the record for the next batch, the
9//! way a message or a notification is queued, so they can be called from an event handler,
10//! a component or a worker thread alike. A runtime that writes the tree itself can write
11//! [`Mutation::StartAnimation`] into its batch directly; the result is the same record.
12//!
13//! [`Mutation::StartAnimation`]: crate::protocol::Mutation::StartAnimation
14
15use crate::protocol::Mutation;
16use crate::schema::{
17    AnimatedProperty, Animation, AnimationControl, AnimationEventKind, ReducedMotion,
18};
19use std::cell::{Cell, RefCell};
20use std::rc::Rc;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Mutex, MutexGuard};
23
24/// One moment the Renderer reported about an animation it is playing.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct AnimationMoment {
27    /// The node the animation plays on.
28    pub node_id: u32,
29    pub animation_id: u32,
30    pub kind: AnimationEventKind,
31    pub property: AnimatedProperty,
32    pub slot: u8,
33    pub iteration: u32,
34    /// Active time without the delay, in ms, which is CSS's `elapsedTime`.
35    pub elapsed_ms: f32,
36    /// The frame it happened in, on the Renderer's frame clock. For `Ready`, the start
37    /// time the animation was given.
38    pub time_nanos: u64,
39}
40
41enum Command {
42    Start(Animation<'static>),
43    Control {
44        node_id: u32,
45        animation_id: u32,
46        property: AnimatedProperty,
47        slot: u8,
48        op: AnimationControl,
49        at_time_nanos: u64,
50    },
51}
52
53/// Records waiting for the next batch. Shared across threads, because a worker may start
54/// an animation as well as a handler can.
55static QUEUE: Mutex<Vec<Command>> = Mutex::new(Vec::new());
56
57/// Whether the queue has anything in it, read without the lock. Almost every batch finds
58/// nothing, and then looking costs one atomic load.
59static PENDING: AtomicBool = AtomicBool::new(false);
60
61fn queue() -> MutexGuard<'static, Vec<Command>> {
62    QUEUE.lock().unwrap_or_else(|poison| poison.into_inner())
63}
64
65fn submit(command: Command) {
66    queue().push(command);
67    PENDING.store(true, Ordering::Release);
68    // Inside a Host call the batch that call produces carries it.
69    if !crate::boundary::in_host_call() {
70        crate::boundary::request_frame_from_worker();
71    }
72}
73
74/// Asks the Renderer to play `animation` to its end. It goes out with the next batch.
75pub fn start_animation(animation: Animation<'static>) {
76    submit(Command::Start(animation));
77}
78
79/// Pauses, resumes or cancels the animation playing under `(node_id, property, slot)`,
80/// if it is still the one called `animation_id`. It goes out with the next batch.
81pub fn control_animation(
82    node_id: u32,
83    animation_id: u32,
84    property: AnimatedProperty,
85    slot: u8,
86    op: AnimationControl,
87    at_time_nanos: u64,
88) {
89    submit(Command::Control {
90        node_id,
91        animation_id,
92        property,
93        slot,
94        op,
95        at_time_nanos,
96    });
97}
98
99/// Hands every queued record to `emit` and empties the queue, reusing its buffer.
100pub(crate) fn drain(mut emit: impl FnMut(Mutation<'_>)) {
101    if !PENDING.swap(false, Ordering::AcqRel) {
102        return;
103    }
104    let mut taken = std::mem::take(&mut *queue());
105    for command in taken.drain(..) {
106        emit(match command {
107            Command::Start(animation) => Mutation::StartAnimation(animation),
108            Command::Control {
109                node_id,
110                animation_id,
111                property,
112                slot,
113                op,
114                at_time_nanos,
115            } => Mutation::ControlAnimation {
116                node_id,
117                animation_id,
118                property,
119                slot,
120                op,
121                at_time_nanos,
122            },
123        });
124    }
125    let mut guard = queue();
126    if guard.is_empty() {
127        *guard = taken;
128    }
129}
130
131struct Subscriber {
132    id: u64,
133    notify: Arc<dyn Fn() + Send + Sync>,
134}
135
136type MomentHandler = Rc<dyn Fn(AnimationMoment)>;
137
138thread_local! {
139    /// Unknown until the Renderer says, which it does once after start.
140    static REDUCED_MOTION: Cell<ReducedMotion> = const { Cell::new(ReducedMotion::Unknown) };
141    static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
142    static MOMENT_HANDLERS: RefCell<Vec<(u64, MomentHandler)>> = const { RefCell::new(Vec::new()) };
143    static NEXT_ID: Cell<u64> = const { Cell::new(1) };
144}
145
146fn next_id() -> u64 {
147    NEXT_ID.with(|next| {
148        let id = next.get();
149        next.set(id + 1);
150        id
151    })
152}
153
154/// Whether the platform asks for reduced motion, as the Renderer last reported it.
155///
156/// What to do about it is the application's decision, the way a CSS media query is the
157/// page's: the Renderer plays every animation it is sent as it was described.
158pub fn reduced_motion() -> ReducedMotion {
159    REDUCED_MOTION.with(Cell::get)
160}
161
162/// Records the Renderer's answer and wakes the readers. Returns whether any was woken.
163pub(crate) fn publish_reduced_motion(state: ReducedMotion) -> bool {
164    if REDUCED_MOTION.with(Cell::get) == state {
165        return false;
166    }
167    REDUCED_MOTION.with(|current| current.set(state));
168    SUBSCRIBERS.with_borrow(|subscribers| {
169        for subscriber in subscribers {
170            (subscriber.notify)();
171        }
172        !subscribers.is_empty()
173    })
174}
175
176/// A registration for changes of the reduced motion setting, ended by dropping it.
177pub struct ReducedMotionSubscription {
178    id: u64,
179}
180
181/// Registers `notify` to be called each time the setting changes. A runtime's
182/// `use_reduced_motion` holds one for as long as the reader it wakes is alive.
183pub fn subscribe_reduced_motion(notify: Arc<dyn Fn() + Send + Sync>) -> ReducedMotionSubscription {
184    let id = next_id();
185    SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
186    ReducedMotionSubscription { id }
187}
188
189impl Drop for ReducedMotionSubscription {
190    fn drop(&mut self) {
191        SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.retain(|s| s.id != self.id));
192    }
193}
194
195/// A registration for animation events, ended by dropping it.
196pub struct AnimationEventSubscription {
197    id: u64,
198}
199
200/// Calls `handler` with every animation event the Renderer reports. Only the kinds an
201/// animation asked for are ever reported.
202pub fn on_animation_event(handler: Rc<dyn Fn(AnimationMoment)>) -> AnimationEventSubscription {
203    let id = next_id();
204    MOMENT_HANDLERS.with_borrow_mut(|handlers| handlers.push((id, handler)));
205    AnimationEventSubscription { id }
206}
207
208impl Drop for AnimationEventSubscription {
209    fn drop(&mut self) {
210        MOMENT_HANDLERS.with_borrow_mut(|handlers| handlers.retain(|(id, _)| *id != self.id));
211    }
212}
213
214/// Hands one moment to every registered handler. Returns whether any was registered.
215pub(crate) fn deliver(moment: AnimationMoment) -> bool {
216    // Copied out first, because a handler may register or drop one.
217    let handlers: Vec<MomentHandler> =
218        MOMENT_HANDLERS.with_borrow(|handlers| handlers.iter().map(|(_, h)| h.clone()).collect());
219    for handler in &handlers {
220        handler(moment);
221    }
222    !handlers.is_empty()
223}
224
225/// Forgets the setting, the readers, the handlers and anything still queued. Used between
226/// tests.
227#[doc(hidden)]
228pub fn reset_animations() {
229    queue().clear();
230    PENDING.store(false, Ordering::Release);
231    REDUCED_MOTION.with(|current| current.set(ReducedMotion::Unknown));
232    SUBSCRIBERS.with_borrow_mut(Vec::clear);
233    MOMENT_HANDLERS.with_borrow_mut(Vec::clear);
234}
235
236/// How long a change takes and along which curve.
237///
238/// A role is the default way to say this (`MotionRole`), and the design system answers
239/// it; this is the escape hatch for a screen whose author has already decided, as CSS
240/// does. Without one, [`AnimationSpec::STANDARD`] is used: the standard motion of
241/// Material 3, the length and curve the design systems' own standard role is built on.
242#[derive(Clone, Copy, Debug, PartialEq)]
243pub struct AnimationSpec {
244    pub duration_ms: f32,
245    pub delay_ms: f32,
246    pub timing: crate::schema::Timing,
247}
248
249impl AnimationSpec {
250    /// 300 ms along cubic-bezier(0.2, 0, 0, 1).
251    pub const STANDARD: Self = Self {
252        duration_ms: 300.0,
253        delay_ms: 0.0,
254        timing: crate::schema::Timing::CubicBezier {
255            x1: 0.2,
256            y1: 0.0,
257            x2: 0.0,
258            y2: 1.0,
259        },
260    };
261
262    /// A change of `duration_ms` along `timing`, starting at once.
263    pub const fn tween(duration_ms: f32, timing: crate::schema::Timing) -> Self {
264        Self {
265            duration_ms,
266            delay_ms: 0.0,
267            timing,
268        }
269    }
270}
271
272impl Default for AnimationSpec {
273    fn default() -> Self {
274        Self::STANDARD
275    }
276}
277
278/// A transition to `to`: from whatever the node shows the frame it starts, to `to`, once,
279/// along `spec`, in slot 0, with no events. What a CSS transition and an
280/// `animate_*_as_state` both are.
281pub fn transition(
282    node_id: u32,
283    animation_id: u32,
284    property: AnimatedProperty,
285    to: crate::schema::KeyframeValue,
286    spec: AnimationSpec,
287) -> Animation<'static> {
288    use crate::schema::{
289        AnimationEvents, ColorInterpolation, FillMode, Keyframe, PlayState, PlaybackDirection,
290        Timing,
291    };
292    let colour = matches!(
293        property,
294        AnimatedProperty::Color | AnimatedProperty::Background | AnimatedProperty::BorderColor
295    );
296    let transform = property == AnimatedProperty::Transform;
297    Animation {
298        node_id,
299        animation_id,
300        property,
301        slot: 0,
302        direction: PlaybackDirection::Normal,
303        fill: FillMode::None,
304        play_state: PlayState::Running,
305        interpolation: colour.then_some(ColorInterpolation::SrgbPremultiplied),
306        start_time_nanos: 0,
307        delay_ms: spec.delay_ms,
308        duration_ms: spec.duration_ms,
309        iterations: 1.0,
310        origin_x: if transform { 0.5 } else { 0.0 },
311        origin_y: if transform { 0.5 } else { 0.0 },
312        events: AnimationEvents::NONE,
313        keyframes: std::borrow::Cow::Owned(vec![
314            Keyframe {
315                offset: 0.0,
316                timing: spec.timing,
317                from_presented: true,
318                value: to.clone(),
319            },
320            Keyframe {
321                offset: 1.0,
322                timing: Timing::Linear,
323                from_presented: false,
324                value: to,
325            },
326        ]),
327    }
328}
329
330/// What Compose's `graphicsLayer` takes, drawn without moving the layout: a rotation in
331/// degrees, a scale, a translation in dp, the origin they turn about as a fraction of the
332/// node's size, and an opacity for the whole group.
333#[derive(Clone, Copy, Debug, PartialEq)]
334pub struct GraphicsLayer {
335    pub rotation_z: f32,
336    pub scale_x: f32,
337    pub scale_y: f32,
338    pub translation_x: f32,
339    pub translation_y: f32,
340    pub transform_origin: (f32, f32),
341    pub alpha: f32,
342}
343
344impl Default for GraphicsLayer {
345    fn default() -> Self {
346        Self {
347            rotation_z: 0.0,
348            scale_x: 1.0,
349            scale_y: 1.0,
350            translation_x: 0.0,
351            translation_y: 0.0,
352            transform_origin: (0.5, 0.5),
353            alpha: 1.0,
354        }
355    }
356}
357
358impl GraphicsLayer {
359    /// The `Transform` this layer draws: scaled, then turned, about its origin, then moved,
360    /// which is the order a Compose layer applies them in.
361    pub fn transform(&self) -> crate::schema::Modifier {
362        let radians = self.rotation_z.to_radians();
363        let (sin, cos) = radians.sin_cos();
364        crate::schema::Modifier::Transform {
365            a: self.scale_x * cos,
366            b: self.scale_x * sin,
367            c: -self.scale_y * sin,
368            d: self.scale_y * cos,
369            e: self.translation_x,
370            f: self.translation_y,
371            origin_x: self.transform_origin.0,
372            origin_y: self.transform_origin.1,
373        }
374    }
375
376    /// The `Alpha` this layer fades its group with.
377    pub fn alpha(&self) -> crate::schema::Modifier {
378        crate::schema::Modifier::Alpha(self.alpha)
379    }
380}
381
382/// A value the Renderer animates to whenever it changes: what [`animate_float_as_state`]
383/// and its siblings return. [`value`](Self::value) is the target, because the value in
384/// between is the Renderer's and is never sent back.
385///
386/// Given to a Modifier (`animated_alpha`, `animated_background`, `animated_offset`). The
387/// first value a node is given is where it starts; every later one is a change, written
388/// as one `StartAnimation` that the Renderer plays on its own clock, so nothing is composed
389/// again while it plays.
390#[derive(Clone, Copy, Debug, PartialEq)]
391pub struct Animated<T> {
392    pub target: T,
393    pub spec: AnimationSpec,
394}
395
396impl<T: Copy> Animated<T> {
397    /// The target, which is also what the screen shows once the change has played.
398    pub fn value(&self) -> T {
399        self.target
400    }
401}
402
403/// An opacity the Renderer animates to whenever `target` changes. Compose's
404/// `animateFloatAsState`, for the one float a node draws without laying out again.
405pub fn animate_float_as_state(target: f32, spec: AnimationSpec) -> Animated<f32> {
406    Animated { target, spec }
407}
408
409/// A background the Renderer animates to whenever `target` changes.
410pub fn animate_color_as_state(
411    target: crate::schema::Paint,
412    spec: AnimationSpec,
413) -> Animated<crate::schema::Paint> {
414    Animated { target, spec }
415}
416
417/// A translation in dp the Renderer animates to whenever `target` changes. Drawn as a
418/// transform, so the layout does not move.
419pub fn animate_offset_as_state(target: (f32, f32), spec: AnimationSpec) -> Animated<(f32, f32)> {
420    Animated { target, spec }
421}
422
423/// How an animated value travels as a widget attribute, which carries scalars and bytes
424/// only: the target's words, then the spec. Read back by [`read_animated`].
425pub(crate) fn animated_bytes(target: &[u32], spec: &AnimationSpec) -> Vec<u8> {
426    use crate::schema::Timing;
427    let mut out = Vec::with_capacity(4 * target.len() + 28);
428    out.push(target.len() as u8);
429    for word in target {
430        out.extend_from_slice(&word.to_le_bytes());
431    }
432    out.extend_from_slice(&spec.duration_ms.to_le_bytes());
433    out.extend_from_slice(&spec.delay_ms.to_le_bytes());
434    match spec.timing {
435        Timing::Linear => out.push(1),
436        Timing::CubicBezier { x1, y1, x2, y2 } => {
437            out.push(2);
438            for value in [x1, y1, x2, y2] {
439                out.extend_from_slice(&value.to_le_bytes());
440            }
441        }
442        Timing::Steps { count, position } => {
443            out.push(3);
444            out.extend_from_slice(&count.to_le_bytes());
445            out.extend_from_slice(&u16::from(position).to_le_bytes());
446        }
447    }
448    out
449}
450
451/// The target's words and the spec [`animated_bytes`] wrote, or `None` for bytes it did
452/// not write.
453pub(crate) fn read_animated(bytes: &[u8]) -> Option<(Vec<u32>, AnimationSpec)> {
454    use crate::schema::{StepPosition, Timing};
455    let word = |at: usize| -> Option<u32> {
456        Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
457    };
458    let float = |at: usize| word(at).map(f32::from_bits);
459    let count = usize::from(*bytes.first()?);
460    let target = (0..count)
461        .map(|index| word(1 + 4 * index))
462        .collect::<Option<Vec<_>>>()?;
463    let at = 1 + 4 * count;
464    let duration_ms = float(at)?;
465    let delay_ms = float(at + 4)?;
466    let timing = match *bytes.get(at + 8)? {
467        1 => Timing::Linear,
468        2 => Timing::CubicBezier {
469            x1: float(at + 9)?,
470            y1: float(at + 13)?,
471            x2: float(at + 17)?,
472            y2: float(at + 21)?,
473        },
474        3 => Timing::Steps {
475            count: word(at + 9)?,
476            position: StepPosition::try_from(u16::from_le_bytes(
477                bytes.get(at + 13..at + 15)?.try_into().ok()?,
478            ))
479            .ok()?,
480        },
481        _ => return None,
482    };
483    Some((
484        target,
485        AnimationSpec {
486            duration_ms,
487            delay_ms,
488            timing,
489        },
490    ))
491}