1use crate::ui::{Composite, Spacer, Widget};
8use crate::CurrentTime;
9use fission_ir::op::Color;
10use fission_ir::{CompositeScalar, WidgetId};
11use fission_layout::{LayoutPoint, LayoutSnapshot};
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::ops::Add;
15use std::sync::Arc;
16
17pub trait IntoMotionId {
26 fn into_motion_id(self) -> WidgetId;
28}
29
30impl IntoMotionId for WidgetId {
31 fn into_motion_id(self) -> WidgetId {
32 self
33 }
34}
35
36impl IntoMotionId for &'static str {
37 fn into_motion_id(self) -> WidgetId {
38 WidgetId::explicit(self)
39 }
40}
41
42impl IntoMotionId for String {
43 fn into_motion_id(self) -> WidgetId {
44 WidgetId::explicit(&self)
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub enum MotionPhase {
55 Layout,
57 Composite,
59 Paint,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
64pub enum MotionPropertyId {
83 Opacity,
85 TranslateX,
87 TranslateY,
89 Scale,
91 Rotation,
93 Width,
95 Height,
97 LayoutX,
99 LayoutY,
101 LayoutWidth,
103 LayoutHeight,
105 IntrinsicWidth,
107 IntrinsicHeight,
109 CornerRadius,
111 BackgroundColor,
113 BorderColor,
115 TextColor,
117 Custom(Arc<str>),
119}
120
121impl MotionPropertyId {
122 pub fn opacity() -> Self {
124 Self::Opacity
125 }
126
127 pub fn translate_x() -> Self {
129 Self::TranslateX
130 }
131
132 pub fn translate_y() -> Self {
134 Self::TranslateY
135 }
136
137 pub fn scale() -> Self {
139 Self::Scale
140 }
141
142 pub fn rotation() -> Self {
144 Self::Rotation
145 }
146
147 pub fn custom(name: impl Into<String>) -> Self {
152 Self::Custom(Arc::from(name.into()))
153 }
154
155 pub fn default_value(&self) -> MotionValue {
157 match self {
158 Self::Opacity | Self::Scale => MotionValue::Scalar(1.0),
159 Self::BackgroundColor | Self::BorderColor | Self::TextColor => {
160 MotionValue::Color(Color {
161 r: 0,
162 g: 0,
163 b: 0,
164 a: 0,
165 })
166 }
167 Self::TranslateX
168 | Self::TranslateY
169 | Self::Width
170 | Self::Height
171 | Self::LayoutX
172 | Self::LayoutY
173 | Self::LayoutWidth
174 | Self::LayoutHeight
175 | Self::IntrinsicWidth
176 | Self::IntrinsicHeight
177 | Self::CornerRadius => MotionValue::Px(0.0),
178 Self::Rotation => MotionValue::Deg(0.0),
179 Self::Custom(_) => MotionValue::Scalar(0.0),
180 }
181 }
182
183 pub fn default_scalar_value(&self) -> f32 {
187 self.default_value().as_scalar_like().unwrap_or(0.0)
188 }
189}
190
191#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
192pub enum MotionValue {
198 Bool(bool),
200 Scalar(f32),
202 Px(f32),
204 Deg(f32),
206 Color(Color),
208}
209
210impl MotionValue {
211 pub fn as_scalar_like(&self) -> Option<f32> {
213 match self {
214 Self::Scalar(v) | Self::Px(v) | Self::Deg(v) => Some(*v),
215 Self::Bool(_) | Self::Color(_) => None,
216 }
217 }
218
219 fn interpolate(&self, to: &Self, t: f32) -> Self {
220 let t = t.clamp(0.0, 1.0);
221 match (self, to) {
222 (Self::Scalar(a), Self::Scalar(b)) => Self::Scalar(lerp(*a, *b, t)),
223 (Self::Px(a), Self::Px(b)) => Self::Px(lerp(*a, *b, t)),
224 (Self::Deg(a), Self::Deg(b)) => Self::Deg(lerp(*a, *b, t)),
225 (Self::Color(a), Self::Color(b)) => Self::Color(Color {
226 r: lerp(a.r as f32, b.r as f32, t).round().clamp(0.0, 255.0) as u8,
227 g: lerp(a.g as f32, b.g as f32, t).round().clamp(0.0, 255.0) as u8,
228 b: lerp(a.b as f32, b.b as f32, t).round().clamp(0.0, 255.0) as u8,
229 a: lerp(a.a as f32, b.a as f32, t).round().clamp(0.0, 255.0) as u8,
230 }),
231 _ => {
232 if t >= 1.0 {
233 to.clone()
234 } else {
235 self.clone()
236 }
237 }
238 }
239 }
240}
241
242#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
243pub enum MotionPredicate {
248 Hovered(WidgetId),
250 Pressed(WidgetId),
252 Focused(WidgetId),
254 Disabled(WidgetId),
256}
257
258#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259pub enum MotionExpr {
283 Value(MotionValue),
285 IntrinsicWidth,
287 IntrinsicHeight,
289 LayoutX(WidgetId),
291 LayoutY(WidgetId),
293 LayoutWidth(WidgetId),
295 LayoutHeight(WidgetId),
297 PointerLocalX,
299 PointerLocalY,
301 If {
303 predicate: MotionPredicate,
305 then_expr: Box<MotionExpr>,
307 else_expr: Box<MotionExpr>,
309 },
310 Add(Box<MotionExpr>, Box<MotionExpr>),
312 Sub(Box<MotionExpr>, Box<MotionExpr>),
314 Mul(Box<MotionExpr>, Box<MotionExpr>),
316 Div(Box<MotionExpr>, Box<MotionExpr>),
318 Neg(Box<MotionExpr>),
320 Abs(Box<MotionExpr>),
322 Min(Box<MotionExpr>, Box<MotionExpr>),
324 Max(Box<MotionExpr>, Box<MotionExpr>),
326 Clamp {
328 value: Box<MotionExpr>,
330 min: Box<MotionExpr>,
332 max: Box<MotionExpr>,
334 },
335 Lerp {
337 from: Box<MotionExpr>,
339 to: Box<MotionExpr>,
341 t: Box<MotionExpr>,
343 },
344 MapRange {
346 value: Box<MotionExpr>,
348 from_start: f32,
350 from_end: f32,
352 to_start: f32,
354 to_end: f32,
356 clamp: bool,
358 },
359}
360
361impl MotionExpr {
362 pub fn eval(&self, input: &MotionEvalInput<'_>) -> MotionValue {
367 match self {
368 Self::Value(value) => value.clone(),
369 Self::IntrinsicWidth => input
370 .self_rect
371 .map(|rect| MotionValue::Px(rect.width()))
372 .unwrap_or(MotionValue::Px(0.0)),
373 Self::IntrinsicHeight => input
374 .self_rect
375 .map(|rect| MotionValue::Px(rect.height()))
376 .unwrap_or(MotionValue::Px(0.0)),
377 Self::LayoutX(id) => input
378 .layout
379 .and_then(|layout| layout.get_node_rect(*id))
380 .map(|rect| MotionValue::Px(rect.x()))
381 .unwrap_or(MotionValue::Px(0.0)),
382 Self::LayoutY(id) => input
383 .layout
384 .and_then(|layout| layout.get_node_rect(*id))
385 .map(|rect| MotionValue::Px(rect.y()))
386 .unwrap_or(MotionValue::Px(0.0)),
387 Self::LayoutWidth(id) => input
388 .layout
389 .and_then(|layout| layout.get_node_rect(*id))
390 .map(|rect| MotionValue::Px(rect.width()))
391 .unwrap_or(MotionValue::Px(0.0)),
392 Self::LayoutHeight(id) => input
393 .layout
394 .and_then(|layout| layout.get_node_rect(*id))
395 .map(|rect| MotionValue::Px(rect.height()))
396 .unwrap_or(MotionValue::Px(0.0)),
397 Self::PointerLocalX => input
398 .pointer_local
399 .map(|point| MotionValue::Px(point.x))
400 .unwrap_or(MotionValue::Px(0.0)),
401 Self::PointerLocalY => input
402 .pointer_local
403 .map(|point| MotionValue::Px(point.y))
404 .unwrap_or(MotionValue::Px(0.0)),
405 Self::If {
406 predicate,
407 then_expr,
408 else_expr,
409 } => {
410 if input.predicate(predicate) {
411 then_expr.eval(input)
412 } else {
413 else_expr.eval(input)
414 }
415 }
416 Self::Add(a, b) => numeric_binary(a, b, input, |a, b| a + b),
417 Self::Sub(a, b) => numeric_binary(a, b, input, |a, b| a - b),
418 Self::Mul(a, b) => numeric_binary(a, b, input, |a, b| a * b),
419 Self::Div(a, b) => numeric_binary(a, b, input, |a, b| if b == 0.0 { a } else { a / b }),
420 Self::Neg(v) => numeric_unary(v, input, |v| -v),
421 Self::Abs(v) => numeric_unary(v, input, f32::abs),
422 Self::Min(a, b) => numeric_binary(a, b, input, f32::min),
423 Self::Max(a, b) => numeric_binary(a, b, input, f32::max),
424 Self::Clamp { value, min, max } => {
425 let value = value.eval(input);
426 let min = min.eval(input).as_scalar_like().unwrap_or(0.0);
427 let max = max.eval(input).as_scalar_like().unwrap_or(min);
428 map_numeric(value, |v| v.clamp(min, max))
429 }
430 Self::Lerp { from, to, t } => {
431 let t = t.eval(input).as_scalar_like().unwrap_or(0.0);
432 from.eval(input).interpolate(&to.eval(input), t)
433 }
434 Self::MapRange {
435 value,
436 from_start,
437 from_end,
438 to_start,
439 to_end,
440 clamp,
441 } => {
442 let raw = value.eval(input).as_scalar_like().unwrap_or(0.0);
443 let denom = from_end - from_start;
444 let mut t = if denom.abs() <= f32::EPSILON {
445 0.0
446 } else {
447 (raw - from_start) / denom
448 };
449 if *clamp {
450 t = t.clamp(0.0, 1.0);
451 }
452 MotionValue::Scalar(lerp(*to_start, *to_end, t))
453 }
454 }
455 }
456}
457
458#[derive(Clone, Debug)]
459pub struct MotionEvalInput<'a> {
464 pub runtime: &'a crate::RuntimeState,
466 pub layout: Option<&'a LayoutSnapshot>,
468 pub self_id: WidgetId,
470 pub self_rect: Option<fission_layout::LayoutRect>,
472 pub pointer_local: Option<LayoutPoint>,
474}
475
476impl<'a> MotionEvalInput<'a> {
477 fn predicate(&self, predicate: &MotionPredicate) -> bool {
478 match predicate {
479 MotionPredicate::Hovered(id) => self.runtime.interaction.is_hovered(*id),
480 MotionPredicate::Pressed(id) => self.runtime.interaction.is_pressed(*id),
481 MotionPredicate::Focused(id) => self.runtime.interaction.is_focused(*id),
482 MotionPredicate::Disabled(_) => false,
483 }
484 }
485}
486
487#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
488pub enum MotionStartValue {
490 Current,
492 Explicit(MotionExpr),
494}
495
496#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
497pub enum MotionEasing {
506 Linear,
508 EaseIn,
510 EaseOut,
512 EaseInOut,
514 CubicBezier(f32, f32, f32, f32),
516}
517
518impl Default for MotionEasing {
519 fn default() -> Self {
520 Self::EaseInOut
521 }
522}
523
524impl MotionEasing {
525 pub fn apply(&self, t: f32) -> f32 {
527 match self {
528 Self::Linear => t,
529 Self::EaseIn => t * t,
530 Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
531 Self::EaseInOut => {
532 if t < 0.5 {
533 2.0 * t * t
534 } else {
535 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
536 }
537 }
538 Self::CubicBezier(_x1, y1, _x2, y2) => {
539 let t2 = t * t;
540 let t3 = t2 * t;
541 3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
542 }
543 }
544 }
545}
546
547#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
548pub enum MotionTransition {
554 Instant,
556 Tween {
558 duration_ms: u64,
560 delay_ms: u64,
562 easing: MotionEasing,
564 repeat: bool,
566 frame_interval_ms: Option<u64>,
568 },
569 Spring {
571 stiffness: f32,
573 damping: f32,
575 mass: f32,
577 epsilon: f32,
579 delay_ms: u64,
581 },
582}
583
584impl Default for MotionTransition {
585 fn default() -> Self {
586 Self::Tween {
587 duration_ms: 160,
588 delay_ms: 0,
589 easing: MotionEasing::EaseInOut,
590 repeat: false,
591 frame_interval_ms: None,
592 }
593 }
594}
595
596impl MotionTransition {
597 pub fn tween(duration_ms: u64, easing: MotionEasing) -> Self {
599 Self::Tween {
600 duration_ms,
601 delay_ms: 0,
602 easing,
603 repeat: false,
604 frame_interval_ms: None,
605 }
606 }
607
608 pub fn spring(stiffness: f32, damping: f32) -> Self {
610 Self::Spring {
611 stiffness,
612 damping,
613 mass: 1.0,
614 epsilon: 0.001,
615 delay_ms: 0,
616 }
617 }
618
619 pub fn delay_ms(mut self, delay_ms: u64) -> Self {
621 match &mut self {
622 Self::Instant => {}
623 Self::Tween {
624 delay_ms: delay, ..
625 }
626 | Self::Spring {
627 delay_ms: delay, ..
628 } => {
629 *delay = delay_ms;
630 }
631 }
632 self
633 }
634
635 pub fn repeat(mut self, repeat: bool) -> Self {
637 if let Self::Tween {
638 repeat: current, ..
639 } = &mut self
640 {
641 *current = repeat;
642 }
643 self
644 }
645
646 pub fn frame_interval_ms(mut self, frame_interval_ms: Option<u64>) -> Self {
648 if let Self::Tween {
649 frame_interval_ms: current,
650 ..
651 } = &mut self
652 {
653 *current = frame_interval_ms;
654 }
655 self
656 }
657
658 fn duration_ms(&self) -> u64 {
659 match self {
660 Self::Instant => 0,
661 Self::Tween { duration_ms, .. } => *duration_ms,
662 Self::Spring { .. } => 260,
663 }
664 }
665
666 fn delay_value_ms(&self) -> u64 {
667 match self {
668 Self::Instant => 0,
669 Self::Tween { delay_ms, .. } | Self::Spring { delay_ms, .. } => *delay_ms,
670 }
671 }
672
673 fn repeat_enabled(&self) -> bool {
674 matches!(self, Self::Tween { repeat: true, .. })
675 }
676
677 fn easing(&self) -> MotionEasing {
678 match self {
679 Self::Instant | Self::Spring { .. } => MotionEasing::EaseOut,
680 Self::Tween { easing, .. } => easing.clone(),
681 }
682 }
683
684 fn frame_interval_value_ms(&self) -> Option<u64> {
685 match self {
686 Self::Tween {
687 frame_interval_ms, ..
688 } => frame_interval_ms.filter(|ms| *ms > 0),
689 _ => None,
690 }
691 }
692}
693
694#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
695pub struct MotionTrack {
714 pub property: MotionPropertyId,
716 pub phase: MotionPhase,
718 pub from: MotionStartValue,
720 pub to: MotionExpr,
722 pub transition: MotionTransition,
724}
725
726impl MotionTrack {
727 pub fn composite(property: MotionPropertyId, from: MotionStartValue, to: MotionExpr) -> Self {
729 Self {
730 property,
731 phase: MotionPhase::Composite,
732 from,
733 to,
734 transition: MotionTransition::default(),
735 }
736 }
737
738 pub fn transition(mut self, transition: MotionTransition) -> Self {
740 self.transition = transition;
741 self
742 }
743}
744
745#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
746pub enum PresencePhase {
748 Hidden,
750 Entering,
752 Present,
754 Exiting,
756}
757
758impl Default for PresencePhase {
759 fn default() -> Self {
760 Self::Hidden
761 }
762}
763
764#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
765pub enum RipplePlacement {
767 BehindChild,
769 AboveChild,
771}
772
773#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
774pub struct RippleFx {
785 pub color: Color,
787 pub opacity: f32,
789 pub scale: f32,
791 pub transition: MotionTransition,
793 pub max_instances: usize,
795 pub placement: RipplePlacement,
797}
798
799impl Default for RippleFx {
800 fn default() -> Self {
801 Self {
802 color: Color {
803 r: 255,
804 g: 255,
805 b: 255,
806 a: 64,
807 },
808 opacity: 0.35,
809 scale: 10.0,
810 transition: MotionTransition::tween(600, MotionEasing::EaseOut),
811 max_instances: 8,
812 placement: RipplePlacement::BehindChild,
813 }
814 }
815}
816
817impl RippleFx {
818 pub fn scale(mut self, scale: f32) -> Self {
820 self.scale = scale;
821 self
822 }
823
824 pub fn duration(mut self, duration_ms: u64) -> Self {
826 if let MotionTransition::Tween {
827 duration_ms: current,
828 ..
829 } = &mut self.transition
830 {
831 *current = duration_ms;
832 }
833 self
834 }
835
836 pub fn ease(mut self, easing: MotionEasing) -> Self {
838 if let MotionTransition::Tween {
839 easing: current, ..
840 } = &mut self.transition
841 {
842 *current = easing;
843 }
844 self
845 }
846}
847
848#[derive(Clone, Debug, Serialize, Deserialize)]
849pub struct MotionDeclaration {
855 pub id: WidgetId,
857 pub kind: MotionDeclarationKind,
859}
860
861#[derive(Clone, Debug, Serialize, Deserialize)]
862pub enum MotionDeclarationKind {
864 Tracks {
866 tracks: Vec<MotionTrack>,
868 },
869 Presence {
871 visible: bool,
873 keep_rendered: bool,
875 enter: Vec<MotionTrack>,
877 exit: Vec<MotionTrack>,
879 inert_while_exiting: bool,
881 },
882 RippleLayer(RippleFx),
884}
885
886#[derive(Clone, Debug, Serialize, Deserialize)]
887pub struct Motion {
905 pub id: WidgetId,
907 pub tracks: Vec<MotionTrack>,
909 pub child: Widget,
911 pub clip_to_bounds: bool,
913 pub repaint_boundary: bool,
915}
916
917impl Default for Motion {
918 fn default() -> Self {
919 Self {
920 id: WidgetId::explicit("motion"),
921 tracks: Vec::new(),
922 child: Spacer::default().into(),
923 clip_to_bounds: false,
924 repaint_boundary: true,
925 }
926 }
927}
928
929#[derive(Clone, Debug, Serialize, Deserialize)]
930pub struct Presence {
950 pub id: WidgetId,
952 pub visible: bool,
954 pub keep_rendered: bool,
956 pub enter: Vec<MotionTrack>,
958 pub exit: Vec<MotionTrack>,
960 pub child: Widget,
962 pub clip_to_bounds: bool,
964 pub repaint_boundary: bool,
966 pub inert_while_exiting: bool,
968}
969
970impl Default for Presence {
971 fn default() -> Self {
972 Self {
973 id: WidgetId::explicit("presence"),
974 visible: true,
975 keep_rendered: false,
976 enter: Vec::new(),
977 exit: Vec::new(),
978 child: Spacer::default().into(),
979 clip_to_bounds: false,
980 repaint_boundary: true,
981 inert_while_exiting: true,
982 }
983 }
984}
985
986#[derive(Clone, Debug, Serialize, Deserialize)]
987pub struct RippleLayer {
989 pub id: WidgetId,
991 pub effect: RippleFx,
993 pub child: Widget,
995}
996
997impl Default for RippleLayer {
998 fn default() -> Self {
999 Self {
1000 id: WidgetId::explicit("ripple_layer"),
1001 effect: RippleFx::default(),
1002 child: Spacer::default().into(),
1003 }
1004 }
1005}
1006
1007impl From<Motion> for Widget {
1008 fn from(component: Motion) -> Self {
1009 crate::build::try_register_motion(MotionDeclaration {
1010 id: component.id,
1011 kind: MotionDeclarationKind::Tracks {
1012 tracks: component.tracks.clone(),
1013 },
1014 });
1015 let style = composite_style_for_tracks(component.id, component.child, &component.tracks)
1016 .clip_to_bounds(component.clip_to_bounds)
1017 .repaint_boundary(component.repaint_boundary);
1018 style.into()
1019 }
1020}
1021
1022impl From<Presence> for Widget {
1023 fn from(component: Presence) -> Self {
1024 crate::build::try_register_motion(MotionDeclaration {
1025 id: component.id,
1026 kind: MotionDeclarationKind::Presence {
1027 visible: component.visible,
1028 keep_rendered: component.keep_rendered,
1029 enter: component.enter.clone(),
1030 exit: component.exit.clone(),
1031 inert_while_exiting: component.inert_while_exiting,
1032 },
1033 });
1034
1035 let phase = crate::build::try_current_runtime_state()
1036 .and_then(|runtime| runtime.motion.presence.get(&component.id).copied())
1037 .unwrap_or(if component.visible {
1038 PresencePhase::Present
1039 } else {
1040 PresencePhase::Hidden
1041 });
1042 let should_render = component.visible
1043 || component.keep_rendered
1044 || matches!(
1045 phase,
1046 PresencePhase::Entering | PresencePhase::Present | PresencePhase::Exiting
1047 );
1048 if !should_render {
1049 return Spacer::default().into();
1050 }
1051
1052 let tracks = if component.visible {
1053 &component.enter
1054 } else {
1055 &component.exit
1056 };
1057 composite_style_for_tracks(component.id, component.child, tracks)
1058 .clip_to_bounds(component.clip_to_bounds)
1059 .repaint_boundary(component.repaint_boundary)
1060 .into()
1061 }
1062}
1063
1064impl From<RippleLayer> for Widget {
1065 fn from(component: RippleLayer) -> Self {
1066 crate::build::try_register_motion(MotionDeclaration {
1067 id: component.id,
1068 kind: MotionDeclarationKind::RippleLayer(component.effect),
1069 });
1070 Composite {
1071 id: Some(component.id),
1072 child: component.child,
1073 ..Default::default()
1074 }
1075 .into()
1076 }
1077}
1078
1079fn composite_style_for_tracks(id: WidgetId, child: Widget, tracks: &[MotionTrack]) -> Composite {
1080 let mut composite = Composite {
1081 id: Some(id),
1082 child,
1083 ..Default::default()
1084 };
1085 for track in tracks {
1086 if track.phase != MotionPhase::Composite {
1087 continue;
1088 }
1089 match track.property {
1090 MotionPropertyId::Opacity => {
1091 composite.style.opacity = Some(CompositeScalar::new(1.0).motion(id));
1092 }
1093 MotionPropertyId::TranslateX => {
1094 composite.style.translate_x = Some(CompositeScalar::new(0.0).motion(id));
1095 }
1096 MotionPropertyId::TranslateY => {
1097 composite.style.translate_y = Some(CompositeScalar::new(0.0).motion(id));
1098 }
1099 MotionPropertyId::Scale => {
1100 composite.style.scale = Some(CompositeScalar::new(1.0).motion(id));
1101 }
1102 MotionPropertyId::Rotation => {
1103 composite.style.rotation = Some(CompositeScalar::new(0.0).motion(id));
1104 }
1105 _ => {}
1106 }
1107 }
1108 composite
1109}
1110
1111#[derive(Clone, Debug, Default)]
1112pub struct MotionStateMap {
1117 pub values: HashMap<(WidgetId, MotionPropertyId), MotionValue>,
1119 pub active: HashMap<(WidgetId, MotionPropertyId), ActiveMotion>,
1121 pub presence: HashMap<WidgetId, PresencePhase>,
1123 pub ripples: HashMap<WidgetId, Vec<SpawnedRipple>>,
1125}
1126
1127impl MotionStateMap {
1128 pub fn scalar_value(&self, widget_id: WidgetId, property: MotionPropertyId) -> f32 {
1132 self.values
1133 .get(&(widget_id, property.clone()))
1134 .and_then(MotionValue::as_scalar_like)
1135 .unwrap_or_else(|| property.default_scalar_value())
1136 }
1137}
1138
1139#[derive(Clone, Debug)]
1140pub struct ActiveMotion {
1142 pub target: WidgetId,
1144 pub property: MotionPropertyId,
1146 pub start_value: MotionValue,
1148 pub end_value: MotionValue,
1150 pub start_time: u64,
1152 pub duration: u64,
1154 pub repeat: bool,
1156 pub frame_interval_ms: Option<u64>,
1158 pub easing: MotionEasing,
1160}
1161
1162#[derive(Clone, Debug)]
1163pub struct SpawnedRipple {
1165 pub id: WidgetId,
1167 pub parent: WidgetId,
1169 pub sequence: u64,
1171 pub origin_x: f32,
1173 pub origin_y: f32,
1175 pub birth_ms: u64,
1177 pub duration_ms: u64,
1179}
1180
1181#[derive(Default)]
1182pub struct MotionSyncResult {
1184 pub changed: Vec<(WidgetId, MotionPropertyId)>,
1186}
1187
1188pub fn sync_motion_declarations(
1193 state: &mut MotionStateMap,
1194 declarations: &[MotionDeclaration],
1195 runtime: &crate::RuntimeState,
1196 layout: Option<&LayoutSnapshot>,
1197 now: CurrentTime,
1198) -> MotionSyncResult {
1199 let mut result = MotionSyncResult::default();
1200 let mut requested = HashSet::new();
1201
1202 for declaration in declarations {
1203 match &declaration.kind {
1204 MotionDeclarationKind::Tracks { tracks } => {
1205 sync_tracks(
1206 state,
1207 declaration.id,
1208 tracks,
1209 runtime,
1210 layout,
1211 now,
1212 &mut requested,
1213 &mut result,
1214 );
1215 }
1216 MotionDeclarationKind::Presence {
1217 visible,
1218 keep_rendered: _,
1219 enter,
1220 exit,
1221 inert_while_exiting: _,
1222 } => {
1223 let phase = state
1224 .presence
1225 .get(&declaration.id)
1226 .copied()
1227 .unwrap_or(PresencePhase::Hidden);
1228 let next_phase = match (phase, *visible) {
1229 (PresencePhase::Hidden, true) => PresencePhase::Entering,
1230 (PresencePhase::Exiting, true) => PresencePhase::Entering,
1231 (PresencePhase::Entering, true) => PresencePhase::Entering,
1232 (PresencePhase::Present, true) => PresencePhase::Present,
1233 (PresencePhase::Hidden, false) => PresencePhase::Hidden,
1234 (PresencePhase::Entering, false)
1235 | (PresencePhase::Present, false)
1236 | (PresencePhase::Exiting, false) => PresencePhase::Exiting,
1237 };
1238 state.presence.insert(declaration.id, next_phase);
1239 let tracks = if *visible { enter } else { exit };
1240 if tracks.is_empty() {
1241 match next_phase {
1242 PresencePhase::Entering => {
1243 state
1244 .presence
1245 .insert(declaration.id, PresencePhase::Present);
1246 }
1247 PresencePhase::Exiting => {
1248 state.presence.insert(declaration.id, PresencePhase::Hidden);
1249 }
1250 PresencePhase::Hidden | PresencePhase::Present => {}
1251 }
1252 continue;
1253 }
1254 if !*visible && phase == PresencePhase::Hidden {
1255 continue;
1256 }
1257 sync_tracks(
1258 state,
1259 declaration.id,
1260 tracks,
1261 runtime,
1262 layout,
1263 now,
1264 &mut requested,
1265 &mut result,
1266 );
1267 }
1268 MotionDeclarationKind::RippleLayer(_) => {}
1269 }
1270 }
1271
1272 state.active.retain(|key, _| requested.contains(key));
1273 state.values.retain(|key, _| requested.contains(key));
1274 result
1275}
1276
1277pub fn tick_motion(
1281 state: &mut MotionStateMap,
1282 current_time: CurrentTime,
1283) -> Vec<(WidgetId, MotionPropertyId)> {
1284 let mut changed = Vec::new();
1285 let mut finished = Vec::new();
1286 let mut finished_presence = Vec::new();
1287
1288 for ((target, property), motion) in state.active.iter_mut() {
1289 let elapsed = current_time.saturating_sub(motion.start_time);
1290 let mut progress = if motion.duration == 0 {
1291 1.0
1292 } else {
1293 elapsed as f32 / motion.duration as f32
1294 };
1295
1296 if motion.repeat && progress >= 1.0 {
1297 progress %= 1.0;
1298 } else {
1299 progress = progress.clamp(0.0, 1.0);
1300 }
1301
1302 if !motion.repeat && (elapsed >= motion.duration || motion.duration == 0) {
1303 finished.push((*target, property.clone()));
1304 }
1305
1306 let eased = motion.easing.apply(progress);
1307 let value = motion.start_value.interpolate(&motion.end_value, eased);
1308 if state.values.get(&(*target, property.clone())) != Some(&value) {
1309 state.values.insert((*target, property.clone()), value);
1310 changed.push((*target, property.clone()));
1311 }
1312 }
1313
1314 for key in finished {
1315 state.active.remove(&key);
1316 if state
1317 .presence
1318 .get(&key.0)
1319 .is_some_and(|phase| *phase == PresencePhase::Entering)
1320 {
1321 finished_presence.push((key.0, PresencePhase::Present));
1322 } else if state
1323 .presence
1324 .get(&key.0)
1325 .is_some_and(|phase| *phase == PresencePhase::Exiting)
1326 {
1327 finished_presence.push((key.0, PresencePhase::Hidden));
1328 }
1329 }
1330
1331 for (id, phase) in finished_presence {
1332 state.presence.insert(id, phase);
1333 }
1334
1335 changed
1336}
1337
1338fn sync_tracks(
1339 state: &mut MotionStateMap,
1340 id: WidgetId,
1341 tracks: &[MotionTrack],
1342 runtime: &crate::RuntimeState,
1343 layout: Option<&LayoutSnapshot>,
1344 now: CurrentTime,
1345 requested: &mut HashSet<(WidgetId, MotionPropertyId)>,
1346 result: &mut MotionSyncResult,
1347) {
1348 let self_rect = layout.and_then(|layout| layout.get_node_rect(id));
1349 let input = MotionEvalInput {
1350 runtime,
1351 layout,
1352 self_id: id,
1353 self_rect,
1354 pointer_local: None,
1355 };
1356 for track in tracks {
1357 let key = (id, track.property.clone());
1358 requested.insert(key.clone());
1359 let target_value = track.to.eval(&input);
1360 if let Some(active) = state.active.get(&key) {
1361 if active.end_value == target_value
1362 && active.duration == track.transition.duration_ms()
1363 && active.repeat == track.transition.repeat_enabled()
1364 && active.frame_interval_ms == track.transition.frame_interval_value_ms()
1365 && active.easing == track.transition.easing()
1366 {
1367 continue;
1368 }
1369 }
1370
1371 let current_value = state
1372 .values
1373 .get(&key)
1374 .cloned()
1375 .unwrap_or_else(|| track.property.default_value());
1376 if !track.transition.repeat_enabled()
1377 && state.values.contains_key(&key)
1378 && current_value == target_value
1379 {
1380 continue;
1381 }
1382
1383 let start_value = match &track.from {
1384 MotionStartValue::Explicit(expr) => expr.eval(&input),
1385 MotionStartValue::Current => current_value,
1386 };
1387
1388 state.values.insert(key.clone(), start_value.clone());
1389 state.active.insert(
1390 key.clone(),
1391 ActiveMotion {
1392 target: id,
1393 property: track.property.clone(),
1394 start_value,
1395 end_value: target_value,
1396 start_time: now + track.transition.delay_value_ms(),
1397 duration: track.transition.duration_ms(),
1398 repeat: track.transition.repeat_enabled(),
1399 frame_interval_ms: track.transition.frame_interval_value_ms(),
1400 easing: track.transition.easing(),
1401 },
1402 );
1403 result.changed.push(key);
1404 }
1405}
1406
1407pub fn scalar(value: f32) -> MotionExpr {
1409 MotionExpr::Value(MotionValue::Scalar(value))
1410}
1411
1412pub fn px(value: f32) -> MotionExpr {
1414 MotionExpr::Value(MotionValue::Px(value))
1415}
1416
1417pub fn deg(value: f32) -> MotionExpr {
1419 MotionExpr::Value(MotionValue::Deg(value))
1420}
1421
1422pub fn color(value: Color) -> MotionExpr {
1424 MotionExpr::Value(MotionValue::Color(value))
1425}
1426
1427pub fn fade() -> Vec<MotionTrack> {
1429 vec![MotionTrack::composite(
1430 MotionPropertyId::Opacity,
1431 MotionStartValue::Explicit(scalar(0.0)),
1432 scalar(1.0),
1433 )]
1434}
1435
1436pub fn slide_x(offset: f32) -> Vec<MotionTrack> {
1438 vec![MotionTrack::composite(
1439 MotionPropertyId::TranslateX,
1440 MotionStartValue::Explicit(px(offset)),
1441 px(0.0),
1442 )]
1443}
1444
1445pub fn slide_y(offset: f32) -> Vec<MotionTrack> {
1447 vec![MotionTrack::composite(
1448 MotionPropertyId::TranslateY,
1449 MotionStartValue::Explicit(px(offset)),
1450 px(0.0),
1451 )]
1452}
1453
1454pub fn collapse_x() -> Vec<MotionTrack> {
1456 vec![MotionTrack {
1457 property: MotionPropertyId::Width,
1458 phase: MotionPhase::Layout,
1459 from: MotionStartValue::Explicit(px(0.0)),
1460 to: MotionExpr::IntrinsicWidth,
1461 transition: MotionTransition::default(),
1462 }]
1463}
1464
1465pub fn collapse_y() -> Vec<MotionTrack> {
1467 vec![MotionTrack {
1468 property: MotionPropertyId::Height,
1469 phase: MotionPhase::Layout,
1470 from: MotionStartValue::Explicit(px(0.0)),
1471 to: MotionExpr::IntrinsicHeight,
1472 transition: MotionTransition::default(),
1473 }]
1474}
1475
1476pub fn follow_x_and_width(target: WidgetId) -> Vec<MotionTrack> {
1480 vec![
1481 MotionTrack::composite(
1482 MotionPropertyId::TranslateX,
1483 MotionStartValue::Current,
1484 MotionExpr::LayoutX(target),
1485 ),
1486 MotionTrack {
1487 property: MotionPropertyId::Width,
1488 phase: MotionPhase::Layout,
1489 from: MotionStartValue::Current,
1490 to: MotionExpr::LayoutWidth(target),
1491 transition: MotionTransition::default(),
1492 },
1493 ]
1494}
1495
1496pub fn hover_press(id: WidgetId) -> Vec<MotionTrack> {
1498 vec![MotionTrack::composite(
1499 MotionPropertyId::Scale,
1500 MotionStartValue::Current,
1501 MotionExpr::If {
1502 predicate: MotionPredicate::Pressed(id),
1503 then_expr: Box::new(scalar(0.97)),
1504 else_expr: Box::new(MotionExpr::If {
1505 predicate: MotionPredicate::Hovered(id),
1506 then_expr: Box::new(scalar(1.02)),
1507 else_expr: Box::new(scalar(1.0)),
1508 }),
1509 },
1510 )
1511 .transition(MotionTransition::spring(420.0, 30.0))]
1512}
1513
1514pub fn ripple_effect() -> RippleFx {
1516 RippleFx::default()
1517}
1518
1519pub fn presence(
1523 id: impl IntoMotionId,
1524 visible: bool,
1525 tracks: Vec<MotionTrack>,
1526 child: impl Into<Widget>,
1527) -> Widget {
1528 Presence {
1529 id: id.into_motion_id(),
1530 visible,
1531 enter: tracks.clone(),
1532 exit: reverse_tracks_for_exit(&tracks),
1533 child: child.into(),
1534 ..Default::default()
1535 }
1536 .into()
1537}
1538
1539pub fn appear(id: impl IntoMotionId, tracks: Vec<MotionTrack>, child: impl Into<Widget>) -> Widget {
1541 Motion {
1542 id: id.into_motion_id(),
1543 tracks,
1544 child: child.into(),
1545 ..Default::default()
1546 }
1547 .into()
1548}
1549
1550pub fn layout(id: impl IntoMotionId, tracks: Vec<MotionTrack>, child: impl Into<Widget>) -> Widget {
1552 appear(id, tracks, child)
1553}
1554
1555pub fn interactive(
1557 id: impl IntoMotionId,
1558 tracks: Vec<MotionTrack>,
1559 child: impl Into<Widget>,
1560) -> Widget {
1561 appear(id, tracks, child)
1562}
1563
1564pub fn ripple(id: impl IntoMotionId, effect: RippleFx, child: impl Into<Widget>) -> Widget {
1566 RippleLayer {
1567 id: id.into_motion_id(),
1568 effect,
1569 child: child.into(),
1570 }
1571 .into()
1572}
1573
1574pub fn reverse_tracks_for_exit(tracks: &[MotionTrack]) -> Vec<MotionTrack> {
1579 tracks
1580 .iter()
1581 .map(|track| MotionTrack {
1582 property: track.property.clone(),
1583 phase: track.phase,
1584 from: MotionStartValue::Current,
1585 to: match &track.from {
1586 MotionStartValue::Explicit(expr) => expr.clone(),
1587 MotionStartValue::Current => track.property.default_value().into(),
1588 },
1589 transition: track.transition.clone(),
1590 })
1591 .collect()
1592}
1593
1594pub fn dedupe_tracks_later_wins(tracks: Vec<MotionTrack>) -> Vec<MotionTrack> {
1599 let mut seen = HashSet::new();
1600 let mut out = Vec::with_capacity(tracks.len());
1601 for track in tracks.into_iter().rev() {
1602 if seen.insert((track.property.clone(), track.phase)) {
1603 out.push(track);
1604 }
1605 }
1606 out.reverse();
1607 out
1608}
1609
1610impl From<MotionValue> for MotionExpr {
1611 fn from(value: MotionValue) -> Self {
1612 Self::Value(value)
1613 }
1614}
1615
1616#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1617pub enum SurfaceMotion {
1623 Default,
1625 Fade,
1627 Scale,
1629 SlideX(f32),
1631 SlideY(f32),
1633 Pop,
1635 Composition(Vec<SurfaceMotion>),
1637 Custom {
1639 enter: Vec<MotionTrack>,
1641 exit: Vec<MotionTrack>,
1643 keep_rendered: bool,
1645 },
1646}
1647
1648impl SurfaceMotion {
1649 pub fn compose(items: impl IntoIterator<Item = Self>) -> Self {
1651 let mut out = Vec::new();
1652 for item in items {
1653 item.flatten_into(&mut out);
1654 }
1655 match out.len() {
1656 0 => Self::Composition(Vec::new()),
1657 1 => out.remove(0),
1658 _ => Self::Composition(out),
1659 }
1660 }
1661
1662 pub fn enter_tracks(&self) -> Vec<MotionTrack> {
1664 let mut out = Vec::new();
1665 self.append_enter_tracks(&mut out);
1666 dedupe_tracks_later_wins(out)
1667 }
1668
1669 pub fn exit_tracks(&self) -> Vec<MotionTrack> {
1671 match self {
1672 Self::Custom { exit, .. } => exit.clone(),
1673 _ => reverse_tracks_for_exit(&self.enter_tracks()),
1674 }
1675 }
1676
1677 pub fn keep_rendered(&self) -> bool {
1679 match self {
1680 Self::Custom { keep_rendered, .. } => *keep_rendered,
1681 Self::Composition(items) => items.iter().any(Self::keep_rendered),
1682 _ => false,
1683 }
1684 }
1685
1686 fn append_enter_tracks(&self, out: &mut Vec<MotionTrack>) {
1687 match self {
1688 Self::Default => {
1689 Self::Fade.append_enter_tracks(out);
1690 Self::Scale.append_enter_tracks(out);
1691 }
1692 Self::Fade => out.extend(fade()),
1693 Self::Scale => out.push(MotionTrack::composite(
1694 MotionPropertyId::Scale,
1695 MotionStartValue::Explicit(scalar(0.96)),
1696 scalar(1.0),
1697 )),
1698 Self::SlideX(offset) => out.extend(slide_x(*offset)),
1699 Self::SlideY(offset) => out.extend(slide_y(*offset)),
1700 Self::Pop => {
1701 Self::Fade.append_enter_tracks(out);
1702 Self::Scale.append_enter_tracks(out);
1703 }
1704 Self::Composition(items) => {
1705 for item in items {
1706 item.append_enter_tracks(out);
1707 }
1708 }
1709 Self::Custom { enter, .. } => out.extend(enter.clone()),
1710 }
1711 }
1712
1713 fn flatten_into(self, out: &mut Vec<Self>) {
1714 match self {
1715 Self::Composition(items) => {
1716 for item in items {
1717 item.flatten_into(out);
1718 }
1719 }
1720 item => out.push(item),
1721 }
1722 }
1723}
1724
1725impl Add for SurfaceMotion {
1726 type Output = Self;
1727
1728 fn add(self, rhs: Self) -> Self::Output {
1729 Self::compose([self, rhs])
1730 }
1731}
1732
1733fn numeric_binary(
1734 a: &MotionExpr,
1735 b: &MotionExpr,
1736 input: &MotionEvalInput<'_>,
1737 f: impl FnOnce(f32, f32) -> f32,
1738) -> MotionValue {
1739 let left = a.eval(input);
1740 let right = b.eval(input).as_scalar_like().unwrap_or(0.0);
1741 map_numeric(left, |left| f(left, right))
1742}
1743
1744fn numeric_unary(
1745 value: &MotionExpr,
1746 input: &MotionEvalInput<'_>,
1747 f: impl FnOnce(f32) -> f32,
1748) -> MotionValue {
1749 let value = value.eval(input);
1750 map_numeric(value, f)
1751}
1752
1753fn map_numeric(value: MotionValue, f: impl FnOnce(f32) -> f32) -> MotionValue {
1754 match value {
1755 MotionValue::Scalar(v) => MotionValue::Scalar(f(v)),
1756 MotionValue::Px(v) => MotionValue::Px(f(v)),
1757 MotionValue::Deg(v) => MotionValue::Deg(f(v)),
1758 MotionValue::Bool(_) | MotionValue::Color(_) => value,
1759 }
1760}
1761
1762fn lerp(a: f32, b: f32, t: f32) -> f32 {
1763 a + (b - a) * t
1764}