use super::animations::AnimationState;
use super::{Color, Padding, Scale, Shadow, Translate};
use crate::clock::FrameInstant;
use crate::jobs::{JobRequest, JobType, RequiredJob, request_job};
use crate::reactive::with_signal_tracking;
use crate::tree::{LayoutCtx, WidgetId};
use crate::widgets::Corners;
macro_rules! animated_properties {
($emit:ident) => {
$emit! {
width: f32 as Length, target: none, layout: yes,
timeline: no,
test {
value: |k| k,
declared_as: |v| container().width(v).height(100.0),
probe: |h: &mut H| h.tree.cached_size(h.root).unwrap().width,
from: 10.0, to: 150.0, then: 50.0, base: 10.0,
};
height: f32 as Length, target: none, layout: yes,
timeline: no,
test {
value: |k| k,
declared_as: |v| container().width(200.0).height(v),
probe: |h: &mut H| h.tree.cached_size(h.root).unwrap().height,
from: 10.0, to: 150.0, then: 50.0, base: 10.0,
};
padding: Padding as Padding, target: effective_padding_target, layout: yes,
timeline: yes,
test {
value: |k| Padding::all(k),
declared_as: |v| container().padding(v).child(box_of(10.0, 10.0)),
probe: |h: &mut H| {
h.paint();
(h.tree.cached_size(h.root).unwrap().width - 10.0) / 2.0
},
from: 4.0, to: 40.0, then: 16.0, base: 4.0,
};
background: Color as Color, target: effective_background_target, layout: no,
timeline: yes,
test {
value: |k| Color::rgb(k, 0.0, 0.0),
declared_as: |v| container().width(200.0).height(100.0).background(v),
probe: |h: &mut H| rects(&h.paint())[0].1.r,
from: 0.0, to: 1.0, then: 0.5, base: 0.0,
};
corners: Corners as Corners, target: effective_corners_target, layout: no,
timeline: yes,
test {
value: |k| crate::widgets::Corners::rounded(k),
declared_as: |v| container().width(200.0).height(100.0)
.background(Color::RED).corners(v),
probe: |h: &mut H| painted_style(&h.paint()).1.top_left,
from: 2.0, to: 20.0, then: 8.0, base: 2.0,
};
shadow: Shadow as Shadow, target: effective_shadow_target, layout: no,
timeline: yes,
test {
value: |k| Shadow::new((0.0, 0.0), k, 0.0, Color::BLACK),
declared_as: |v| container().width(200.0).height(100.0)
.background(Color::RED).shadow(v),
probe: |h: &mut H| painted_style(&h.paint()).3.unwrap().blur,
from: 2.0, to: 20.0, then: 8.0,
base: 20.0,
};
border_width: f32 as f32, target: effective_border_width_target, layout: no,
timeline: yes,
test {
value: |k| k,
declared_as: |v| container().width(200.0).height(100.0)
.background(Color::RED).border(v, Color::WHITE),
probe: |h: &mut H| borders(&h.paint())[0].0,
from: 1.0, to: 10.0, then: 4.0, base: 1.0,
};
border_color: Color as Color, target: effective_border_color_target, layout: no,
timeline: yes,
test {
value: |k| Color::rgb(k, 0.0, 0.0),
declared_as: |v| container().width(200.0).height(100.0)
.background(Color::RED).border(2.0, v),
probe: |h: &mut H| borders(&h.paint())[0].1.r,
from: 0.0, to: 1.0, then: 0.5, base: 0.0,
};
translate: Translate as Translate, target: effective_translate_target, layout: no,
timeline: yes,
test {
value: |k| Translate::new(k, 0.0),
declared_as: |v| container().width(200.0).height(100.0).translate(v),
probe: |h: &mut H| h.paint().local_transform.tx(),
from: 5.0, to: 50.0, then: 20.0, base: 5.0,
};
rotate: f32 as f32, target: effective_rotate_target, layout: no,
timeline: yes,
test {
value: |k| k,
declared_as: |v| container().width(200.0).height(100.0).rotate(v),
probe: |h: &mut H| turned_degrees(&h.paint()),
from: 4.0, to: 40.0, then: 16.0, base: 4.0,
};
scale: Scale as Scale, target: effective_scale_target, layout: no,
timeline: yes,
test {
value: |k| Scale::uniform(k),
declared_as: |v| container().width(200.0).height(100.0).scale(v),
probe: |h: &mut H| h.paint().local_transform.data[0],
from: 0.5, to: 2.0, then: 1.25, base: 0.5,
};
opacity: f32 as f32, target: effective_opacity_target, layout: no,
timeline: yes,
test {
value: |k| k,
declared_as: |v| container().width(200.0).height(100.0)
.background(Color::RED).opacity(v),
probe: |h: &mut H| h.paint().opacity,
from: 0.2, to: 1.0, then: 0.5, base: 0.2,
};
}
};
}
#[cfg(test)]
pub(crate) use animated_properties;
macro_rules! emit_store {
($($name:ident: $anim:ty as $decl:ty, target: $target:tt, layout: $layout:ident,
timeline: $timeline:ident,
test { $($test:tt)* };)*) => {
#[allow(non_camel_case_types)]
pub(crate) enum AnimSlot {
$($name(AnimationState<$anim>),)*
}
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum AnimKind {
$($name,)*
}
impl AnimSlot {
pub(crate) fn kind(&self) -> AnimKind {
match self {
$(AnimSlot::$name(_) => AnimKind::$name,)*
}
}
}
#[derive(Default)]
pub(crate) struct ContainerAnims {
slots: smallvec::SmallVec<[AnimSlot; 2]>,
}
impl ContainerAnims {
pub(crate) fn set(&mut self, slot: AnimSlot) {
match self.slots.iter_mut().find(|s| s.kind() == slot.kind()) {
Some(existing) => *existing = slot,
None => self.slots.push(slot),
}
}
pub(crate) fn clear(&mut self, kind: AnimKind) {
self.slots.retain(|slot| slot.kind() != kind);
}
#[cfg(test)]
pub(crate) fn declared_count(&self) -> usize {
self.slots.len()
}
pub(crate) fn slots(&self) -> impl Iterator<Item = &AnimSlot> {
self.slots.iter()
}
pub(crate) fn slots_mut(&mut self) -> impl Iterator<Item = &mut AnimSlot> {
self.slots.iter_mut()
}
$(
pub(crate) fn $name(
&self,
) -> Option<&AnimationState<$anim>> {
self.slots.iter().find_map(|slot| match slot {
AnimSlot::$name(anim) => Some(anim),
_ => None,
})
}
)*
}
};
}
animated_properties!(emit_store);
macro_rules! follows_a_signal {
(none) => {
false
};
($target:ident) => {
true
};
}
macro_rules! moves_the_box {
(yes) => {
true
};
(no) => {
false
};
}
macro_rules! emit_slot_facts {
($($name:ident: $anim:ty as $decl:ty, target: $target:tt, layout: $layout:ident,
timeline: $timeline:ident,
test { $($test:tt)* };)*) => {
impl AnimSlot {
pub(crate) fn follows_a_signal(&self) -> bool {
match self {
$(AnimSlot::$name(_) => follows_a_signal!($target),)*
}
}
pub(crate) fn moves_the_box(&self) -> bool {
match self {
$(AnimSlot::$name(_) => moves_the_box!($layout),)*
}
}
pub(crate) fn is_initial(&self) -> bool {
match self {
$(AnimSlot::$name(anim) => anim.is_initial(),)*
}
}
pub(crate) fn is_animating(&self) -> bool {
match self {
$(AnimSlot::$name(anim) => anim.is_animating(),)*
}
}
pub(crate) fn declares_exit(&self) -> bool {
match self {
$(AnimSlot::$name(anim) => anim.declares_exit(),)*
}
}
pub(crate) fn begin_exit(&mut self, now: FrameInstant) -> bool {
match self {
$(AnimSlot::$name(anim) => anim.begin_exit(now),)*
}
}
pub(crate) fn is_leaving(&self) -> bool {
match self {
$(AnimSlot::$name(anim) => anim.is_leaving(),)*
}
}
pub(crate) fn cancel_exit(&mut self) {
match self {
$(AnimSlot::$name(anim) => anim.cancel_exit(),)*
}
}
}
};
}
animated_properties!(emit_slot_facts);
macro_rules! retarget {
(none, $anim:expr, $container:expr, $id:expr, $now:expr) => {};
($target:ident, $anim:expr, $container:expr, $id:expr, $now:expr) => {
if $anim.take_play() {
$anim.play($now);
}
$anim.animate_to($container.$target($id), $now);
};
}
macro_rules! seed_from_target {
(none, $anim:expr, $container:expr, $id:expr, $now:expr, $entered:expr,
$ctx:expr) => {
let _ = $anim;
};
($target:ident, $anim:expr, $container:expr, $id:expr, $now:expr, $entered:expr,
$ctx:expr) => {
if $anim.is_initial() {
$entered |=
super::anim_bridge::seed_or_enter($ctx, $anim, $container.$target($id), $now);
}
};
}
macro_rules! drift_from_target {
(none, $anim:expr, $container:expr, $id:expr, $drift:expr) => {
let _ = $anim;
};
($target:ident, $anim:expr, $container:expr, $id:expr, $drift:expr) => {
$drift |= !$anim.is_initial() && *$anim.target() != $container.$target($id);
$drift |= $anim.wants_play();
};
}
macro_rules! emit_passes {
($($name:ident: $anim:ty as $decl:ty, target: $target:tt, layout: $layout:ident,
timeline: $timeline:ident,
test { $($test:tt)* };)*) => {
impl super::Container {
pub(super) fn seed_animations(&mut self, ctx: &mut LayoutCtx, id: WidgetId) {
if !self
.anims
.as_deref()
.is_some_and(|declared| declared.slots().any(AnimSlot::is_initial))
{
return;
}
let now = ctx.tree_ref().frame_instant();
let mut anims = self.anims.take();
let mut entered = false;
if let Some(declared) = anims.as_deref_mut() {
with_signal_tracking(id, JobType::Animation, || {
for slot in declared.slots_mut() {
match slot {
$(AnimSlot::$name(anim) => {
seed_from_target!(
$target, anim, self, id,
|| now, entered, ctx
);
})*
}
}
});
}
self.anims = anims;
if entered {
request_job(id, JobRequest::Animation(RequiredJob::Paint));
}
}
pub(super) fn resync_animation_targets(&self, id: WidgetId) {
let Some(declared) = self.anims.as_deref() else {
return;
};
if !declared.slots().any(AnimSlot::follows_a_signal) || self.is_leaving() {
return;
}
let drifted = with_signal_tracking(id, JobType::Animation, || {
let mut drift = false;
for slot in declared.slots() {
match slot {
$(AnimSlot::$name(anim) => {
drift_from_target!($target, anim, self, id, drift);
})*
}
}
drift
});
if drifted {
request_job(id, JobRequest::Animation(RequiredJob::None));
}
}
pub(super) fn advance_declared_animations(
&mut self,
id: WidgetId,
now: FrameInstant,
) -> bool {
let mut anims = self.anims.take();
let mut any_animating = false;
if let Some(declared) = anims.as_deref_mut() {
let leaving = declared.slots().any(AnimSlot::is_leaving);
crate::reactive::diagnostics::snapshot_zone(|| {
for slot in declared.slots_mut() {
match slot {
$(AnimSlot::$name(anim) => {
if !leaving {
retarget!($target, anim, self, id, now);
}
if anim.is_animating() {
any_animating = true;
let required =
match (anim.advance(now).is_changed(), moves_the_box!($layout)) {
(false, _) => RequiredJob::None,
(true, true) => RequiredJob::Layout,
(true, false) => RequiredJob::Paint,
};
request_job(id, JobRequest::Animation(required));
}
})*
}
}
});
}
self.anims = anims;
any_animating
}
}
};
}
animated_properties!(emit_passes);
macro_rules! emit_declare {
(none, $name:ident, $decl:ty) => {
pub(in crate::widgets::container) fn $name<M>(
anims: &mut Option<Box<ContainerAnims>>,
value: impl crate::animation::IntoAnimated<$decl, M>,
) -> crate::reactive::Prop<$decl> {
declare_size(anims, value, AnimKind::$name, AnimSlot::$name)
}
};
($target:ident, $name:ident, $decl:ty) => {
pub(in crate::widgets::container) fn $name<M>(
anims: &mut Option<Box<ContainerAnims>>,
value: impl crate::animation::IntoAnimated<$decl, M>,
) -> crate::reactive::Prop<$decl> {
declare_anim(anims, value, AnimKind::$name, AnimSlot::$name)
}
};
}
macro_rules! emit_declarers {
($($name:ident: $anim:ty as $decl:ty, target: $target:tt, layout: $layout:ident,
timeline: $timeline:ident,
test { $($test:tt)* };)*) => {
pub(in crate::widgets::container) mod declare {
use super::super::{declare_anim, declare_size};
use super::{AnimKind, AnimSlot, ContainerAnims};
use crate::layout::Length;
use crate::widgets::container::{Color, Padding, Scale, Shadow, Translate};
use crate::widgets::Corners;
$(emit_declare!($target, $name, $decl);)*
}
};
}
animated_properties!(emit_declarers);