use std::cell::RefCell;
use std::sync::Arc;
use crate::div::{ElementBuilder, ElementTypeId};
use crate::element::ElementBounds;
use crate::element::{MotionAnimation, MotionKeyframe, RenderProps};
use crate::key::InstanceKey;
thread_local! {
static MOTION_CONTEXT_STACK: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
pub fn push_motion_context(key: &str) {
MOTION_CONTEXT_STACK.with(|stack| {
stack.borrow_mut().push(key.to_string());
});
}
pub fn pop_motion_context() {
MOTION_CONTEXT_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
pub fn current_motion_key() -> Option<String> {
MOTION_CONTEXT_STACK.with(|stack| stack.borrow().last().cloned())
}
pub fn is_inside_animating_motion() -> bool {
if let Some(key) = current_motion_key() {
let state = blinc_core::query_motion(&key);
state.is_animating()
} else {
false
}
}
pub fn is_inside_motion() -> bool {
MOTION_CONTEXT_STACK.with(|stack| !stack.borrow().is_empty())
}
use crate::tree::{LayoutNodeId, LayoutTree};
use blinc_animation::{AnimatedValue, AnimationPreset, MultiKeyframeAnimation};
use blinc_core::Transform;
use taffy::{Display, FlexDirection, Style};
#[derive(Clone)]
pub struct ElementAnimation {
pub animation: MultiKeyframeAnimation,
}
impl ElementAnimation {
pub fn new(animation: MultiKeyframeAnimation) -> Self {
Self { animation }
}
pub fn with_delay(mut self, delay_ms: u32) -> Self {
self.animation = self.animation.delay(delay_ms);
self
}
}
impl From<MultiKeyframeAnimation> for ElementAnimation {
fn from(animation: MultiKeyframeAnimation) -> Self {
Self::new(animation)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StaggerDirection {
#[default]
Forward,
Reverse,
FromCenter,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SlideDirection {
Left,
Right,
Top,
Bottom,
}
#[derive(Clone)]
pub struct StaggerConfig {
pub delay_ms: u32,
pub animation: ElementAnimation,
pub direction: StaggerDirection,
pub limit: Option<usize>,
}
impl StaggerConfig {
pub fn new(delay_ms: u32, animation: impl Into<ElementAnimation>) -> Self {
Self {
delay_ms,
animation: animation.into(),
direction: StaggerDirection::Forward,
limit: None,
}
}
pub fn reverse(mut self) -> Self {
self.direction = StaggerDirection::Reverse;
self
}
pub fn from_center(mut self) -> Self {
self.direction = StaggerDirection::FromCenter;
self
}
pub fn limit(mut self, n: usize) -> Self {
self.limit = Some(n);
self
}
pub fn delay_for_index(&self, index: usize, total: usize) -> u32 {
let effective_index = match self.direction {
StaggerDirection::Forward => index,
StaggerDirection::Reverse => total.saturating_sub(1).saturating_sub(index),
StaggerDirection::FromCenter => {
let center = total / 2;
index.abs_diff(center)
}
};
let capped_index = if let Some(limit) = self.limit {
effective_index.min(limit)
} else {
effective_index
};
self.delay_ms * capped_index as u32
}
}
pub type SharedAnimatedValue = std::sync::Arc<std::sync::Mutex<AnimatedValue>>;
#[derive(Clone)]
pub struct TimelineRotation {
pub timeline: blinc_animation::SharedAnimatedTimeline,
pub entry_id: blinc_animation::TimelineEntryId,
}
#[derive(Clone, Default)]
pub struct MotionBindings {
pub translate_x: Option<SharedAnimatedValue>,
pub translate_y: Option<SharedAnimatedValue>,
pub scale: Option<SharedAnimatedValue>,
pub scale_x: Option<SharedAnimatedValue>,
pub scale_y: Option<SharedAnimatedValue>,
pub rotation: Option<SharedAnimatedValue>,
pub rotation_timeline: Option<TimelineRotation>,
pub opacity: Option<SharedAnimatedValue>,
}
impl MotionBindings {
pub fn is_empty(&self) -> bool {
self.translate_x.is_none()
&& self.translate_y.is_none()
&& self.scale.is_none()
&& self.scale_x.is_none()
&& self.scale_y.is_none()
&& self.rotation.is_none()
&& self.rotation_timeline.is_none()
&& self.opacity.is_none()
}
pub fn get_transform(&self) -> Option<Transform> {
let tx = self
.translate_x
.as_ref()
.map(|v| v.lock().unwrap().get())
.unwrap_or(0.0);
let ty = self
.translate_y
.as_ref()
.map(|v| v.lock().unwrap().get())
.unwrap_or(0.0);
if tx.abs() > 0.001 || ty.abs() > 0.001 {
Some(Transform::translate(tx, ty))
} else {
None
}
}
pub fn get_scale(&self) -> Option<(f32, f32)> {
let scale = self.scale.as_ref().map(|v| v.lock().unwrap().get());
let scale_x = self.scale_x.as_ref().map(|v| v.lock().unwrap().get());
let scale_y = self.scale_y.as_ref().map(|v| v.lock().unwrap().get());
if let Some(s) = scale {
Some((s, s))
} else if scale_x.is_some() || scale_y.is_some() {
Some((scale_x.unwrap_or(1.0), scale_y.unwrap_or(1.0)))
} else {
None
}
}
pub fn get_rotation(&self) -> Option<f32> {
if let Some(ref tl_rot) = self.rotation_timeline {
if let Ok(timeline) = tl_rot.timeline.lock() {
return timeline.get(tl_rot.entry_id);
}
}
self.rotation.as_ref().map(|v| v.lock().unwrap().get())
}
pub fn get_opacity(&self) -> Option<f32> {
self.opacity.as_ref().map(|v| v.lock().unwrap().get())
}
}
pub struct Motion {
children: Vec<Box<dyn ElementBuilder>>,
enter: Option<ElementAnimation>,
exit: Option<ElementAnimation>,
stagger_config: Option<StaggerConfig>,
style: Style,
key: InstanceKey,
use_stable_key: bool,
replay: bool,
suspended: bool,
on_ready_callback: Option<Arc<dyn Fn(ElementBounds) + Send + Sync>>,
translate_x: Option<SharedAnimatedValue>,
translate_y: Option<SharedAnimatedValue>,
scale: Option<SharedAnimatedValue>,
scale_x: Option<SharedAnimatedValue>,
scale_y: Option<SharedAnimatedValue>,
rotation: Option<SharedAnimatedValue>,
rotation_timeline: Option<TimelineRotation>,
opacity: Option<SharedAnimatedValue>,
#[deprecated(
since = "0.1.0",
note = "Use query_motion(key).exit() to explicitly trigger motion exit"
)]
is_exiting: bool,
pointer_events_none: bool,
}
fn motion_keyframe_to_properties(kf: &MotionKeyframe) -> blinc_animation::KeyframeProperties {
let mut props = blinc_animation::KeyframeProperties::default();
if let Some(opacity) = kf.opacity {
props = props.with_opacity(opacity);
}
if let Some(scale_x) = kf.scale_x {
props.scale_x = Some(scale_x);
}
if let Some(scale_y) = kf.scale_y {
props.scale_y = Some(scale_y);
}
if let Some(tx) = kf.translate_x {
props.translate_x = Some(tx);
}
if let Some(ty) = kf.translate_y {
props.translate_y = Some(ty);
}
if let Some(rotate) = kf.rotate {
props.rotate = Some(rotate);
}
props
}
#[track_caller]
#[allow(deprecated)]
pub fn motion() -> Motion {
Motion {
children: Vec::new(),
enter: None,
exit: None,
stagger_config: None,
style: Style {
display: Display::Flex,
flex_direction: FlexDirection::Column,
size: taffy::Size {
width: taffy::Dimension::Percent(1.0),
height: taffy::Dimension::Auto,
},
flex_grow: 1.0,
..Style::default()
},
key: InstanceKey::new("motion"),
use_stable_key: true, replay: false,
suspended: false,
on_ready_callback: None,
translate_x: None,
translate_y: None,
scale: None,
scale_x: None,
scale_y: None,
rotation: None,
rotation_timeline: None,
opacity: None,
is_exiting: false,
pointer_events_none: false,
}
}
#[allow(deprecated)]
pub fn motion_derived(parent_key: &str) -> Motion {
Motion {
children: Vec::new(),
enter: None,
exit: None,
stagger_config: None,
style: Style {
display: Display::Flex,
flex_direction: FlexDirection::Column,
size: taffy::Size {
width: taffy::Dimension::Percent(1.0),
height: taffy::Dimension::Auto,
},
flex_grow: 1.0,
..Style::default()
},
key: InstanceKey::explicit(format!("motion:{}", parent_key)),
use_stable_key: true,
replay: false,
suspended: false,
on_ready_callback: None,
translate_x: None,
translate_y: None,
scale: None,
scale_x: None,
scale_y: None,
rotation: None,
rotation_timeline: None,
opacity: None,
is_exiting: false,
pointer_events_none: false,
}
}
impl Motion {
pub fn child(mut self, child: impl ElementBuilder + 'static) -> Self {
self.children = vec![Box::new(child)];
self
}
pub fn children<I, E>(mut self, children: I) -> Self
where
I: IntoIterator<Item = E>,
E: ElementBuilder + 'static,
{
self.children = children
.into_iter()
.map(|c| Box::new(c) as Box<dyn ElementBuilder>)
.collect();
self
}
pub fn enter_animation(mut self, animation: impl Into<ElementAnimation>) -> Self {
self.enter = Some(animation.into());
self
}
pub fn exit_animation(mut self, animation: impl Into<ElementAnimation>) -> Self {
self.exit = Some(animation.into());
self
}
pub fn stagger(mut self, config: StaggerConfig) -> Self {
self.stagger_config = Some(config);
self
}
pub fn animation(self, config: MotionAnimation) -> Self {
use blinc_animation::{Easing, KeyframeProperties};
let mut result = self;
if let Some(ref enter_from) = config.enter_from {
let from_props = motion_keyframe_to_properties(enter_from);
let to_props = KeyframeProperties::default()
.with_opacity(1.0)
.with_scale(1.0)
.with_translate(0.0, 0.0);
let enter = MultiKeyframeAnimation::new(config.enter_duration_ms)
.keyframe(0.0, from_props, Easing::Linear)
.keyframe(1.0, to_props, Easing::EaseOut);
result = result.enter_animation(enter);
}
if let Some(ref exit_to) = config.exit_to {
let from_props = KeyframeProperties::default()
.with_opacity(1.0)
.with_scale(1.0)
.with_translate(0.0, 0.0);
let to_props = motion_keyframe_to_properties(exit_to);
let exit = MultiKeyframeAnimation::new(config.exit_duration_ms)
.keyframe(0.0, from_props, Easing::Linear)
.keyframe(1.0, to_props, Easing::EaseIn);
result = result.exit_animation(exit);
}
result
}
pub fn fade_in(self, duration_ms: u32) -> Self {
self.enter_animation(AnimationPreset::fade_in(duration_ms))
}
pub fn fade_out(self, duration_ms: u32) -> Self {
self.exit_animation(AnimationPreset::fade_out(duration_ms))
}
pub fn scale_in(self, duration_ms: u32) -> Self {
self.enter_animation(AnimationPreset::scale_in(duration_ms))
}
pub fn scale_out(self, duration_ms: u32) -> Self {
self.exit_animation(AnimationPreset::scale_out(duration_ms))
}
pub fn bounce_in(self, duration_ms: u32) -> Self {
self.enter_animation(AnimationPreset::bounce_in(duration_ms))
}
pub fn bounce_out(self, duration_ms: u32) -> Self {
self.exit_animation(AnimationPreset::bounce_out(duration_ms))
}
pub fn slide_in(self, direction: SlideDirection, duration_ms: u32) -> Self {
let distance = 50.0;
let anim = match direction {
SlideDirection::Left => AnimationPreset::slide_in_left(duration_ms, distance),
SlideDirection::Right => AnimationPreset::slide_in_right(duration_ms, distance),
SlideDirection::Top => AnimationPreset::slide_in_top(duration_ms, distance),
SlideDirection::Bottom => AnimationPreset::slide_in_bottom(duration_ms, distance),
};
self.enter_animation(anim)
}
pub fn slide_out(self, direction: SlideDirection, duration_ms: u32) -> Self {
let distance = 50.0;
let anim = match direction {
SlideDirection::Left => AnimationPreset::slide_out_left(duration_ms, distance),
SlideDirection::Right => AnimationPreset::slide_out_right(duration_ms, distance),
SlideDirection::Top => AnimationPreset::slide_out_top(duration_ms, distance),
SlideDirection::Bottom => AnimationPreset::slide_out_bottom(duration_ms, distance),
};
self.exit_animation(anim)
}
pub fn pop_in(self, duration_ms: u32) -> Self {
self.enter_animation(AnimationPreset::pop_in(duration_ms))
}
pub fn from_stylesheet(
self,
stylesheet: &crate::css_parser::Stylesheet,
animation_name: &str,
enter_duration_ms: u32,
exit_duration_ms: u32,
) -> Self {
if let Some(keyframes) = stylesheet.get_keyframes(animation_name) {
let motion_anim = keyframes.to_motion_animation(enter_duration_ms, exit_duration_ms);
self.animation(motion_anim)
} else {
tracing::warn!(
animation_name = animation_name,
"Keyframes not found in stylesheet"
);
self
}
}
pub fn keyframes_from_stylesheet(
self,
stylesheet: &crate::css_parser::Stylesheet,
animation_name: &str,
duration_ms: u32,
easing: blinc_animation::Easing,
) -> Self {
if let Some(keyframes) = stylesheet.get_keyframes(animation_name) {
let animation = keyframes.to_multi_keyframe_animation(duration_ms, easing);
self.enter_animation(animation)
} else {
tracing::warn!(
animation_name = animation_name,
"Keyframes not found in stylesheet"
);
self
}
}
pub fn translate_x(mut self, value: SharedAnimatedValue) -> Self {
self.translate_x = Some(value);
self
}
pub fn translate_y(mut self, value: SharedAnimatedValue) -> Self {
self.translate_y = Some(value);
self
}
pub fn scale(mut self, value: SharedAnimatedValue) -> Self {
self.scale = Some(value);
self
}
pub fn scale_x(mut self, value: SharedAnimatedValue) -> Self {
self.scale_x = Some(value);
self
}
pub fn scale_y(mut self, value: SharedAnimatedValue) -> Self {
self.scale_y = Some(value);
self
}
pub fn rotate(mut self, value: SharedAnimatedValue) -> Self {
self.rotation = Some(value);
self
}
pub fn rotate_timeline(
mut self,
timeline: blinc_animation::SharedAnimatedTimeline,
entry_id: blinc_animation::TimelineEntryId,
) -> Self {
self.rotation_timeline = Some(TimelineRotation { timeline, entry_id });
self
}
pub fn opacity(mut self, value: SharedAnimatedValue) -> Self {
self.opacity = Some(value);
self
}
pub fn has_animated_bindings(&self) -> bool {
self.translate_x.is_some()
|| self.translate_y.is_some()
|| self.scale.is_some()
|| self.scale_x.is_some()
|| self.scale_y.is_some()
|| self.rotation.is_some()
|| self.rotation_timeline.is_some()
|| self.opacity.is_some()
}
pub fn get_motion_bindings(&self) -> Option<MotionBindings> {
if !self.has_animated_bindings() {
return None;
}
Some(MotionBindings {
translate_x: self.translate_x.clone(),
translate_y: self.translate_y.clone(),
scale: self.scale.clone(),
scale_x: self.scale_x.clone(),
scale_y: self.scale_y.clone(),
rotation: self.rotation.clone(),
rotation_timeline: self.rotation_timeline.clone(),
opacity: self.opacity.clone(),
})
}
pub fn gap(mut self, gap: f32) -> Self {
self.style.gap = taffy::Size {
width: taffy::LengthPercentage::Length(gap),
height: taffy::LengthPercentage::Length(gap),
};
self
}
pub fn flex_row(mut self) -> Self {
self.style.flex_direction = FlexDirection::Row;
self
}
pub fn flex_col(mut self) -> Self {
self.style.flex_direction = FlexDirection::Column;
self
}
pub fn items_center(mut self) -> Self {
self.style.align_items = Some(taffy::AlignItems::Center);
self
}
pub fn items_start(mut self) -> Self {
self.style.align_items = Some(taffy::AlignItems::FlexStart);
self
}
pub fn justify_center(mut self) -> Self {
self.style.justify_content = Some(taffy::JustifyContent::Center);
self
}
pub fn justify_between(mut self) -> Self {
self.style.justify_content = Some(taffy::JustifyContent::SpaceBetween);
self
}
pub fn w_full(mut self) -> Self {
self.style.size.width = taffy::Dimension::Percent(1.0);
self
}
pub fn h_full(mut self) -> Self {
self.style.size.height = taffy::Dimension::Percent(1.0);
self
}
pub fn flex_grow(mut self) -> Self {
self.style.flex_grow = 1.0;
self
}
pub fn get_enter_animation(&self) -> Option<&ElementAnimation> {
self.enter.as_ref()
}
pub fn get_exit_animation(&self) -> Option<&ElementAnimation> {
self.exit.as_ref()
}
pub fn get_stagger_config(&self) -> Option<&StaggerConfig> {
self.stagger_config.as_ref()
}
pub fn id(mut self, id: impl std::fmt::Display) -> Self {
let new_key = format!("{}:{}", self.key.get(), id);
self.key = InstanceKey::explicit(new_key);
self
}
pub fn get_stable_key(&self) -> &str {
self.key.get()
}
pub fn transient(mut self) -> Self {
self.use_stable_key = false;
self
}
pub fn replay(mut self) -> Self {
self.replay = true;
self
}
pub fn should_replay(&self) -> bool {
self.replay
}
pub fn suspended(mut self) -> Self {
self.suspended = true;
self
}
pub fn is_suspended(&self) -> bool {
self.suspended
}
pub fn pointer_events_none(mut self) -> Self {
self.pointer_events_none = true;
self
}
pub fn on_ready<F>(mut self, callback: F) -> Self
where
F: Fn(ElementBounds) + Send + Sync + 'static,
{
self.on_ready_callback = Some(Arc::new(callback));
self
}
pub fn motion_animation_for_child(&self, child_index: usize) -> Option<MotionAnimation> {
let total_children = self.children.len();
if total_children == 0 {
return None;
}
let delay_ms = if let Some(ref stagger) = self.stagger_config {
stagger.delay_for_index(child_index, total_children)
} else {
0
};
let enter_anim = if let Some(ref stagger) = self.stagger_config {
Some(&stagger.animation.animation)
} else {
self.enter.as_ref().map(|e| &e.animation)
};
let exit_anim = self.exit.as_ref().map(|e| &e.animation);
if let Some(enter) = enter_anim {
let enter_from = enter
.first_keyframe()
.map(|kf| MotionKeyframe::from_keyframe_properties(&kf.properties));
let mut motion = MotionAnimation {
enter_from,
enter_duration_ms: enter.duration_ms(),
enter_delay_ms: delay_ms,
exit_to: None,
exit_duration_ms: 0,
};
if let Some(exit) = exit_anim {
motion.exit_to = exit
.last_keyframe()
.map(|kf| MotionKeyframe::from_keyframe_properties(&kf.properties));
motion.exit_duration_ms = exit.duration_ms();
}
Some(motion)
} else if let Some(exit) = exit_anim {
let exit_to = exit
.last_keyframe()
.map(|kf| MotionKeyframe::from_keyframe_properties(&kf.properties));
Some(MotionAnimation {
enter_from: None,
enter_duration_ms: 0,
enter_delay_ms: delay_ms,
exit_to,
exit_duration_ms: exit.duration_ms(),
})
} else {
None
}
}
pub fn child_count(&self) -> usize {
self.children.len()
}
}
pub mod motion_events {
pub const MOUNT: u32 = 30001;
pub const ENTER_COMPLETE: u32 = 30002;
pub const EXIT: u32 = 30003;
pub const EXIT_COMPLETE: u32 = 30004;
pub const KEY_CHANGED: u32 = 30005;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum MotionPresenceState {
#[default]
Empty,
Entering,
Visible,
Exiting,
}
impl MotionPresenceState {
pub fn is_mounted(&self) -> bool {
!matches!(self, MotionPresenceState::Empty)
}
pub fn is_animating(&self) -> bool {
matches!(
self,
MotionPresenceState::Entering | MotionPresenceState::Exiting
)
}
pub fn is_exiting(&self) -> bool {
matches!(self, MotionPresenceState::Exiting)
}
pub fn is_visible(&self) -> bool {
matches!(self, MotionPresenceState::Visible)
}
}
impl crate::stateful::StateTransitions for MotionPresenceState {
fn on_event(&self, event: u32) -> Option<Self> {
use motion_events::*;
use MotionPresenceState::*;
match (self, event) {
(Empty, MOUNT) => Some(Entering),
(Entering, ENTER_COMPLETE) => Some(Visible),
(Visible, EXIT) | (Visible, KEY_CHANGED) => Some(Exiting),
(Exiting, EXIT_COMPLETE) => Some(Empty),
(Entering, KEY_CHANGED) => Some(Entering),
_ => None,
}
}
}
#[derive(Clone)]
pub struct ExitingChild {
pub key: String,
pub motion_key: String,
}
#[derive(Clone, Default)]
pub struct MotionPresenceStore {
pub current_key: Option<String>,
pub exiting: Vec<ExitingChild>,
pub state: MotionPresenceState,
pub pending_enter: bool,
}
impl MotionPresenceStore {
pub fn is_key_exiting(&self, key: &str) -> bool {
self.exiting.iter().any(|e| e.key == key)
}
pub fn remove_exiting(&mut self, key: &str) {
self.exiting.retain(|e| e.key != key);
}
pub fn add_exiting(&mut self, key: String, motion_key: String) {
if !self.is_key_exiting(&key) {
self.exiting.push(ExitingChild { key, motion_key });
}
}
pub fn has_exiting(&self) -> bool {
!self.exiting.is_empty()
}
}
pub fn motion_presence_store() -> &'static blinc_core::Store<MotionPresenceStore> {
blinc_core::create_store::<MotionPresenceStore>("motion_presence")
}
pub fn query_presence_state(store_key: &str) -> MotionPresenceStore {
motion_presence_store().get(store_key)
}
pub fn update_presence_state<F>(store_key: &str, f: F)
where
F: FnOnce(&mut MotionPresenceStore),
{
motion_presence_store().update(store_key, f);
}
pub fn check_and_clear_exiting(store_key: &str) -> bool {
use crate::selector::query_motion;
let state = motion_presence_store().get(store_key);
let mut cleared_any = false;
for child in &state.exiting {
let motion = query_motion(&child.motion_key);
if !motion.is_animating() {
tracing::debug!(
"MotionPresence '{}': exit complete for '{}' (motion state: {:?})",
store_key,
child.key,
motion.state()
);
cleared_any = true;
}
}
if cleared_any {
motion_presence_store().update(store_key, |s| {
s.exiting.retain(|child| {
let motion = query_motion(&child.motion_key);
motion.is_animating()
});
});
}
cleared_any
}
pub fn start_exit_for_key(store_key: &str, child_key: &str, motion_key: &str) {
use crate::selector::query_motion;
tracing::debug!(
"MotionPresence '{}': starting exit for '{}' (motion: '{}')",
store_key,
child_key,
motion_key
);
query_motion(motion_key).exit();
motion_presence_store().update(store_key, |s| {
s.add_exiting(child_key.to_string(), motion_key.to_string());
s.state = MotionPresenceState::Exiting;
});
}
pub fn check_ready_for_enter(store_key: &str) -> bool {
let state = motion_presence_store().get(store_key);
if state.pending_enter && state.exiting.is_empty() {
tracing::debug!(
"MotionPresence '{}': all exits complete, ready for enter",
store_key
);
motion_presence_store().update(store_key, |s| {
s.pending_enter = false;
s.state = MotionPresenceState::Entering;
});
return true;
}
false
}
#[cfg(test)]
mod tests_motion_presence {
use super::*;
#[test]
fn test_motion_presence_state_transitions() {
use crate::stateful::StateTransitions;
let state = MotionPresenceState::Empty;
assert_eq!(
state.on_event(motion_events::MOUNT),
Some(MotionPresenceState::Entering)
);
let state = MotionPresenceState::Entering;
assert_eq!(
state.on_event(motion_events::ENTER_COMPLETE),
Some(MotionPresenceState::Visible)
);
let state = MotionPresenceState::Visible;
assert_eq!(
state.on_event(motion_events::EXIT),
Some(MotionPresenceState::Exiting)
);
let state = MotionPresenceState::Exiting;
assert_eq!(
state.on_event(motion_events::EXIT_COMPLETE),
Some(MotionPresenceState::Empty)
);
}
#[test]
fn test_motion_presence_state_helpers() {
assert!(!MotionPresenceState::Empty.is_mounted());
assert!(MotionPresenceState::Entering.is_mounted());
assert!(MotionPresenceState::Visible.is_mounted());
assert!(MotionPresenceState::Exiting.is_mounted());
assert!(MotionPresenceState::Entering.is_animating());
assert!(!MotionPresenceState::Visible.is_animating());
assert!(MotionPresenceState::Exiting.is_animating());
assert!(MotionPresenceState::Exiting.is_exiting());
assert!(!MotionPresenceState::Visible.is_exiting());
}
}
impl ElementBuilder for Motion {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
if self.use_stable_key {
push_motion_context(self.key.get());
}
let node = tree.create_node(self.style.clone());
for child in &self.children {
let child_node = child.build(tree);
tree.add_child(node, child_node);
}
if self.use_stable_key {
pop_motion_context();
}
node
}
fn render_props(&self) -> RenderProps {
RenderProps {
pointer_events_none: self.pointer_events_none,
..RenderProps::default()
}
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
&self.children
}
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Motion
}
fn motion_animation_for_child(&self, child_index: usize) -> Option<MotionAnimation> {
self.motion_animation_for_child(child_index)
}
fn motion_bindings(&self) -> Option<MotionBindings> {
self.get_motion_bindings()
}
fn motion_stable_id(&self) -> Option<&str> {
if self.use_stable_key {
Some(self.key.get())
} else {
None
}
}
fn motion_should_replay(&self) -> bool {
self.replay
}
fn motion_is_suspended(&self) -> bool {
self.suspended
}
#[allow(deprecated)]
fn motion_is_exiting(&self) -> bool {
self.is_exiting
}
fn layout_style(&self) -> Option<&taffy::Style> {
Some(&self.style)
}
fn motion_on_ready_callback(&self) -> Option<Arc<dyn Fn(ElementBounds) + Send + Sync>> {
self.on_ready_callback.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stagger_delay_forward() {
let config = StaggerConfig::new(50, AnimationPreset::fade_in(300));
assert_eq!(config.delay_for_index(0, 5), 0);
assert_eq!(config.delay_for_index(1, 5), 50);
assert_eq!(config.delay_for_index(2, 5), 100);
assert_eq!(config.delay_for_index(4, 5), 200);
}
#[test]
fn test_stagger_delay_reverse() {
let config = StaggerConfig::new(50, AnimationPreset::fade_in(300)).reverse();
assert_eq!(config.delay_for_index(0, 5), 200);
assert_eq!(config.delay_for_index(1, 5), 150);
assert_eq!(config.delay_for_index(4, 5), 0);
}
#[test]
fn test_stagger_delay_from_center() {
let config = StaggerConfig::new(50, AnimationPreset::fade_in(300)).from_center();
assert_eq!(config.delay_for_index(0, 5), 100); assert_eq!(config.delay_for_index(1, 5), 50); assert_eq!(config.delay_for_index(2, 5), 0); assert_eq!(config.delay_for_index(3, 5), 50); assert_eq!(config.delay_for_index(4, 5), 100); }
#[test]
fn test_stagger_delay_with_limit() {
let config = StaggerConfig::new(50, AnimationPreset::fade_in(300)).limit(3);
assert_eq!(config.delay_for_index(0, 10), 0);
assert_eq!(config.delay_for_index(3, 10), 150); assert_eq!(config.delay_for_index(5, 10), 150); assert_eq!(config.delay_for_index(9, 10), 150); }
}