1use crate::ui::{Composite, Spacer, Widget};
8use crate::CurrentTime;
9use fission_ir::op::{BoxShadow, Color, Fill};
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 BackgroundFill,
115 BorderColor,
117 BorderWidth,
119 TextColor,
121 BoxShadows,
123 PaddingLeft,
125 PaddingRight,
127 PaddingTop,
129 PaddingBottom,
131 Custom(Arc<str>),
133}
134
135impl MotionPropertyId {
136 pub fn opacity() -> Self {
138 Self::Opacity
139 }
140
141 pub fn translate_x() -> Self {
143 Self::TranslateX
144 }
145
146 pub fn translate_y() -> Self {
148 Self::TranslateY
149 }
150
151 pub fn scale() -> Self {
153 Self::Scale
154 }
155
156 pub fn rotation() -> Self {
158 Self::Rotation
159 }
160
161 pub fn custom(name: impl Into<String>) -> Self {
166 Self::Custom(Arc::from(name.into()))
167 }
168
169 pub fn default_value(&self) -> MotionValue {
171 match self {
172 Self::Opacity | Self::Scale => MotionValue::Scalar(1.0),
173 Self::BackgroundColor | Self::BorderColor | Self::TextColor => {
174 MotionValue::Color(Color {
175 r: 0,
176 g: 0,
177 b: 0,
178 a: 0,
179 })
180 }
181 Self::BackgroundFill => MotionValue::Fill(Fill::Solid(Color::TRANSPARENT)),
182 Self::BoxShadows => MotionValue::Shadows(Vec::new()),
183 Self::TranslateX
184 | Self::TranslateY
185 | Self::Width
186 | Self::Height
187 | Self::LayoutX
188 | Self::LayoutY
189 | Self::LayoutWidth
190 | Self::LayoutHeight
191 | Self::IntrinsicWidth
192 | Self::IntrinsicHeight
193 | Self::CornerRadius
194 | Self::BorderWidth
195 | Self::PaddingLeft
196 | Self::PaddingRight
197 | Self::PaddingTop
198 | Self::PaddingBottom => MotionValue::Px(0.0),
199 Self::Rotation => MotionValue::Deg(0.0),
200 Self::Custom(_) => MotionValue::Scalar(0.0),
201 }
202 }
203
204 pub fn default_scalar_value(&self) -> f32 {
208 self.default_value().as_scalar_like().unwrap_or(0.0)
209 }
210}
211
212#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
213pub enum MotionValue {
219 Bool(bool),
221 Scalar(f32),
223 Px(f32),
225 Deg(f32),
227 Color(Color),
229 Fill(Fill),
231 Shadows(Vec<BoxShadow>),
233}
234
235impl MotionValue {
236 pub fn as_scalar_like(&self) -> Option<f32> {
238 match self {
239 Self::Scalar(v) | Self::Px(v) | Self::Deg(v) => Some(*v),
240 Self::Bool(_) | Self::Color(_) | Self::Fill(_) | Self::Shadows(_) => None,
241 }
242 }
243
244 fn interpolate(&self, to: &Self, t: f32) -> Self {
245 let t = t.clamp(0.0, 1.0);
246 match (self, to) {
247 (Self::Scalar(a), Self::Scalar(b)) => Self::Scalar(lerp(*a, *b, t)),
248 (Self::Px(a), Self::Px(b)) => Self::Px(lerp(*a, *b, t)),
249 (Self::Deg(a), Self::Deg(b)) => Self::Deg(lerp(*a, *b, t)),
250 (Self::Color(a), Self::Color(b)) => Self::Color(Color {
251 r: lerp(a.r as f32, b.r as f32, t).round().clamp(0.0, 255.0) as u8,
252 g: lerp(a.g as f32, b.g as f32, t).round().clamp(0.0, 255.0) as u8,
253 b: lerp(a.b as f32, b.b as f32, t).round().clamp(0.0, 255.0) as u8,
254 a: lerp(a.a as f32, b.a as f32, t).round().clamp(0.0, 255.0) as u8,
255 }),
256 _ => {
257 if t >= 1.0 {
258 to.clone()
259 } else {
260 self.clone()
261 }
262 }
263 }
264 }
265}
266
267#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
268pub enum MotionPredicate {
273 Hovered(WidgetId),
275 Pressed(WidgetId),
277 Focused(WidgetId),
279 Disabled(WidgetId),
281}
282
283#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
284pub enum MotionExpr {
308 Value(MotionValue),
310 IntrinsicWidth,
312 IntrinsicHeight,
314 LayoutX(WidgetId),
316 LayoutY(WidgetId),
318 LayoutWidth(WidgetId),
320 LayoutHeight(WidgetId),
322 PointerLocalX,
324 PointerLocalY,
326 If {
328 predicate: MotionPredicate,
330 then_expr: Box<MotionExpr>,
332 else_expr: Box<MotionExpr>,
334 },
335 Add(Box<MotionExpr>, Box<MotionExpr>),
337 Sub(Box<MotionExpr>, Box<MotionExpr>),
339 Mul(Box<MotionExpr>, Box<MotionExpr>),
341 Div(Box<MotionExpr>, Box<MotionExpr>),
343 Neg(Box<MotionExpr>),
345 Abs(Box<MotionExpr>),
347 Min(Box<MotionExpr>, Box<MotionExpr>),
349 Max(Box<MotionExpr>, Box<MotionExpr>),
351 Clamp {
353 value: Box<MotionExpr>,
355 min: Box<MotionExpr>,
357 max: Box<MotionExpr>,
359 },
360 Lerp {
362 from: Box<MotionExpr>,
364 to: Box<MotionExpr>,
366 t: Box<MotionExpr>,
368 },
369 MapRange {
371 value: Box<MotionExpr>,
373 from_start: f32,
375 from_end: f32,
377 to_start: f32,
379 to_end: f32,
381 clamp: bool,
383 },
384}
385
386impl MotionExpr {
387 pub fn eval(&self, input: &MotionEvalInput<'_>) -> MotionValue {
392 match self {
393 Self::Value(value) => value.clone(),
394 Self::IntrinsicWidth => input
395 .self_rect
396 .map(|rect| MotionValue::Px(rect.width()))
397 .unwrap_or(MotionValue::Px(0.0)),
398 Self::IntrinsicHeight => input
399 .self_rect
400 .map(|rect| MotionValue::Px(rect.height()))
401 .unwrap_or(MotionValue::Px(0.0)),
402 Self::LayoutX(id) => input
403 .layout
404 .and_then(|layout| layout.get_node_rect(*id))
405 .map(|rect| MotionValue::Px(rect.x()))
406 .unwrap_or(MotionValue::Px(0.0)),
407 Self::LayoutY(id) => input
408 .layout
409 .and_then(|layout| layout.get_node_rect(*id))
410 .map(|rect| MotionValue::Px(rect.y()))
411 .unwrap_or(MotionValue::Px(0.0)),
412 Self::LayoutWidth(id) => input
413 .layout
414 .and_then(|layout| layout.get_node_rect(*id))
415 .map(|rect| MotionValue::Px(rect.width()))
416 .unwrap_or(MotionValue::Px(0.0)),
417 Self::LayoutHeight(id) => input
418 .layout
419 .and_then(|layout| layout.get_node_rect(*id))
420 .map(|rect| MotionValue::Px(rect.height()))
421 .unwrap_or(MotionValue::Px(0.0)),
422 Self::PointerLocalX => input
423 .pointer_local
424 .map(|point| MotionValue::Px(point.x))
425 .unwrap_or(MotionValue::Px(0.0)),
426 Self::PointerLocalY => input
427 .pointer_local
428 .map(|point| MotionValue::Px(point.y))
429 .unwrap_or(MotionValue::Px(0.0)),
430 Self::If {
431 predicate,
432 then_expr,
433 else_expr,
434 } => {
435 if input.predicate(predicate) {
436 then_expr.eval(input)
437 } else {
438 else_expr.eval(input)
439 }
440 }
441 Self::Add(a, b) => numeric_binary(a, b, input, |a, b| a + b),
442 Self::Sub(a, b) => numeric_binary(a, b, input, |a, b| a - b),
443 Self::Mul(a, b) => numeric_binary(a, b, input, |a, b| a * b),
444 Self::Div(a, b) => numeric_binary(a, b, input, |a, b| if b == 0.0 { a } else { a / b }),
445 Self::Neg(v) => numeric_unary(v, input, |v| -v),
446 Self::Abs(v) => numeric_unary(v, input, f32::abs),
447 Self::Min(a, b) => numeric_binary(a, b, input, f32::min),
448 Self::Max(a, b) => numeric_binary(a, b, input, f32::max),
449 Self::Clamp { value, min, max } => {
450 let value = value.eval(input);
451 let min = min.eval(input).as_scalar_like().unwrap_or(0.0);
452 let max = max.eval(input).as_scalar_like().unwrap_or(min);
453 map_numeric(value, |v| v.clamp(min, max))
454 }
455 Self::Lerp { from, to, t } => {
456 let t = t.eval(input).as_scalar_like().unwrap_or(0.0);
457 from.eval(input).interpolate(&to.eval(input), t)
458 }
459 Self::MapRange {
460 value,
461 from_start,
462 from_end,
463 to_start,
464 to_end,
465 clamp,
466 } => {
467 let raw = value.eval(input).as_scalar_like().unwrap_or(0.0);
468 let denom = from_end - from_start;
469 let mut t = if denom.abs() <= f32::EPSILON {
470 0.0
471 } else {
472 (raw - from_start) / denom
473 };
474 if *clamp {
475 t = t.clamp(0.0, 1.0);
476 }
477 MotionValue::Scalar(lerp(*to_start, *to_end, t))
478 }
479 }
480 }
481}
482
483#[derive(Clone, Debug)]
484pub struct MotionEvalInput<'a> {
489 pub runtime: &'a crate::RuntimeState,
491 pub layout: Option<&'a LayoutSnapshot>,
493 pub self_id: WidgetId,
495 pub self_rect: Option<fission_layout::LayoutRect>,
497 pub pointer_local: Option<LayoutPoint>,
499}
500
501impl<'a> MotionEvalInput<'a> {
502 fn predicate(&self, predicate: &MotionPredicate) -> bool {
503 match predicate {
504 MotionPredicate::Hovered(id) => self.runtime.interaction.is_hovered(*id),
505 MotionPredicate::Pressed(id) => self.runtime.interaction.is_pressed(*id),
506 MotionPredicate::Focused(id) => self.runtime.interaction.is_focused(*id),
507 MotionPredicate::Disabled(_) => false,
508 }
509 }
510}
511
512#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
513pub enum MotionStartValue {
515 Current,
517 Explicit(MotionExpr),
519}
520
521#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
522pub enum MotionEasing {
531 Linear,
533 EaseIn,
535 EaseOut,
537 EaseInOut,
539 CubicBezier(f32, f32, f32, f32),
541}
542
543impl Default for MotionEasing {
544 fn default() -> Self {
545 Self::EaseInOut
546 }
547}
548
549impl MotionEasing {
550 pub fn apply(&self, t: f32) -> f32 {
552 match self {
553 Self::Linear => t,
554 Self::EaseIn => t * t,
555 Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
556 Self::EaseInOut => {
557 if t < 0.5 {
558 2.0 * t * t
559 } else {
560 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
561 }
562 }
563 Self::CubicBezier(_x1, y1, _x2, y2) => {
564 let t2 = t * t;
565 let t3 = t2 * t;
566 3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
567 }
568 }
569 }
570}
571
572#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
573pub enum MotionTransition {
579 Instant,
581 Tween {
583 duration_ms: u64,
585 delay_ms: u64,
587 easing: MotionEasing,
589 repeat: bool,
591 frame_interval_ms: Option<u64>,
593 },
594 Spring {
596 stiffness: f32,
598 damping: f32,
600 mass: f32,
602 epsilon: f32,
604 delay_ms: u64,
606 },
607}
608
609pub type Transition = MotionTransition;
611
612impl Default for MotionTransition {
613 fn default() -> Self {
614 Self::Tween {
615 duration_ms: 160,
616 delay_ms: 0,
617 easing: MotionEasing::EaseInOut,
618 repeat: false,
619 frame_interval_ms: None,
620 }
621 }
622}
623
624impl MotionTransition {
625 pub fn ease_out(duration_ms: u64) -> Self {
627 Self::tween(duration_ms, MotionEasing::EaseOut)
628 }
629 pub fn tween(duration_ms: u64, easing: MotionEasing) -> Self {
631 Self::Tween {
632 duration_ms,
633 delay_ms: 0,
634 easing,
635 repeat: false,
636 frame_interval_ms: None,
637 }
638 }
639
640 pub fn spring(stiffness: f32, damping: f32) -> Self {
642 Self::Spring {
643 stiffness,
644 damping,
645 mass: 1.0,
646 epsilon: 0.001,
647 delay_ms: 0,
648 }
649 }
650
651 pub fn delay_ms(mut self, delay_ms: u64) -> Self {
653 match &mut self {
654 Self::Instant => {}
655 Self::Tween {
656 delay_ms: delay, ..
657 }
658 | Self::Spring {
659 delay_ms: delay, ..
660 } => {
661 *delay = delay_ms;
662 }
663 }
664 self
665 }
666
667 pub fn repeat(mut self, repeat: bool) -> Self {
669 if let Self::Tween {
670 repeat: current, ..
671 } = &mut self
672 {
673 *current = repeat;
674 }
675 self
676 }
677
678 pub fn frame_interval_ms(mut self, frame_interval_ms: Option<u64>) -> Self {
680 if let Self::Tween {
681 frame_interval_ms: current,
682 ..
683 } = &mut self
684 {
685 *current = frame_interval_ms;
686 }
687 self
688 }
689
690 fn duration_ms(&self) -> u64 {
691 match self {
692 Self::Instant => 0,
693 Self::Tween { duration_ms, .. } => *duration_ms,
694 Self::Spring { .. } => 260,
695 }
696 }
697
698 fn delay_value_ms(&self) -> u64 {
699 match self {
700 Self::Instant => 0,
701 Self::Tween { delay_ms, .. } | Self::Spring { delay_ms, .. } => *delay_ms,
702 }
703 }
704
705 fn repeat_enabled(&self) -> bool {
706 matches!(self, Self::Tween { repeat: true, .. })
707 }
708
709 fn easing(&self) -> MotionEasing {
710 match self {
711 Self::Instant | Self::Spring { .. } => MotionEasing::EaseOut,
712 Self::Tween { easing, .. } => easing.clone(),
713 }
714 }
715
716 fn frame_interval_value_ms(&self) -> Option<u64> {
717 match self {
718 Self::Tween {
719 frame_interval_ms, ..
720 } => frame_interval_ms.filter(|ms| *ms > 0),
721 _ => None,
722 }
723 }
724}
725
726#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
727pub struct MotionTrack {
746 pub property: MotionPropertyId,
748 pub phase: MotionPhase,
750 pub from: MotionStartValue,
752 pub to: MotionExpr,
754 pub transition: MotionTransition,
756}
757
758impl MotionTrack {
759 pub fn composite(property: MotionPropertyId, from: MotionStartValue, to: MotionExpr) -> Self {
761 Self {
762 property,
763 phase: MotionPhase::Composite,
764 from,
765 to,
766 transition: MotionTransition::default(),
767 }
768 }
769
770 pub fn layout(property: MotionPropertyId, from: MotionStartValue, to: MotionExpr) -> Self {
772 Self {
773 property,
774 phase: MotionPhase::Layout,
775 from,
776 to,
777 transition: MotionTransition::default(),
778 }
779 }
780
781 pub fn paint(property: MotionPropertyId, from: MotionStartValue, to: MotionExpr) -> Self {
783 Self {
784 property,
785 phase: MotionPhase::Paint,
786 from,
787 to,
788 transition: MotionTransition::default(),
789 }
790 }
791
792 pub fn transition(mut self, transition: MotionTransition) -> Self {
794 self.transition = transition;
795 self
796 }
797}
798
799#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
800pub enum PresencePhase {
802 Hidden,
804 Entering,
806 Present,
808 Exiting,
810}
811
812impl Default for PresencePhase {
813 fn default() -> Self {
814 Self::Hidden
815 }
816}
817
818#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
819pub enum RipplePlacement {
821 BehindChild,
823 AboveChild,
825}
826
827#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
828pub struct RippleFx {
839 pub color: Color,
841 pub opacity: f32,
843 pub scale: f32,
845 pub transition: MotionTransition,
847 pub max_instances: usize,
849 pub placement: RipplePlacement,
851}
852
853impl Default for RippleFx {
854 fn default() -> Self {
855 Self {
856 color: Color {
857 r: 255,
858 g: 255,
859 b: 255,
860 a: 64,
861 },
862 opacity: 0.35,
863 scale: 10.0,
864 transition: MotionTransition::tween(600, MotionEasing::EaseOut),
865 max_instances: 8,
866 placement: RipplePlacement::BehindChild,
867 }
868 }
869}
870
871impl RippleFx {
872 pub fn scale(mut self, scale: f32) -> Self {
874 self.scale = scale;
875 self
876 }
877
878 pub fn duration(mut self, duration_ms: u64) -> Self {
880 if let MotionTransition::Tween {
881 duration_ms: current,
882 ..
883 } = &mut self.transition
884 {
885 *current = duration_ms;
886 }
887 self
888 }
889
890 pub fn ease(mut self, easing: MotionEasing) -> Self {
892 if let MotionTransition::Tween {
893 easing: current, ..
894 } = &mut self.transition
895 {
896 *current = easing;
897 }
898 self
899 }
900}
901
902#[derive(Clone, Debug, Serialize, Deserialize)]
903pub struct MotionDeclaration {
909 pub id: WidgetId,
911 pub kind: MotionDeclarationKind,
913}
914
915#[derive(Clone, Debug, Serialize, Deserialize)]
916pub enum MotionDeclarationKind {
918 Tracks {
920 tracks: Vec<MotionTrack>,
922 },
923 Presence {
925 visible: bool,
927 keep_rendered: bool,
929 enter: Vec<MotionTrack>,
931 exit: Vec<MotionTrack>,
933 inert_while_exiting: bool,
935 },
936 RippleLayer(RippleFx),
938}
939
940#[derive(Clone, Debug, Serialize, Deserialize)]
941pub struct Motion {
959 pub id: WidgetId,
961 pub tracks: Vec<MotionTrack>,
963 pub child: Widget,
965 pub clip_to_bounds: bool,
967 pub repaint_boundary: bool,
969}
970
971impl Default for Motion {
972 fn default() -> Self {
973 Self {
974 id: WidgetId::explicit("motion"),
975 tracks: Vec::new(),
976 child: Spacer::default().into(),
977 clip_to_bounds: false,
978 repaint_boundary: true,
979 }
980 }
981}
982
983#[derive(Clone, Debug, Serialize, Deserialize)]
984pub struct Presence {
1004 pub id: WidgetId,
1006 pub visible: bool,
1008 pub keep_rendered: bool,
1010 pub enter: Vec<MotionTrack>,
1012 pub exit: Vec<MotionTrack>,
1014 pub child: Widget,
1016 pub clip_to_bounds: bool,
1018 pub repaint_boundary: bool,
1020 pub inert_while_exiting: bool,
1022}
1023
1024impl Default for Presence {
1025 fn default() -> Self {
1026 Self {
1027 id: WidgetId::explicit("presence"),
1028 visible: true,
1029 keep_rendered: false,
1030 enter: Vec::new(),
1031 exit: Vec::new(),
1032 child: Spacer::default().into(),
1033 clip_to_bounds: false,
1034 repaint_boundary: true,
1035 inert_while_exiting: true,
1036 }
1037 }
1038}
1039
1040#[derive(Clone, Debug, Serialize, Deserialize)]
1041pub struct RippleLayer {
1043 pub id: WidgetId,
1045 pub effect: RippleFx,
1047 pub child: Widget,
1049}
1050
1051impl Default for RippleLayer {
1052 fn default() -> Self {
1053 Self {
1054 id: WidgetId::explicit("ripple_layer"),
1055 effect: RippleFx::default(),
1056 child: Spacer::default().into(),
1057 }
1058 }
1059}
1060
1061impl From<Motion> for Widget {
1062 fn from(component: Motion) -> Self {
1063 crate::build::try_register_motion(MotionDeclaration {
1064 id: component.id,
1065 kind: MotionDeclarationKind::Tracks {
1066 tracks: component.tracks.clone(),
1067 },
1068 });
1069 let style = composite_style_for_tracks(component.id, component.child, &component.tracks)
1070 .clip_to_bounds(component.clip_to_bounds)
1071 .repaint_boundary(component.repaint_boundary);
1072 style.into()
1073 }
1074}
1075
1076impl From<Presence> for Widget {
1077 fn from(component: Presence) -> Self {
1078 crate::build::try_register_motion(MotionDeclaration {
1079 id: component.id,
1080 kind: MotionDeclarationKind::Presence {
1081 visible: component.visible,
1082 keep_rendered: component.keep_rendered,
1083 enter: component.enter.clone(),
1084 exit: component.exit.clone(),
1085 inert_while_exiting: component.inert_while_exiting,
1086 },
1087 });
1088
1089 let phase = crate::build::try_current_runtime_state()
1090 .and_then(|runtime| runtime.motion.presence.get(&component.id).copied())
1091 .unwrap_or(if component.visible {
1092 PresencePhase::Present
1093 } else {
1094 PresencePhase::Hidden
1095 });
1096 let should_render = component.visible
1097 || component.keep_rendered
1098 || matches!(
1099 phase,
1100 PresencePhase::Entering | PresencePhase::Present | PresencePhase::Exiting
1101 );
1102 if !should_render {
1103 return Spacer::default().into();
1104 }
1105
1106 let tracks = if component.visible {
1107 &component.enter
1108 } else {
1109 &component.exit
1110 };
1111 composite_style_for_tracks(component.id, component.child, tracks)
1112 .clip_to_bounds(component.clip_to_bounds)
1113 .repaint_boundary(component.repaint_boundary)
1114 .into()
1115 }
1116}
1117
1118impl From<RippleLayer> for Widget {
1119 fn from(component: RippleLayer) -> Self {
1120 crate::build::try_register_motion(MotionDeclaration {
1121 id: component.id,
1122 kind: MotionDeclarationKind::RippleLayer(component.effect),
1123 });
1124 Composite {
1125 id: Some(component.id),
1126 child: component.child,
1127 ..Default::default()
1128 }
1129 .into()
1130 }
1131}
1132
1133fn composite_style_for_tracks(id: WidgetId, child: Widget, tracks: &[MotionTrack]) -> Composite {
1134 let mut composite = Composite {
1135 id: Some(id),
1136 child,
1137 ..Default::default()
1138 };
1139 for track in tracks {
1140 if track.phase != MotionPhase::Composite {
1141 continue;
1142 }
1143 match track.property {
1144 MotionPropertyId::Opacity => {
1145 composite.style.opacity = Some(CompositeScalar::new(1.0).motion(id));
1146 }
1147 MotionPropertyId::TranslateX => {
1148 composite.style.translate_x = Some(CompositeScalar::new(0.0).motion(id));
1149 }
1150 MotionPropertyId::TranslateY => {
1151 composite.style.translate_y = Some(CompositeScalar::new(0.0).motion(id));
1152 }
1153 MotionPropertyId::Scale => {
1154 composite.style.scale = Some(CompositeScalar::new(1.0).motion(id));
1155 }
1156 MotionPropertyId::Rotation => {
1157 composite.style.rotation = Some(CompositeScalar::new(0.0).motion(id));
1158 }
1159 _ => {}
1160 }
1161 }
1162 composite
1163}
1164
1165#[derive(Clone, Debug, Default)]
1166pub struct MotionStateMap {
1171 pub values: HashMap<(WidgetId, MotionPropertyId), MotionValue>,
1173 pub active: HashMap<(WidgetId, MotionPropertyId), ActiveMotion>,
1175 pub presence: HashMap<WidgetId, PresencePhase>,
1177 pub ripples: HashMap<WidgetId, Vec<SpawnedRipple>>,
1179}
1180
1181impl MotionStateMap {
1182 pub fn scalar_value(&self, widget_id: WidgetId, property: MotionPropertyId) -> f32 {
1186 self.values
1187 .get(&(widget_id, property.clone()))
1188 .and_then(MotionValue::as_scalar_like)
1189 .unwrap_or_else(|| property.default_scalar_value())
1190 }
1191}
1192
1193#[derive(Clone, Debug)]
1194pub struct ActiveMotion {
1196 pub target: WidgetId,
1198 pub property: MotionPropertyId,
1200 pub start_value: MotionValue,
1202 pub end_value: MotionValue,
1204 pub start_time: u64,
1206 pub duration: u64,
1208 pub repeat: bool,
1210 pub frame_interval_ms: Option<u64>,
1212 pub easing: MotionEasing,
1214}
1215
1216#[derive(Clone, Debug)]
1217pub struct SpawnedRipple {
1219 pub id: WidgetId,
1221 pub parent: WidgetId,
1223 pub sequence: u64,
1225 pub origin_x: f32,
1227 pub origin_y: f32,
1229 pub birth_ms: u64,
1231 pub duration_ms: u64,
1233}
1234
1235#[derive(Default)]
1236pub struct MotionSyncResult {
1238 pub changed: Vec<(WidgetId, MotionPropertyId)>,
1240}
1241
1242pub fn sync_motion_declarations(
1247 state: &mut MotionStateMap,
1248 declarations: &[MotionDeclaration],
1249 runtime: &crate::RuntimeState,
1250 layout: Option<&LayoutSnapshot>,
1251 now: CurrentTime,
1252) -> MotionSyncResult {
1253 let mut result = MotionSyncResult::default();
1254 let mut requested = HashSet::new();
1255
1256 for declaration in declarations {
1257 match &declaration.kind {
1258 MotionDeclarationKind::Tracks { tracks } => {
1259 sync_tracks(
1260 state,
1261 declaration.id,
1262 tracks,
1263 runtime,
1264 layout,
1265 now,
1266 &mut requested,
1267 &mut result,
1268 );
1269 }
1270 MotionDeclarationKind::Presence {
1271 visible,
1272 keep_rendered: _,
1273 enter,
1274 exit,
1275 inert_while_exiting: _,
1276 } => {
1277 let phase = state
1278 .presence
1279 .get(&declaration.id)
1280 .copied()
1281 .unwrap_or(PresencePhase::Hidden);
1282 let next_phase = match (phase, *visible) {
1283 (PresencePhase::Hidden, true) => PresencePhase::Entering,
1284 (PresencePhase::Exiting, true) => PresencePhase::Entering,
1285 (PresencePhase::Entering, true) => PresencePhase::Entering,
1286 (PresencePhase::Present, true) => PresencePhase::Present,
1287 (PresencePhase::Hidden, false) => PresencePhase::Hidden,
1288 (PresencePhase::Entering, false)
1289 | (PresencePhase::Present, false)
1290 | (PresencePhase::Exiting, false) => PresencePhase::Exiting,
1291 };
1292 state.presence.insert(declaration.id, next_phase);
1293 let tracks = if *visible { enter } else { exit };
1294 if tracks.is_empty() {
1295 match next_phase {
1296 PresencePhase::Entering => {
1297 state
1298 .presence
1299 .insert(declaration.id, PresencePhase::Present);
1300 }
1301 PresencePhase::Exiting => {
1302 state.presence.insert(declaration.id, PresencePhase::Hidden);
1303 }
1304 PresencePhase::Hidden | PresencePhase::Present => {}
1305 }
1306 continue;
1307 }
1308 if !*visible && phase == PresencePhase::Hidden {
1309 continue;
1310 }
1311 sync_tracks(
1312 state,
1313 declaration.id,
1314 tracks,
1315 runtime,
1316 layout,
1317 now,
1318 &mut requested,
1319 &mut result,
1320 );
1321 }
1322 MotionDeclarationKind::RippleLayer(_) => {}
1323 }
1324 }
1325
1326 state.active.retain(|key, _| requested.contains(key));
1327 state.values.retain(|key, _| requested.contains(key));
1328 result
1329}
1330
1331pub fn tick_motion(
1335 state: &mut MotionStateMap,
1336 current_time: CurrentTime,
1337) -> Vec<(WidgetId, MotionPropertyId)> {
1338 let mut changed = Vec::new();
1339 let mut finished = Vec::new();
1340 let mut finished_presence = Vec::new();
1341
1342 for ((target, property), motion) in state.active.iter_mut() {
1343 let elapsed = current_time.saturating_sub(motion.start_time);
1344 let mut progress = if motion.duration == 0 {
1345 1.0
1346 } else {
1347 elapsed as f32 / motion.duration as f32
1348 };
1349
1350 if motion.repeat && progress >= 1.0 {
1351 progress %= 1.0;
1352 } else {
1353 progress = progress.clamp(0.0, 1.0);
1354 }
1355
1356 if !motion.repeat && (elapsed >= motion.duration || motion.duration == 0) {
1357 finished.push((*target, property.clone()));
1358 }
1359
1360 let eased = motion.easing.apply(progress);
1361 let value = motion.start_value.interpolate(&motion.end_value, eased);
1362 if state.values.get(&(*target, property.clone())) != Some(&value) {
1363 state.values.insert((*target, property.clone()), value);
1364 changed.push((*target, property.clone()));
1365 }
1366 }
1367
1368 for key in finished {
1369 state.active.remove(&key);
1370 if state
1371 .presence
1372 .get(&key.0)
1373 .is_some_and(|phase| *phase == PresencePhase::Entering)
1374 {
1375 finished_presence.push((key.0, PresencePhase::Present));
1376 } else if state
1377 .presence
1378 .get(&key.0)
1379 .is_some_and(|phase| *phase == PresencePhase::Exiting)
1380 {
1381 finished_presence.push((key.0, PresencePhase::Hidden));
1382 }
1383 }
1384
1385 for (id, phase) in finished_presence {
1386 state.presence.insert(id, phase);
1387 }
1388
1389 changed
1390}
1391
1392fn sync_tracks(
1393 state: &mut MotionStateMap,
1394 id: WidgetId,
1395 tracks: &[MotionTrack],
1396 runtime: &crate::RuntimeState,
1397 layout: Option<&LayoutSnapshot>,
1398 now: CurrentTime,
1399 requested: &mut HashSet<(WidgetId, MotionPropertyId)>,
1400 result: &mut MotionSyncResult,
1401) {
1402 let self_rect = layout.and_then(|layout| layout.get_node_rect(id));
1403 let input = MotionEvalInput {
1404 runtime,
1405 layout,
1406 self_id: id,
1407 self_rect,
1408 pointer_local: None,
1409 };
1410 for track in tracks {
1411 let key = (id, track.property.clone());
1412 requested.insert(key.clone());
1413 let target_value = track.to.eval(&input);
1414 if let Some(active) = state.active.get(&key) {
1415 if active.end_value == target_value
1416 && active.duration == track.transition.duration_ms()
1417 && active.repeat == track.transition.repeat_enabled()
1418 && active.frame_interval_ms == track.transition.frame_interval_value_ms()
1419 && active.easing == track.transition.easing()
1420 {
1421 continue;
1422 }
1423 }
1424
1425 let current_value = state
1426 .values
1427 .get(&key)
1428 .cloned()
1429 .unwrap_or_else(|| track.property.default_value());
1430 if !track.transition.repeat_enabled()
1431 && state.values.contains_key(&key)
1432 && current_value == target_value
1433 {
1434 continue;
1435 }
1436
1437 let start_value = match &track.from {
1438 MotionStartValue::Explicit(expr) => expr.eval(&input),
1439 MotionStartValue::Current => current_value,
1440 };
1441
1442 if !track.transition.repeat_enabled() && start_value == target_value {
1443 let changed = state.values.get(&key) != Some(&target_value);
1444 state.values.insert(key.clone(), target_value);
1445 state.active.remove(&key);
1446 if changed {
1447 result.changed.push(key);
1448 }
1449 continue;
1450 }
1451
1452 state.values.insert(key.clone(), start_value.clone());
1453 state.active.insert(
1454 key.clone(),
1455 ActiveMotion {
1456 target: id,
1457 property: track.property.clone(),
1458 start_value,
1459 end_value: target_value,
1460 start_time: now + track.transition.delay_value_ms(),
1461 duration: track.transition.duration_ms(),
1462 repeat: track.transition.repeat_enabled(),
1463 frame_interval_ms: track.transition.frame_interval_value_ms(),
1464 easing: track.transition.easing(),
1465 },
1466 );
1467 result.changed.push(key);
1468 }
1469}
1470
1471pub fn scalar(value: f32) -> MotionExpr {
1473 MotionExpr::Value(MotionValue::Scalar(value))
1474}
1475
1476pub fn px(value: f32) -> MotionExpr {
1478 MotionExpr::Value(MotionValue::Px(value))
1479}
1480
1481pub fn deg(value: f32) -> MotionExpr {
1483 MotionExpr::Value(MotionValue::Deg(value))
1484}
1485
1486pub fn color(value: Color) -> MotionExpr {
1488 MotionExpr::Value(MotionValue::Color(value))
1489}
1490
1491pub fn fill(value: Fill) -> MotionExpr {
1493 MotionExpr::Value(MotionValue::Fill(value))
1494}
1495
1496pub fn shadows(value: Vec<BoxShadow>) -> MotionExpr {
1498 MotionExpr::Value(MotionValue::Shadows(value))
1499}
1500
1501pub fn fade() -> Vec<MotionTrack> {
1503 vec![MotionTrack::composite(
1504 MotionPropertyId::Opacity,
1505 MotionStartValue::Explicit(scalar(0.0)),
1506 scalar(1.0),
1507 )]
1508}
1509
1510pub fn slide_x(offset: f32) -> Vec<MotionTrack> {
1512 vec![MotionTrack::composite(
1513 MotionPropertyId::TranslateX,
1514 MotionStartValue::Explicit(px(offset)),
1515 px(0.0),
1516 )]
1517}
1518
1519pub fn slide_y(offset: f32) -> Vec<MotionTrack> {
1521 vec![MotionTrack::composite(
1522 MotionPropertyId::TranslateY,
1523 MotionStartValue::Explicit(px(offset)),
1524 px(0.0),
1525 )]
1526}
1527
1528pub fn collapse_x() -> Vec<MotionTrack> {
1530 vec![MotionTrack {
1531 property: MotionPropertyId::Width,
1532 phase: MotionPhase::Layout,
1533 from: MotionStartValue::Explicit(px(0.0)),
1534 to: MotionExpr::IntrinsicWidth,
1535 transition: MotionTransition::default(),
1536 }]
1537}
1538
1539pub fn collapse_y() -> Vec<MotionTrack> {
1541 vec![MotionTrack {
1542 property: MotionPropertyId::Height,
1543 phase: MotionPhase::Layout,
1544 from: MotionStartValue::Explicit(px(0.0)),
1545 to: MotionExpr::IntrinsicHeight,
1546 transition: MotionTransition::default(),
1547 }]
1548}
1549
1550pub fn follow_x_and_width(target: WidgetId) -> Vec<MotionTrack> {
1554 vec![
1555 MotionTrack::composite(
1556 MotionPropertyId::TranslateX,
1557 MotionStartValue::Current,
1558 MotionExpr::LayoutX(target),
1559 ),
1560 MotionTrack {
1561 property: MotionPropertyId::Width,
1562 phase: MotionPhase::Layout,
1563 from: MotionStartValue::Current,
1564 to: MotionExpr::LayoutWidth(target),
1565 transition: MotionTransition::default(),
1566 },
1567 ]
1568}
1569
1570pub fn hover_press(id: WidgetId) -> Vec<MotionTrack> {
1572 vec![MotionTrack::composite(
1573 MotionPropertyId::Scale,
1574 MotionStartValue::Current,
1575 MotionExpr::If {
1576 predicate: MotionPredicate::Pressed(id),
1577 then_expr: Box::new(scalar(0.97)),
1578 else_expr: Box::new(MotionExpr::If {
1579 predicate: MotionPredicate::Hovered(id),
1580 then_expr: Box::new(scalar(1.02)),
1581 else_expr: Box::new(scalar(1.0)),
1582 }),
1583 },
1584 )
1585 .transition(MotionTransition::spring(420.0, 30.0))]
1586}
1587
1588pub fn ripple_effect() -> RippleFx {
1590 RippleFx::default()
1591}
1592
1593pub fn presence(
1597 id: impl IntoMotionId,
1598 visible: bool,
1599 tracks: Vec<MotionTrack>,
1600 child: impl Into<Widget>,
1601) -> Widget {
1602 Presence {
1603 id: id.into_motion_id(),
1604 visible,
1605 enter: tracks.clone(),
1606 exit: reverse_tracks_for_exit(&tracks),
1607 child: child.into(),
1608 ..Default::default()
1609 }
1610 .into()
1611}
1612
1613pub fn appear(id: impl IntoMotionId, tracks: Vec<MotionTrack>, child: impl Into<Widget>) -> Widget {
1615 Motion {
1616 id: id.into_motion_id(),
1617 tracks,
1618 child: child.into(),
1619 ..Default::default()
1620 }
1621 .into()
1622}
1623
1624pub fn layout(id: impl IntoMotionId, tracks: Vec<MotionTrack>, child: impl Into<Widget>) -> Widget {
1626 appear(id, tracks, child)
1627}
1628
1629pub fn interactive(
1631 id: impl IntoMotionId,
1632 tracks: Vec<MotionTrack>,
1633 child: impl Into<Widget>,
1634) -> Widget {
1635 appear(id, tracks, child)
1636}
1637
1638pub fn ripple(id: impl IntoMotionId, effect: RippleFx, child: impl Into<Widget>) -> Widget {
1640 RippleLayer {
1641 id: id.into_motion_id(),
1642 effect,
1643 child: child.into(),
1644 }
1645 .into()
1646}
1647
1648pub fn reverse_tracks_for_exit(tracks: &[MotionTrack]) -> Vec<MotionTrack> {
1653 tracks
1654 .iter()
1655 .map(|track| MotionTrack {
1656 property: track.property.clone(),
1657 phase: track.phase,
1658 from: MotionStartValue::Current,
1659 to: match &track.from {
1660 MotionStartValue::Explicit(expr) => expr.clone(),
1661 MotionStartValue::Current => track.property.default_value().into(),
1662 },
1663 transition: track.transition.clone(),
1664 })
1665 .collect()
1666}
1667
1668pub fn dedupe_tracks_later_wins(tracks: Vec<MotionTrack>) -> Vec<MotionTrack> {
1673 let mut seen = HashSet::new();
1674 let mut out = Vec::with_capacity(tracks.len());
1675 for track in tracks.into_iter().rev() {
1676 if seen.insert((track.property.clone(), track.phase)) {
1677 out.push(track);
1678 }
1679 }
1680 out.reverse();
1681 out
1682}
1683
1684impl From<MotionValue> for MotionExpr {
1685 fn from(value: MotionValue) -> Self {
1686 Self::Value(value)
1687 }
1688}
1689
1690#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1691pub enum SurfaceMotion {
1697 Default,
1699 Fade,
1701 Scale,
1703 SlideX(f32),
1705 SlideY(f32),
1707 Pop,
1709 Composition(Vec<SurfaceMotion>),
1711 Custom {
1713 enter: Vec<MotionTrack>,
1715 exit: Vec<MotionTrack>,
1717 keep_rendered: bool,
1719 },
1720}
1721
1722impl SurfaceMotion {
1723 pub fn compose(items: impl IntoIterator<Item = Self>) -> Self {
1725 let mut out = Vec::new();
1726 for item in items {
1727 item.flatten_into(&mut out);
1728 }
1729 match out.len() {
1730 0 => Self::Composition(Vec::new()),
1731 1 => out.remove(0),
1732 _ => Self::Composition(out),
1733 }
1734 }
1735
1736 pub fn enter_tracks(&self) -> Vec<MotionTrack> {
1738 let mut out = Vec::new();
1739 self.append_enter_tracks(&mut out);
1740 dedupe_tracks_later_wins(out)
1741 }
1742
1743 pub fn exit_tracks(&self) -> Vec<MotionTrack> {
1745 match self {
1746 Self::Custom { exit, .. } => exit.clone(),
1747 _ => reverse_tracks_for_exit(&self.enter_tracks()),
1748 }
1749 }
1750
1751 pub fn keep_rendered(&self) -> bool {
1753 match self {
1754 Self::Custom { keep_rendered, .. } => *keep_rendered,
1755 Self::Composition(items) => items.iter().any(Self::keep_rendered),
1756 _ => false,
1757 }
1758 }
1759
1760 fn append_enter_tracks(&self, out: &mut Vec<MotionTrack>) {
1761 match self {
1762 Self::Default => {
1763 Self::Fade.append_enter_tracks(out);
1764 Self::Scale.append_enter_tracks(out);
1765 }
1766 Self::Fade => out.extend(fade()),
1767 Self::Scale => out.push(MotionTrack::composite(
1768 MotionPropertyId::Scale,
1769 MotionStartValue::Explicit(scalar(0.96)),
1770 scalar(1.0),
1771 )),
1772 Self::SlideX(offset) => out.extend(slide_x(*offset)),
1773 Self::SlideY(offset) => out.extend(slide_y(*offset)),
1774 Self::Pop => {
1775 Self::Fade.append_enter_tracks(out);
1776 Self::Scale.append_enter_tracks(out);
1777 }
1778 Self::Composition(items) => {
1779 for item in items {
1780 item.append_enter_tracks(out);
1781 }
1782 }
1783 Self::Custom { enter, .. } => out.extend(enter.clone()),
1784 }
1785 }
1786
1787 fn flatten_into(self, out: &mut Vec<Self>) {
1788 match self {
1789 Self::Composition(items) => {
1790 for item in items {
1791 item.flatten_into(out);
1792 }
1793 }
1794 item => out.push(item),
1795 }
1796 }
1797}
1798
1799impl Add for SurfaceMotion {
1800 type Output = Self;
1801
1802 fn add(self, rhs: Self) -> Self::Output {
1803 Self::compose([self, rhs])
1804 }
1805}
1806
1807fn numeric_binary(
1808 a: &MotionExpr,
1809 b: &MotionExpr,
1810 input: &MotionEvalInput<'_>,
1811 f: impl FnOnce(f32, f32) -> f32,
1812) -> MotionValue {
1813 let left = a.eval(input);
1814 let right = b.eval(input).as_scalar_like().unwrap_or(0.0);
1815 map_numeric(left, |left| f(left, right))
1816}
1817
1818fn numeric_unary(
1819 value: &MotionExpr,
1820 input: &MotionEvalInput<'_>,
1821 f: impl FnOnce(f32) -> f32,
1822) -> MotionValue {
1823 let value = value.eval(input);
1824 map_numeric(value, f)
1825}
1826
1827fn map_numeric(value: MotionValue, f: impl FnOnce(f32) -> f32) -> MotionValue {
1828 match value {
1829 MotionValue::Scalar(v) => MotionValue::Scalar(f(v)),
1830 MotionValue::Px(v) => MotionValue::Px(f(v)),
1831 MotionValue::Deg(v) => MotionValue::Deg(f(v)),
1832 MotionValue::Bool(_)
1833 | MotionValue::Color(_)
1834 | MotionValue::Fill(_)
1835 | MotionValue::Shadows(_) => value,
1836 }
1837}
1838
1839fn lerp(a: f32, b: f32, t: f32) -> f32 {
1840 a + (b - a) * t
1841}