use crate::protocol::Mutation;
use crate::schema::{
AnimatedProperty, Animation, AnimationControl, AnimationEventKind, ReducedMotion,
};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnimationMoment {
pub node_id: u32,
pub animation_id: u32,
pub kind: AnimationEventKind,
pub property: AnimatedProperty,
pub slot: u8,
pub iteration: u32,
pub elapsed_ms: f32,
pub time_nanos: u64,
}
enum Command {
Start(Animation<'static>),
Control {
node_id: u32,
animation_id: u32,
property: AnimatedProperty,
slot: u8,
op: AnimationControl,
at_time_nanos: u64,
},
}
static QUEUE: Mutex<Vec<Command>> = Mutex::new(Vec::new());
static PENDING: AtomicBool = AtomicBool::new(false);
fn queue() -> MutexGuard<'static, Vec<Command>> {
QUEUE.lock().unwrap_or_else(|poison| poison.into_inner())
}
fn submit(command: Command) {
queue().push(command);
PENDING.store(true, Ordering::Release);
if !crate::boundary::in_host_call() {
crate::boundary::request_frame_from_worker();
}
}
pub fn start_animation(animation: Animation<'static>) {
submit(Command::Start(animation));
}
pub fn control_animation(
node_id: u32,
animation_id: u32,
property: AnimatedProperty,
slot: u8,
op: AnimationControl,
at_time_nanos: u64,
) {
submit(Command::Control {
node_id,
animation_id,
property,
slot,
op,
at_time_nanos,
});
}
pub(crate) fn drain(mut emit: impl FnMut(Mutation<'_>)) {
if !PENDING.swap(false, Ordering::AcqRel) {
return;
}
let mut taken = std::mem::take(&mut *queue());
for command in taken.drain(..) {
emit(match command {
Command::Start(animation) => Mutation::StartAnimation(animation),
Command::Control {
node_id,
animation_id,
property,
slot,
op,
at_time_nanos,
} => Mutation::ControlAnimation {
node_id,
animation_id,
property,
slot,
op,
at_time_nanos,
},
});
}
let mut guard = queue();
if guard.is_empty() {
*guard = taken;
}
}
struct Subscriber {
id: u64,
notify: Arc<dyn Fn() + Send + Sync>,
}
type MomentHandler = Rc<dyn Fn(AnimationMoment)>;
thread_local! {
static REDUCED_MOTION: Cell<ReducedMotion> = const { Cell::new(ReducedMotion::Unknown) };
static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
static MOMENT_HANDLERS: RefCell<Vec<(u64, MomentHandler)>> = const { RefCell::new(Vec::new()) };
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
}
fn next_id() -> u64 {
NEXT_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
})
}
pub fn reduced_motion() -> ReducedMotion {
REDUCED_MOTION.with(Cell::get)
}
pub(crate) fn publish_reduced_motion(state: ReducedMotion) -> bool {
if REDUCED_MOTION.with(Cell::get) == state {
return false;
}
REDUCED_MOTION.with(|current| current.set(state));
SUBSCRIBERS.with_borrow(|subscribers| {
for subscriber in subscribers {
(subscriber.notify)();
}
!subscribers.is_empty()
})
}
pub struct ReducedMotionSubscription {
id: u64,
}
pub fn subscribe_reduced_motion(notify: Arc<dyn Fn() + Send + Sync>) -> ReducedMotionSubscription {
let id = next_id();
SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
ReducedMotionSubscription { id }
}
impl Drop for ReducedMotionSubscription {
fn drop(&mut self) {
SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.retain(|s| s.id != self.id));
}
}
pub struct AnimationEventSubscription {
id: u64,
}
pub fn on_animation_event(handler: Rc<dyn Fn(AnimationMoment)>) -> AnimationEventSubscription {
let id = next_id();
MOMENT_HANDLERS.with_borrow_mut(|handlers| handlers.push((id, handler)));
AnimationEventSubscription { id }
}
impl Drop for AnimationEventSubscription {
fn drop(&mut self) {
MOMENT_HANDLERS.with_borrow_mut(|handlers| handlers.retain(|(id, _)| *id != self.id));
}
}
pub(crate) fn deliver(moment: AnimationMoment) -> bool {
let handlers: Vec<MomentHandler> =
MOMENT_HANDLERS.with_borrow(|handlers| handlers.iter().map(|(_, h)| h.clone()).collect());
for handler in &handlers {
handler(moment);
}
!handlers.is_empty()
}
#[doc(hidden)]
pub fn reset_animations() {
queue().clear();
PENDING.store(false, Ordering::Release);
REDUCED_MOTION.with(|current| current.set(ReducedMotion::Unknown));
SUBSCRIBERS.with_borrow_mut(Vec::clear);
MOMENT_HANDLERS.with_borrow_mut(Vec::clear);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnimationSpec {
pub duration_ms: f32,
pub delay_ms: f32,
pub timing: crate::schema::Timing,
}
impl AnimationSpec {
pub const STANDARD: Self = Self {
duration_ms: 300.0,
delay_ms: 0.0,
timing: crate::schema::Timing::CubicBezier {
x1: 0.2,
y1: 0.0,
x2: 0.0,
y2: 1.0,
},
};
pub const fn tween(duration_ms: f32, timing: crate::schema::Timing) -> Self {
Self {
duration_ms,
delay_ms: 0.0,
timing,
}
}
}
impl Default for AnimationSpec {
fn default() -> Self {
Self::STANDARD
}
}
pub fn transition(
node_id: u32,
animation_id: u32,
property: AnimatedProperty,
to: crate::schema::KeyframeValue,
spec: AnimationSpec,
) -> Animation<'static> {
use crate::schema::{
AnimationEvents, ColorInterpolation, FillMode, Keyframe, PlayState, PlaybackDirection,
Timing,
};
let colour = matches!(
property,
AnimatedProperty::Color | AnimatedProperty::Background | AnimatedProperty::BorderColor
);
let transform = property == AnimatedProperty::Transform;
Animation {
node_id,
animation_id,
property,
slot: 0,
direction: PlaybackDirection::Normal,
fill: FillMode::None,
play_state: PlayState::Running,
interpolation: colour.then_some(ColorInterpolation::SrgbPremultiplied),
start_time_nanos: 0,
delay_ms: spec.delay_ms,
duration_ms: spec.duration_ms,
iterations: 1.0,
origin_x: if transform { 0.5 } else { 0.0 },
origin_y: if transform { 0.5 } else { 0.0 },
events: AnimationEvents::NONE,
keyframes: std::borrow::Cow::Owned(vec![
Keyframe {
offset: 0.0,
timing: spec.timing,
from_presented: true,
value: to.clone(),
},
Keyframe {
offset: 1.0,
timing: Timing::Linear,
from_presented: false,
value: to,
},
]),
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GraphicsLayer {
pub rotation_z: f32,
pub scale_x: f32,
pub scale_y: f32,
pub translation_x: f32,
pub translation_y: f32,
pub transform_origin: (f32, f32),
pub alpha: f32,
}
impl Default for GraphicsLayer {
fn default() -> Self {
Self {
rotation_z: 0.0,
scale_x: 1.0,
scale_y: 1.0,
translation_x: 0.0,
translation_y: 0.0,
transform_origin: (0.5, 0.5),
alpha: 1.0,
}
}
}
impl GraphicsLayer {
pub fn transform(&self) -> crate::schema::Modifier {
let radians = self.rotation_z.to_radians();
let (sin, cos) = radians.sin_cos();
crate::schema::Modifier::Transform {
a: self.scale_x * cos,
b: self.scale_x * sin,
c: -self.scale_y * sin,
d: self.scale_y * cos,
e: self.translation_x,
f: self.translation_y,
origin_x: self.transform_origin.0,
origin_y: self.transform_origin.1,
}
}
pub fn alpha(&self) -> crate::schema::Modifier {
crate::schema::Modifier::Alpha(self.alpha)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Animated<T> {
pub target: T,
pub spec: AnimationSpec,
}
impl<T: Copy> Animated<T> {
pub fn value(&self) -> T {
self.target
}
}
pub fn animate_float_as_state(target: f32, spec: AnimationSpec) -> Animated<f32> {
Animated { target, spec }
}
pub fn animate_color_as_state(
target: crate::schema::Paint,
spec: AnimationSpec,
) -> Animated<crate::schema::Paint> {
Animated { target, spec }
}
pub fn animate_offset_as_state(target: (f32, f32), spec: AnimationSpec) -> Animated<(f32, f32)> {
Animated { target, spec }
}
pub(crate) fn animated_bytes(target: &[u32], spec: &AnimationSpec) -> Vec<u8> {
use crate::schema::Timing;
let mut out = Vec::with_capacity(4 * target.len() + 28);
out.push(target.len() as u8);
for word in target {
out.extend_from_slice(&word.to_le_bytes());
}
out.extend_from_slice(&spec.duration_ms.to_le_bytes());
out.extend_from_slice(&spec.delay_ms.to_le_bytes());
match spec.timing {
Timing::Linear => out.push(1),
Timing::CubicBezier { x1, y1, x2, y2 } => {
out.push(2);
for value in [x1, y1, x2, y2] {
out.extend_from_slice(&value.to_le_bytes());
}
}
Timing::Steps { count, position } => {
out.push(3);
out.extend_from_slice(&count.to_le_bytes());
out.extend_from_slice(&u16::from(position).to_le_bytes());
}
}
out
}
pub(crate) fn read_animated(bytes: &[u8]) -> Option<(Vec<u32>, AnimationSpec)> {
use crate::schema::{StepPosition, Timing};
let word = |at: usize| -> Option<u32> {
Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
};
let float = |at: usize| word(at).map(f32::from_bits);
let count = usize::from(*bytes.first()?);
let target = (0..count)
.map(|index| word(1 + 4 * index))
.collect::<Option<Vec<_>>>()?;
let at = 1 + 4 * count;
let duration_ms = float(at)?;
let delay_ms = float(at + 4)?;
let timing = match *bytes.get(at + 8)? {
1 => Timing::Linear,
2 => Timing::CubicBezier {
x1: float(at + 9)?,
y1: float(at + 13)?,
x2: float(at + 17)?,
y2: float(at + 21)?,
},
3 => Timing::Steps {
count: word(at + 9)?,
position: StepPosition::try_from(u16::from_le_bytes(
bytes.get(at + 13..at + 15)?.try_into().ok()?,
))
.ok()?,
},
_ => return None,
};
Some((
target,
AnimationSpec {
duration_ms,
delay_ms,
timing,
},
))
}