use crate::ui::{Composite, Spacer, Widget};
use crate::CurrentTime;
use fission_ir::op::Color;
use fission_ir::{CompositeScalar, WidgetId};
use fission_layout::{LayoutPoint, LayoutSnapshot};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::ops::Add;
use std::sync::Arc;
pub trait IntoMotionId {
fn into_motion_id(self) -> WidgetId;
}
impl IntoMotionId for WidgetId {
fn into_motion_id(self) -> WidgetId {
self
}
}
impl IntoMotionId for &'static str {
fn into_motion_id(self) -> WidgetId {
WidgetId::explicit(self)
}
}
impl IntoMotionId for String {
fn into_motion_id(self) -> WidgetId {
WidgetId::explicit(&self)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MotionPhase {
Layout,
Composite,
Paint,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MotionPropertyId {
Opacity,
TranslateX,
TranslateY,
Scale,
Rotation,
Width,
Height,
LayoutX,
LayoutY,
LayoutWidth,
LayoutHeight,
IntrinsicWidth,
IntrinsicHeight,
CornerRadius,
BackgroundColor,
BorderColor,
TextColor,
Custom(Arc<str>),
}
impl MotionPropertyId {
pub fn opacity() -> Self {
Self::Opacity
}
pub fn translate_x() -> Self {
Self::TranslateX
}
pub fn translate_y() -> Self {
Self::TranslateY
}
pub fn scale() -> Self {
Self::Scale
}
pub fn rotation() -> Self {
Self::Rotation
}
pub fn custom(name: impl Into<String>) -> Self {
Self::Custom(Arc::from(name.into()))
}
pub fn default_value(&self) -> MotionValue {
match self {
Self::Opacity | Self::Scale => MotionValue::Scalar(1.0),
Self::BackgroundColor | Self::BorderColor | Self::TextColor => {
MotionValue::Color(Color {
r: 0,
g: 0,
b: 0,
a: 0,
})
}
Self::TranslateX
| Self::TranslateY
| Self::Width
| Self::Height
| Self::LayoutX
| Self::LayoutY
| Self::LayoutWidth
| Self::LayoutHeight
| Self::IntrinsicWidth
| Self::IntrinsicHeight
| Self::CornerRadius => MotionValue::Px(0.0),
Self::Rotation => MotionValue::Deg(0.0),
Self::Custom(_) => MotionValue::Scalar(0.0),
}
}
pub fn default_scalar_value(&self) -> f32 {
self.default_value().as_scalar_like().unwrap_or(0.0)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MotionValue {
Bool(bool),
Scalar(f32),
Px(f32),
Deg(f32),
Color(Color),
}
impl MotionValue {
pub fn as_scalar_like(&self) -> Option<f32> {
match self {
Self::Scalar(v) | Self::Px(v) | Self::Deg(v) => Some(*v),
Self::Bool(_) | Self::Color(_) => None,
}
}
fn interpolate(&self, to: &Self, t: f32) -> Self {
let t = t.clamp(0.0, 1.0);
match (self, to) {
(Self::Scalar(a), Self::Scalar(b)) => Self::Scalar(lerp(*a, *b, t)),
(Self::Px(a), Self::Px(b)) => Self::Px(lerp(*a, *b, t)),
(Self::Deg(a), Self::Deg(b)) => Self::Deg(lerp(*a, *b, t)),
(Self::Color(a), Self::Color(b)) => Self::Color(Color {
r: lerp(a.r as f32, b.r as f32, t).round().clamp(0.0, 255.0) as u8,
g: lerp(a.g as f32, b.g as f32, t).round().clamp(0.0, 255.0) as u8,
b: lerp(a.b as f32, b.b as f32, t).round().clamp(0.0, 255.0) as u8,
a: lerp(a.a as f32, b.a as f32, t).round().clamp(0.0, 255.0) as u8,
}),
_ => {
if t >= 1.0 {
to.clone()
} else {
self.clone()
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MotionPredicate {
Hovered(WidgetId),
Pressed(WidgetId),
Focused(WidgetId),
Disabled(WidgetId),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MotionExpr {
Value(MotionValue),
IntrinsicWidth,
IntrinsicHeight,
LayoutX(WidgetId),
LayoutY(WidgetId),
LayoutWidth(WidgetId),
LayoutHeight(WidgetId),
PointerLocalX,
PointerLocalY,
If {
predicate: MotionPredicate,
then_expr: Box<MotionExpr>,
else_expr: Box<MotionExpr>,
},
Add(Box<MotionExpr>, Box<MotionExpr>),
Sub(Box<MotionExpr>, Box<MotionExpr>),
Mul(Box<MotionExpr>, Box<MotionExpr>),
Div(Box<MotionExpr>, Box<MotionExpr>),
Neg(Box<MotionExpr>),
Abs(Box<MotionExpr>),
Min(Box<MotionExpr>, Box<MotionExpr>),
Max(Box<MotionExpr>, Box<MotionExpr>),
Clamp {
value: Box<MotionExpr>,
min: Box<MotionExpr>,
max: Box<MotionExpr>,
},
Lerp {
from: Box<MotionExpr>,
to: Box<MotionExpr>,
t: Box<MotionExpr>,
},
MapRange {
value: Box<MotionExpr>,
from_start: f32,
from_end: f32,
to_start: f32,
to_end: f32,
clamp: bool,
},
}
impl MotionExpr {
pub fn eval(&self, input: &MotionEvalInput<'_>) -> MotionValue {
match self {
Self::Value(value) => value.clone(),
Self::IntrinsicWidth => input
.self_rect
.map(|rect| MotionValue::Px(rect.width()))
.unwrap_or(MotionValue::Px(0.0)),
Self::IntrinsicHeight => input
.self_rect
.map(|rect| MotionValue::Px(rect.height()))
.unwrap_or(MotionValue::Px(0.0)),
Self::LayoutX(id) => input
.layout
.and_then(|layout| layout.get_node_rect(*id))
.map(|rect| MotionValue::Px(rect.x()))
.unwrap_or(MotionValue::Px(0.0)),
Self::LayoutY(id) => input
.layout
.and_then(|layout| layout.get_node_rect(*id))
.map(|rect| MotionValue::Px(rect.y()))
.unwrap_or(MotionValue::Px(0.0)),
Self::LayoutWidth(id) => input
.layout
.and_then(|layout| layout.get_node_rect(*id))
.map(|rect| MotionValue::Px(rect.width()))
.unwrap_or(MotionValue::Px(0.0)),
Self::LayoutHeight(id) => input
.layout
.and_then(|layout| layout.get_node_rect(*id))
.map(|rect| MotionValue::Px(rect.height()))
.unwrap_or(MotionValue::Px(0.0)),
Self::PointerLocalX => input
.pointer_local
.map(|point| MotionValue::Px(point.x))
.unwrap_or(MotionValue::Px(0.0)),
Self::PointerLocalY => input
.pointer_local
.map(|point| MotionValue::Px(point.y))
.unwrap_or(MotionValue::Px(0.0)),
Self::If {
predicate,
then_expr,
else_expr,
} => {
if input.predicate(predicate) {
then_expr.eval(input)
} else {
else_expr.eval(input)
}
}
Self::Add(a, b) => numeric_binary(a, b, input, |a, b| a + b),
Self::Sub(a, b) => numeric_binary(a, b, input, |a, b| a - b),
Self::Mul(a, b) => numeric_binary(a, b, input, |a, b| a * b),
Self::Div(a, b) => numeric_binary(a, b, input, |a, b| if b == 0.0 { a } else { a / b }),
Self::Neg(v) => numeric_unary(v, input, |v| -v),
Self::Abs(v) => numeric_unary(v, input, f32::abs),
Self::Min(a, b) => numeric_binary(a, b, input, f32::min),
Self::Max(a, b) => numeric_binary(a, b, input, f32::max),
Self::Clamp { value, min, max } => {
let value = value.eval(input);
let min = min.eval(input).as_scalar_like().unwrap_or(0.0);
let max = max.eval(input).as_scalar_like().unwrap_or(min);
map_numeric(value, |v| v.clamp(min, max))
}
Self::Lerp { from, to, t } => {
let t = t.eval(input).as_scalar_like().unwrap_or(0.0);
from.eval(input).interpolate(&to.eval(input), t)
}
Self::MapRange {
value,
from_start,
from_end,
to_start,
to_end,
clamp,
} => {
let raw = value.eval(input).as_scalar_like().unwrap_or(0.0);
let denom = from_end - from_start;
let mut t = if denom.abs() <= f32::EPSILON {
0.0
} else {
(raw - from_start) / denom
};
if *clamp {
t = t.clamp(0.0, 1.0);
}
MotionValue::Scalar(lerp(*to_start, *to_end, t))
}
}
}
}
#[derive(Clone, Debug)]
pub struct MotionEvalInput<'a> {
pub runtime: &'a crate::RuntimeState,
pub layout: Option<&'a LayoutSnapshot>,
pub self_id: WidgetId,
pub self_rect: Option<fission_layout::LayoutRect>,
pub pointer_local: Option<LayoutPoint>,
}
impl<'a> MotionEvalInput<'a> {
fn predicate(&self, predicate: &MotionPredicate) -> bool {
match predicate {
MotionPredicate::Hovered(id) => self.runtime.interaction.is_hovered(*id),
MotionPredicate::Pressed(id) => self.runtime.interaction.is_pressed(*id),
MotionPredicate::Focused(id) => self.runtime.interaction.is_focused(*id),
MotionPredicate::Disabled(_) => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MotionStartValue {
Current,
Explicit(MotionExpr),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MotionEasing {
Linear,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(f32, f32, f32, f32),
}
impl Default for MotionEasing {
fn default() -> Self {
Self::EaseInOut
}
}
impl MotionEasing {
pub fn apply(&self, t: f32) -> f32 {
match self {
Self::Linear => t,
Self::EaseIn => t * t,
Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Self::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
Self::CubicBezier(_x1, y1, _x2, y2) => {
let t2 = t * t;
let t3 = t2 * t;
3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
}
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MotionTransition {
Instant,
Tween {
duration_ms: u64,
delay_ms: u64,
easing: MotionEasing,
repeat: bool,
frame_interval_ms: Option<u64>,
},
Spring {
stiffness: f32,
damping: f32,
mass: f32,
epsilon: f32,
delay_ms: u64,
},
}
impl Default for MotionTransition {
fn default() -> Self {
Self::Tween {
duration_ms: 160,
delay_ms: 0,
easing: MotionEasing::EaseInOut,
repeat: false,
frame_interval_ms: None,
}
}
}
impl MotionTransition {
pub fn tween(duration_ms: u64, easing: MotionEasing) -> Self {
Self::Tween {
duration_ms,
delay_ms: 0,
easing,
repeat: false,
frame_interval_ms: None,
}
}
pub fn spring(stiffness: f32, damping: f32) -> Self {
Self::Spring {
stiffness,
damping,
mass: 1.0,
epsilon: 0.001,
delay_ms: 0,
}
}
pub fn delay_ms(mut self, delay_ms: u64) -> Self {
match &mut self {
Self::Instant => {}
Self::Tween {
delay_ms: delay, ..
}
| Self::Spring {
delay_ms: delay, ..
} => {
*delay = delay_ms;
}
}
self
}
pub fn repeat(mut self, repeat: bool) -> Self {
if let Self::Tween {
repeat: current, ..
} = &mut self
{
*current = repeat;
}
self
}
pub fn frame_interval_ms(mut self, frame_interval_ms: Option<u64>) -> Self {
if let Self::Tween {
frame_interval_ms: current,
..
} = &mut self
{
*current = frame_interval_ms;
}
self
}
fn duration_ms(&self) -> u64 {
match self {
Self::Instant => 0,
Self::Tween { duration_ms, .. } => *duration_ms,
Self::Spring { .. } => 260,
}
}
fn delay_value_ms(&self) -> u64 {
match self {
Self::Instant => 0,
Self::Tween { delay_ms, .. } | Self::Spring { delay_ms, .. } => *delay_ms,
}
}
fn repeat_enabled(&self) -> bool {
matches!(self, Self::Tween { repeat: true, .. })
}
fn easing(&self) -> MotionEasing {
match self {
Self::Instant | Self::Spring { .. } => MotionEasing::EaseOut,
Self::Tween { easing, .. } => easing.clone(),
}
}
fn frame_interval_value_ms(&self) -> Option<u64> {
match self {
Self::Tween {
frame_interval_ms, ..
} => frame_interval_ms.filter(|ms| *ms > 0),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MotionTrack {
pub property: MotionPropertyId,
pub phase: MotionPhase,
pub from: MotionStartValue,
pub to: MotionExpr,
pub transition: MotionTransition,
}
impl MotionTrack {
pub fn composite(property: MotionPropertyId, from: MotionStartValue, to: MotionExpr) -> Self {
Self {
property,
phase: MotionPhase::Composite,
from,
to,
transition: MotionTransition::default(),
}
}
pub fn transition(mut self, transition: MotionTransition) -> Self {
self.transition = transition;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PresencePhase {
Hidden,
Entering,
Present,
Exiting,
}
impl Default for PresencePhase {
fn default() -> Self {
Self::Hidden
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RipplePlacement {
BehindChild,
AboveChild,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RippleFx {
pub color: Color,
pub opacity: f32,
pub scale: f32,
pub transition: MotionTransition,
pub max_instances: usize,
pub placement: RipplePlacement,
}
impl Default for RippleFx {
fn default() -> Self {
Self {
color: Color {
r: 255,
g: 255,
b: 255,
a: 64,
},
opacity: 0.35,
scale: 10.0,
transition: MotionTransition::tween(600, MotionEasing::EaseOut),
max_instances: 8,
placement: RipplePlacement::BehindChild,
}
}
}
impl RippleFx {
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
pub fn duration(mut self, duration_ms: u64) -> Self {
if let MotionTransition::Tween {
duration_ms: current,
..
} = &mut self.transition
{
*current = duration_ms;
}
self
}
pub fn ease(mut self, easing: MotionEasing) -> Self {
if let MotionTransition::Tween {
easing: current, ..
} = &mut self.transition
{
*current = easing;
}
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MotionDeclaration {
pub id: WidgetId,
pub kind: MotionDeclarationKind,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MotionDeclarationKind {
Tracks {
tracks: Vec<MotionTrack>,
},
Presence {
visible: bool,
keep_rendered: bool,
enter: Vec<MotionTrack>,
exit: Vec<MotionTrack>,
inert_while_exiting: bool,
},
RippleLayer(RippleFx),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Motion {
pub id: WidgetId,
pub tracks: Vec<MotionTrack>,
pub child: Widget,
pub clip_to_bounds: bool,
pub repaint_boundary: bool,
}
impl Default for Motion {
fn default() -> Self {
Self {
id: WidgetId::explicit("motion"),
tracks: Vec::new(),
child: Spacer::default().into(),
clip_to_bounds: false,
repaint_boundary: true,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Presence {
pub id: WidgetId,
pub visible: bool,
pub keep_rendered: bool,
pub enter: Vec<MotionTrack>,
pub exit: Vec<MotionTrack>,
pub child: Widget,
pub clip_to_bounds: bool,
pub repaint_boundary: bool,
pub inert_while_exiting: bool,
}
impl Default for Presence {
fn default() -> Self {
Self {
id: WidgetId::explicit("presence"),
visible: true,
keep_rendered: false,
enter: Vec::new(),
exit: Vec::new(),
child: Spacer::default().into(),
clip_to_bounds: false,
repaint_boundary: true,
inert_while_exiting: true,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RippleLayer {
pub id: WidgetId,
pub effect: RippleFx,
pub child: Widget,
}
impl Default for RippleLayer {
fn default() -> Self {
Self {
id: WidgetId::explicit("ripple_layer"),
effect: RippleFx::default(),
child: Spacer::default().into(),
}
}
}
impl From<Motion> for Widget {
fn from(component: Motion) -> Self {
crate::build::try_register_motion(MotionDeclaration {
id: component.id,
kind: MotionDeclarationKind::Tracks {
tracks: component.tracks.clone(),
},
});
let style = composite_style_for_tracks(component.id, component.child, &component.tracks)
.clip_to_bounds(component.clip_to_bounds)
.repaint_boundary(component.repaint_boundary);
style.into()
}
}
impl From<Presence> for Widget {
fn from(component: Presence) -> Self {
crate::build::try_register_motion(MotionDeclaration {
id: component.id,
kind: MotionDeclarationKind::Presence {
visible: component.visible,
keep_rendered: component.keep_rendered,
enter: component.enter.clone(),
exit: component.exit.clone(),
inert_while_exiting: component.inert_while_exiting,
},
});
let phase = crate::build::try_current_runtime_state()
.and_then(|runtime| runtime.motion.presence.get(&component.id).copied())
.unwrap_or(if component.visible {
PresencePhase::Present
} else {
PresencePhase::Hidden
});
let should_render = component.visible
|| component.keep_rendered
|| matches!(
phase,
PresencePhase::Entering | PresencePhase::Present | PresencePhase::Exiting
);
if !should_render {
return Spacer::default().into();
}
let tracks = if component.visible {
&component.enter
} else {
&component.exit
};
composite_style_for_tracks(component.id, component.child, tracks)
.clip_to_bounds(component.clip_to_bounds)
.repaint_boundary(component.repaint_boundary)
.into()
}
}
impl From<RippleLayer> for Widget {
fn from(component: RippleLayer) -> Self {
crate::build::try_register_motion(MotionDeclaration {
id: component.id,
kind: MotionDeclarationKind::RippleLayer(component.effect),
});
Composite {
id: Some(component.id),
child: component.child,
..Default::default()
}
.into()
}
}
fn composite_style_for_tracks(id: WidgetId, child: Widget, tracks: &[MotionTrack]) -> Composite {
let mut composite = Composite {
id: Some(id),
child,
..Default::default()
};
for track in tracks {
if track.phase != MotionPhase::Composite {
continue;
}
match track.property {
MotionPropertyId::Opacity => {
composite.style.opacity = Some(CompositeScalar::new(1.0).motion(id));
}
MotionPropertyId::TranslateX => {
composite.style.translate_x = Some(CompositeScalar::new(0.0).motion(id));
}
MotionPropertyId::TranslateY => {
composite.style.translate_y = Some(CompositeScalar::new(0.0).motion(id));
}
MotionPropertyId::Scale => {
composite.style.scale = Some(CompositeScalar::new(1.0).motion(id));
}
MotionPropertyId::Rotation => {
composite.style.rotation = Some(CompositeScalar::new(0.0).motion(id));
}
_ => {}
}
}
composite
}
#[derive(Clone, Debug, Default)]
pub struct MotionStateMap {
pub values: HashMap<(WidgetId, MotionPropertyId), MotionValue>,
pub active: HashMap<(WidgetId, MotionPropertyId), ActiveMotion>,
pub presence: HashMap<WidgetId, PresencePhase>,
pub ripples: HashMap<WidgetId, Vec<SpawnedRipple>>,
}
impl MotionStateMap {
pub fn scalar_value(&self, widget_id: WidgetId, property: MotionPropertyId) -> f32 {
self.values
.get(&(widget_id, property.clone()))
.and_then(MotionValue::as_scalar_like)
.unwrap_or_else(|| property.default_scalar_value())
}
}
#[derive(Clone, Debug)]
pub struct ActiveMotion {
pub target: WidgetId,
pub property: MotionPropertyId,
pub start_value: MotionValue,
pub end_value: MotionValue,
pub start_time: u64,
pub duration: u64,
pub repeat: bool,
pub frame_interval_ms: Option<u64>,
pub easing: MotionEasing,
}
#[derive(Clone, Debug)]
pub struct SpawnedRipple {
pub id: WidgetId,
pub parent: WidgetId,
pub sequence: u64,
pub origin_x: f32,
pub origin_y: f32,
pub birth_ms: u64,
pub duration_ms: u64,
}
#[derive(Default)]
pub struct MotionSyncResult {
pub changed: Vec<(WidgetId, MotionPropertyId)>,
}
pub fn sync_motion_declarations(
state: &mut MotionStateMap,
declarations: &[MotionDeclaration],
runtime: &crate::RuntimeState,
layout: Option<&LayoutSnapshot>,
now: CurrentTime,
) -> MotionSyncResult {
let mut result = MotionSyncResult::default();
let mut requested = HashSet::new();
for declaration in declarations {
match &declaration.kind {
MotionDeclarationKind::Tracks { tracks } => {
sync_tracks(
state,
declaration.id,
tracks,
runtime,
layout,
now,
&mut requested,
&mut result,
);
}
MotionDeclarationKind::Presence {
visible,
keep_rendered: _,
enter,
exit,
inert_while_exiting: _,
} => {
let phase = state
.presence
.get(&declaration.id)
.copied()
.unwrap_or(PresencePhase::Hidden);
let next_phase = match (phase, *visible) {
(PresencePhase::Hidden, true) => PresencePhase::Entering,
(PresencePhase::Exiting, true) => PresencePhase::Entering,
(PresencePhase::Entering, true) => PresencePhase::Entering,
(PresencePhase::Present, true) => PresencePhase::Present,
(PresencePhase::Hidden, false) => PresencePhase::Hidden,
(PresencePhase::Entering, false)
| (PresencePhase::Present, false)
| (PresencePhase::Exiting, false) => PresencePhase::Exiting,
};
state.presence.insert(declaration.id, next_phase);
let tracks = if *visible { enter } else { exit };
if tracks.is_empty() {
match next_phase {
PresencePhase::Entering => {
state
.presence
.insert(declaration.id, PresencePhase::Present);
}
PresencePhase::Exiting => {
state.presence.insert(declaration.id, PresencePhase::Hidden);
}
PresencePhase::Hidden | PresencePhase::Present => {}
}
continue;
}
if !*visible && phase == PresencePhase::Hidden {
continue;
}
sync_tracks(
state,
declaration.id,
tracks,
runtime,
layout,
now,
&mut requested,
&mut result,
);
}
MotionDeclarationKind::RippleLayer(_) => {}
}
}
state.active.retain(|key, _| requested.contains(key));
state.values.retain(|key, _| requested.contains(key));
result
}
pub fn tick_motion(
state: &mut MotionStateMap,
current_time: CurrentTime,
) -> Vec<(WidgetId, MotionPropertyId)> {
let mut changed = Vec::new();
let mut finished = Vec::new();
let mut finished_presence = Vec::new();
for ((target, property), motion) in state.active.iter_mut() {
let elapsed = current_time.saturating_sub(motion.start_time);
let mut progress = if motion.duration == 0 {
1.0
} else {
elapsed as f32 / motion.duration as f32
};
if motion.repeat && progress >= 1.0 {
progress %= 1.0;
} else {
progress = progress.clamp(0.0, 1.0);
}
if !motion.repeat && (elapsed >= motion.duration || motion.duration == 0) {
finished.push((*target, property.clone()));
}
let eased = motion.easing.apply(progress);
let value = motion.start_value.interpolate(&motion.end_value, eased);
if state.values.get(&(*target, property.clone())) != Some(&value) {
state.values.insert((*target, property.clone()), value);
changed.push((*target, property.clone()));
}
}
for key in finished {
state.active.remove(&key);
if state
.presence
.get(&key.0)
.is_some_and(|phase| *phase == PresencePhase::Entering)
{
finished_presence.push((key.0, PresencePhase::Present));
} else if state
.presence
.get(&key.0)
.is_some_and(|phase| *phase == PresencePhase::Exiting)
{
finished_presence.push((key.0, PresencePhase::Hidden));
}
}
for (id, phase) in finished_presence {
state.presence.insert(id, phase);
}
changed
}
fn sync_tracks(
state: &mut MotionStateMap,
id: WidgetId,
tracks: &[MotionTrack],
runtime: &crate::RuntimeState,
layout: Option<&LayoutSnapshot>,
now: CurrentTime,
requested: &mut HashSet<(WidgetId, MotionPropertyId)>,
result: &mut MotionSyncResult,
) {
let self_rect = layout.and_then(|layout| layout.get_node_rect(id));
let input = MotionEvalInput {
runtime,
layout,
self_id: id,
self_rect,
pointer_local: None,
};
for track in tracks {
let key = (id, track.property.clone());
requested.insert(key.clone());
let target_value = track.to.eval(&input);
if let Some(active) = state.active.get(&key) {
if active.end_value == target_value
&& active.duration == track.transition.duration_ms()
&& active.repeat == track.transition.repeat_enabled()
&& active.frame_interval_ms == track.transition.frame_interval_value_ms()
&& active.easing == track.transition.easing()
{
continue;
}
}
let current_value = state
.values
.get(&key)
.cloned()
.unwrap_or_else(|| track.property.default_value());
if !track.transition.repeat_enabled()
&& state.values.contains_key(&key)
&& current_value == target_value
{
continue;
}
let start_value = match &track.from {
MotionStartValue::Explicit(expr) => expr.eval(&input),
MotionStartValue::Current => current_value,
};
state.values.insert(key.clone(), start_value.clone());
state.active.insert(
key.clone(),
ActiveMotion {
target: id,
property: track.property.clone(),
start_value,
end_value: target_value,
start_time: now + track.transition.delay_value_ms(),
duration: track.transition.duration_ms(),
repeat: track.transition.repeat_enabled(),
frame_interval_ms: track.transition.frame_interval_value_ms(),
easing: track.transition.easing(),
},
);
result.changed.push(key);
}
}
pub fn scalar(value: f32) -> MotionExpr {
MotionExpr::Value(MotionValue::Scalar(value))
}
pub fn px(value: f32) -> MotionExpr {
MotionExpr::Value(MotionValue::Px(value))
}
pub fn deg(value: f32) -> MotionExpr {
MotionExpr::Value(MotionValue::Deg(value))
}
pub fn color(value: Color) -> MotionExpr {
MotionExpr::Value(MotionValue::Color(value))
}
pub fn fade() -> Vec<MotionTrack> {
vec![MotionTrack::composite(
MotionPropertyId::Opacity,
MotionStartValue::Explicit(scalar(0.0)),
scalar(1.0),
)]
}
pub fn slide_x(offset: f32) -> Vec<MotionTrack> {
vec![MotionTrack::composite(
MotionPropertyId::TranslateX,
MotionStartValue::Explicit(px(offset)),
px(0.0),
)]
}
pub fn slide_y(offset: f32) -> Vec<MotionTrack> {
vec![MotionTrack::composite(
MotionPropertyId::TranslateY,
MotionStartValue::Explicit(px(offset)),
px(0.0),
)]
}
pub fn collapse_x() -> Vec<MotionTrack> {
vec![MotionTrack {
property: MotionPropertyId::Width,
phase: MotionPhase::Layout,
from: MotionStartValue::Explicit(px(0.0)),
to: MotionExpr::IntrinsicWidth,
transition: MotionTransition::default(),
}]
}
pub fn collapse_y() -> Vec<MotionTrack> {
vec![MotionTrack {
property: MotionPropertyId::Height,
phase: MotionPhase::Layout,
from: MotionStartValue::Explicit(px(0.0)),
to: MotionExpr::IntrinsicHeight,
transition: MotionTransition::default(),
}]
}
pub fn follow_x_and_width(target: WidgetId) -> Vec<MotionTrack> {
vec![
MotionTrack::composite(
MotionPropertyId::TranslateX,
MotionStartValue::Current,
MotionExpr::LayoutX(target),
),
MotionTrack {
property: MotionPropertyId::Width,
phase: MotionPhase::Layout,
from: MotionStartValue::Current,
to: MotionExpr::LayoutWidth(target),
transition: MotionTransition::default(),
},
]
}
pub fn hover_press(id: WidgetId) -> Vec<MotionTrack> {
vec![MotionTrack::composite(
MotionPropertyId::Scale,
MotionStartValue::Current,
MotionExpr::If {
predicate: MotionPredicate::Pressed(id),
then_expr: Box::new(scalar(0.97)),
else_expr: Box::new(MotionExpr::If {
predicate: MotionPredicate::Hovered(id),
then_expr: Box::new(scalar(1.02)),
else_expr: Box::new(scalar(1.0)),
}),
},
)
.transition(MotionTransition::spring(420.0, 30.0))]
}
pub fn ripple_effect() -> RippleFx {
RippleFx::default()
}
pub fn presence(
id: impl IntoMotionId,
visible: bool,
tracks: Vec<MotionTrack>,
child: impl Into<Widget>,
) -> Widget {
Presence {
id: id.into_motion_id(),
visible,
enter: tracks.clone(),
exit: reverse_tracks_for_exit(&tracks),
child: child.into(),
..Default::default()
}
.into()
}
pub fn appear(id: impl IntoMotionId, tracks: Vec<MotionTrack>, child: impl Into<Widget>) -> Widget {
Motion {
id: id.into_motion_id(),
tracks,
child: child.into(),
..Default::default()
}
.into()
}
pub fn layout(id: impl IntoMotionId, tracks: Vec<MotionTrack>, child: impl Into<Widget>) -> Widget {
appear(id, tracks, child)
}
pub fn interactive(
id: impl IntoMotionId,
tracks: Vec<MotionTrack>,
child: impl Into<Widget>,
) -> Widget {
appear(id, tracks, child)
}
pub fn ripple(id: impl IntoMotionId, effect: RippleFx, child: impl Into<Widget>) -> Widget {
RippleLayer {
id: id.into_motion_id(),
effect,
child: child.into(),
}
.into()
}
pub fn reverse_tracks_for_exit(tracks: &[MotionTrack]) -> Vec<MotionTrack> {
tracks
.iter()
.map(|track| MotionTrack {
property: track.property.clone(),
phase: track.phase,
from: MotionStartValue::Current,
to: match &track.from {
MotionStartValue::Explicit(expr) => expr.clone(),
MotionStartValue::Current => track.property.default_value().into(),
},
transition: track.transition.clone(),
})
.collect()
}
pub fn dedupe_tracks_later_wins(tracks: Vec<MotionTrack>) -> Vec<MotionTrack> {
let mut seen = HashSet::new();
let mut out = Vec::with_capacity(tracks.len());
for track in tracks.into_iter().rev() {
if seen.insert((track.property.clone(), track.phase)) {
out.push(track);
}
}
out.reverse();
out
}
impl From<MotionValue> for MotionExpr {
fn from(value: MotionValue) -> Self {
Self::Value(value)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum SurfaceMotion {
Default,
Fade,
Scale,
SlideX(f32),
SlideY(f32),
Pop,
Composition(Vec<SurfaceMotion>),
Custom {
enter: Vec<MotionTrack>,
exit: Vec<MotionTrack>,
keep_rendered: bool,
},
}
impl SurfaceMotion {
pub fn compose(items: impl IntoIterator<Item = Self>) -> Self {
let mut out = Vec::new();
for item in items {
item.flatten_into(&mut out);
}
match out.len() {
0 => Self::Composition(Vec::new()),
1 => out.remove(0),
_ => Self::Composition(out),
}
}
pub fn enter_tracks(&self) -> Vec<MotionTrack> {
let mut out = Vec::new();
self.append_enter_tracks(&mut out);
dedupe_tracks_later_wins(out)
}
pub fn exit_tracks(&self) -> Vec<MotionTrack> {
match self {
Self::Custom { exit, .. } => exit.clone(),
_ => reverse_tracks_for_exit(&self.enter_tracks()),
}
}
pub fn keep_rendered(&self) -> bool {
match self {
Self::Custom { keep_rendered, .. } => *keep_rendered,
Self::Composition(items) => items.iter().any(Self::keep_rendered),
_ => false,
}
}
fn append_enter_tracks(&self, out: &mut Vec<MotionTrack>) {
match self {
Self::Default => {
Self::Fade.append_enter_tracks(out);
Self::Scale.append_enter_tracks(out);
}
Self::Fade => out.extend(fade()),
Self::Scale => out.push(MotionTrack::composite(
MotionPropertyId::Scale,
MotionStartValue::Explicit(scalar(0.96)),
scalar(1.0),
)),
Self::SlideX(offset) => out.extend(slide_x(*offset)),
Self::SlideY(offset) => out.extend(slide_y(*offset)),
Self::Pop => {
Self::Fade.append_enter_tracks(out);
Self::Scale.append_enter_tracks(out);
}
Self::Composition(items) => {
for item in items {
item.append_enter_tracks(out);
}
}
Self::Custom { enter, .. } => out.extend(enter.clone()),
}
}
fn flatten_into(self, out: &mut Vec<Self>) {
match self {
Self::Composition(items) => {
for item in items {
item.flatten_into(out);
}
}
item => out.push(item),
}
}
}
impl Add for SurfaceMotion {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::compose([self, rhs])
}
}
fn numeric_binary(
a: &MotionExpr,
b: &MotionExpr,
input: &MotionEvalInput<'_>,
f: impl FnOnce(f32, f32) -> f32,
) -> MotionValue {
let left = a.eval(input);
let right = b.eval(input).as_scalar_like().unwrap_or(0.0);
map_numeric(left, |left| f(left, right))
}
fn numeric_unary(
value: &MotionExpr,
input: &MotionEvalInput<'_>,
f: impl FnOnce(f32) -> f32,
) -> MotionValue {
let value = value.eval(input);
map_numeric(value, f)
}
fn map_numeric(value: MotionValue, f: impl FnOnce(f32) -> f32) -> MotionValue {
match value {
MotionValue::Scalar(v) => MotionValue::Scalar(f(v)),
MotionValue::Px(v) => MotionValue::Px(f(v)),
MotionValue::Deg(v) => MotionValue::Deg(f(v)),
MotionValue::Bool(_) | MotionValue::Color(_) => value,
}
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}