1use std::{cell::RefCell, rc::Rc};
4
5use pax_manifest::ValueDefinition;
6use pax_runtime_api::{
7 properties::{current_frame, register_effect_property, PropertyValue},
8 CoercionRules, Duration, EasingCurve, Interpolatable, LocalProperty, PaxValue, ToPaxValue,
9};
10
11use crate::{
12 ExpandedNode, RuntimePropertiesStackFrame, RuntimeResolvedPropertyEntry, RuntimeSettingsSource,
13};
14
15#[derive(Clone, Debug, PartialEq)]
17pub struct SettingsTransitionConfig {
18 pub duration: Duration,
19 pub curve: &'static str,
20}
21impl Default for SettingsTransitionConfig {
22 fn default() -> Self {
23 Self {
24 duration: Duration::default(),
25 curve: "Linear",
26 }
27 }
28}
29impl Interpolatable for SettingsTransitionConfig {}
30
31#[derive(Clone, Default)]
32struct Owner {
33 imported: bool,
34 explicit: bool,
35 config: Option<SettingsTransitionConfig>,
36}
37impl Interpolatable for Owner {}
38
39fn owner_property(
40 entries: &[RuntimeResolvedPropertyEntry],
41 stack: &Rc<RuntimePropertiesStackFrame>,
42) -> LocalProperty<Owner> {
43 let mut dependencies = Vec::new();
44 for entry in entries {
45 if let Some(policy) = &entry.transition {
46 dependencies.push(policy.untyped());
47 }
48 if let Some(condition) = &entry.condition {
49 crate::cartridge::collect_settings_condition_dependencies(
50 condition,
51 entry.source_stack.as_ref().unwrap_or(stack),
52 &mut dependencies,
53 );
54 }
55 }
56 let entries = entries.to_vec();
57 let stack = stack.clone();
58 LocalProperty::computed_with_name(
59 move || {
60 entries
61 .iter()
62 .rev()
63 .find(|entry| {
64 entry.condition.as_ref().map_or(true, |condition| {
65 crate::cartridge::settings_condition_is_active(
66 condition,
67 entry.source_stack.as_ref().unwrap_or(&stack),
68 )
69 })
70 })
71 .map(|entry| Owner {
72 imported: matches!(entry.source, RuntimeSettingsSource::ImportedLayer { .. }),
73 explicit: matches!(entry.source, RuntimeSettingsSource::Inline)
74 || matches!(
75 entry.value,
76 ValueDefinition::Timeline(_)
77 | ValueDefinition::Transition(_)
78 | ValueDefinition::DoubleBinding(_)
79 ),
80 config: entry.transition.as_ref().and_then(LocalProperty::get),
81 })
82 .unwrap_or_default()
83 },
84 &dependencies,
85 "settings transition owner",
86 )
87}
88
89struct Endpoints<T> {
90 from: T,
91 to: T,
92 curve: &'static str,
93}
94
95struct SettingsMotion<T: PropertyValue> {
96 target: LocalProperty<T>,
97 owner: LocalProperty<Owner>,
98 progress: LocalProperty<f64>,
99 _listener: LocalProperty<()>,
100}
101
102impl<T: PropertyValue> Drop for SettingsMotion<T> {
103 fn drop(&mut self) {
104 self.progress.cancel_transitions();
105 }
106}
107
108fn frozen<T: PropertyValue + CoercionRules + ToPaxValue>(
109 value: T,
110) -> Result<(T, PaxValue), String> {
111 let snapshot = value.to_pax_value_in_graph();
114 T::try_coerce(snapshot.clone()).map(|value| (value, snapshot))
115}
116
117impl<T: PropertyValue + CoercionRules + ToPaxValue> SettingsMotion<T> {
118 fn new(
119 output: &LocalProperty<T>,
120 target: LocalProperty<T>,
121 owner: LocalProperty<Owner>,
122 animate_initial_change: bool,
123 interpolate: Rc<dyn Fn(&T, &T, f64) -> T>,
124 ) -> Option<Self> {
125 let (initial, initial_snapshot) = frozen(output.get()).ok()?;
126 let progress = LocalProperty::new_with_name(1.0, "settings transition progress");
127 let revision = LocalProperty::new(0_u64);
128 let endpoints = Rc::new(RefCell::new(Endpoints {
129 from: initial.clone(),
130 to: initial,
131 curve: "Linear",
132 }));
133 let animation = {
134 let progress = progress.clone();
135 let endpoints = endpoints.clone();
136 let dependencies = [progress.untyped(), revision.untyped()];
137 LocalProperty::computed_with_name(
138 move || {
139 let t = progress.get();
140 let endpoints = endpoints.borrow();
141 if t >= 1.0 {
142 endpoints.to.clone()
143 } else {
144 let multiplier =
145 crate::cartridge::easing_curve_from_name(Some(endpoints.curve))
146 .interpolate(&0.0, &1.0, t);
147 interpolate(&endpoints.from, &endpoints.to, multiplier)
148 }
149 },
150 &dependencies,
151 "animated settings value",
152 )
153 };
154 output.cancel_transitions();
155 output.replace_with(animation);
156 let listener = {
157 let target = target.clone();
158 let owner = owner.clone();
159 let output = output.clone();
160 let progress = progress.clone();
161 let previous =
162 RefCell::new((initial_snapshot, Owner::default(), !animate_initial_change));
163 let dependencies = [target.untyped(), owner.untyped()];
164 LocalProperty::computed_with_name(
165 move || {
166 let new_owner = owner.get();
167 let next = target.get();
168 let snapshot = next.clone().to_pax_value_in_graph();
169 let (next, can_interpolate) = match T::try_coerce(snapshot.clone()) {
170 Ok(frozen) => (frozen, true),
171 Err(_) => (next, false),
174 };
175 let mut previous = previous.borrow_mut();
176 let config = if new_owner.explicit {
177 None
178 } else if new_owner.imported {
179 new_owner.config.clone()
180 } else if previous.1.imported {
181 previous.1.config.clone()
182 } else {
183 None
184 };
185 let changed = snapshot != previous.0;
186 let disabled = new_owner.explicit
187 || (new_owner.imported
188 && new_owner.config.as_ref().map_or(true, |config| {
189 let duration = config.duration.raw_value();
190 !duration.is_finite() || duration <= 0.0
191 }));
192 if !changed && !disabled && !previous.2 {
193 previous.1 = new_owner;
194 return;
195 }
196 let current = frozen(output.get())
198 .map(|(value, _)| value)
199 .unwrap_or_else(|_| next.clone());
200 progress.cancel_transitions();
201 let config = config.filter(|config| {
202 let duration = match config.duration {
203 Duration::Frames(v)
204 | Duration::Milliseconds(v)
205 | Duration::Seconds(v) => v.to_float(),
206 };
207 can_interpolate && duration.is_finite() && duration > 0.0
208 });
209 *endpoints.borrow_mut() = Endpoints {
210 from: current,
211 to: next,
212 curve: config.as_ref().map_or("Linear", |config| config.curve),
213 };
214 if changed && !previous.2 {
215 if let Some(config) = config {
216 progress.set(0.0);
217 progress.ease_to(1.0, config.duration, EasingCurve::Linear);
218 } else {
219 progress.set(1.0);
220 }
221 } else {
222 progress.set(1.0);
223 }
224 revision.set(revision.get().wrapping_add(1));
225 *previous = (snapshot, new_owner, false);
226 },
227 &dependencies,
228 "settings target listener",
229 )
230 };
231 register_effect_property(&listener);
232 listener.get();
234 Some(Self {
235 target,
236 owner,
237 progress,
238 _listener: listener,
239 })
240 }
241}
242
243#[doc(hidden)]
246pub fn bind_settings_property<T: PropertyValue + CoercionRules + ToPaxValue>(
247 output: &LocalProperty<T>,
248 target: LocalProperty<T>,
249 name: &str,
250 entries: &[RuntimeResolvedPropertyEntry],
251 stack: &Rc<RuntimePropertiesStackFrame>,
252 node: Option<&ExpandedNode>,
253) {
254 bind_settings_property_with(output, target, name, entries, stack, node, T::interpolate);
255}
256
257pub(crate) fn bind_settings_property_with<T: PropertyValue + CoercionRules + ToPaxValue>(
258 output: &LocalProperty<T>,
259 target: LocalProperty<T>,
260 name: &str,
261 entries: &[RuntimeResolvedPropertyEntry],
262 stack: &Rc<RuntimePropertiesStackFrame>,
263 node: Option<&ExpandedNode>,
264 interpolate: impl Fn(&T, &T, f64) -> T + 'static,
265) {
266 let Some(node) = node else {
267 output.replace_with(target);
268 return;
269 };
270 let mut motions = node.settings_motion.borrow_mut();
271 let explicit = entries.last().map_or(false, |entry| {
273 entry.condition.is_none()
274 && (matches!(entry.source, RuntimeSettingsSource::Inline)
275 || matches!(
276 entry.value,
277 ValueDefinition::Timeline(_)
278 | ValueDefinition::Transition(_)
279 | ValueDefinition::DoubleBinding(_)
280 ))
281 });
282 if explicit {
283 motions.remove(name);
284 output.replace_with(target);
285 return;
286 }
287 if let Some(motion) = motions
288 .get_mut(name)
289 .and_then(|motion| motion.downcast_mut::<SettingsMotion<T>>())
290 {
291 motion.target.replace_with(target);
292 motion.owner.replace_with(owner_property(entries, stack));
293 return;
294 }
295 let enabled = entries.iter().any(|entry| {
296 entry
297 .transition
298 .as_ref()
299 .and_then(LocalProperty::get)
300 .is_some()
301 });
302 if !enabled {
303 output.replace_with(target);
304 return;
305 }
306 let owner = owner_property(entries, stack);
307 if let Some(motion) = SettingsMotion::new(
308 output,
309 target.clone(),
310 owner,
311 current_frame() != node.settings_birth_frame,
312 Rc::new(interpolate),
313 ) {
314 motions.insert(name.to_owned(), Box::new(motion));
315 } else {
316 output.replace_with(target);
317 }
318}
319
320#[doc(hidden)]
322pub fn clear_settings_motion(node: Option<&ExpandedNode>, name: &str) {
323 if let Some(node) = node {
324 node.settings_motion.borrow_mut().remove(name);
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use pax_runtime_api::properties::{
332 drain_effects, property_has_direct_outbound, register_millis, register_time,
333 };
334
335 fn clock() -> LocalProperty<u64> {
336 register_time(&LocalProperty::new(0));
337 let millis = LocalProperty::new(0);
338 register_millis(&millis);
339 millis
340 }
341
342 fn owner() -> LocalProperty<Owner> {
343 LocalProperty::new(Owner {
344 imported: true,
345 explicit: false,
346 config: Some(SettingsTransitionConfig {
347 duration: Duration::Milliseconds(100.into()),
348 curve: "Linear",
349 }),
350 })
351 }
352
353 fn settle() {
354 drain_effects(100);
355 }
356
357 #[test]
358 fn retarget_samples_current_clock_and_restarts_full_duration() {
359 let millis = clock();
360 let output = LocalProperty::new(0.0);
361 let source = LocalProperty::new(0.0);
362 let motion = SettingsMotion::new(
363 &output,
364 source.clone(),
365 owner(),
366 false,
367 Rc::new(f64::interpolate),
368 )
369 .unwrap();
370 source.set(10.0);
371 settle();
372 millis.set(40);
373 source.set(20.0);
375 settle();
376 assert_eq!(output.get(), 4.0);
377 millis.set(90);
378 assert_eq!(output.get(), 12.0);
379 millis.set(140);
380 assert_eq!(output.get(), 20.0);
381 millis.set(141);
382 assert_eq!(output.get(), 20.0);
383 assert!(!property_has_direct_outbound(
384 &millis.untyped(),
385 &motion.progress.untyped()
386 ));
387 source.set(0.0);
389 settle();
390 millis.set(191);
391 assert_eq!(output.get(), 10.0);
392 }
393
394 #[test]
395 fn initial_value_and_equal_targets_do_not_animate_or_restart() {
396 let millis = clock();
397 let output = LocalProperty::new(0.0);
398 let source = LocalProperty::new(10.0);
399 let motion = SettingsMotion::new(
400 &output,
401 source.clone(),
402 owner(),
403 false,
404 Rc::new(f64::interpolate),
405 )
406 .unwrap();
407 assert_eq!(output.get(), 10.0);
408 assert!(!property_has_direct_outbound(
409 &millis.untyped(),
410 &motion.progress.untyped()
411 ));
412 source.set(20.0);
413 settle();
414 millis.set(40);
415 source.set(20.0);
416 settle();
417 millis.set(100);
418 assert_eq!(output.get(), 20.0);
419 }
420
421 #[test]
422 fn rebind_preserves_motion_and_instances_have_independent_clocks() {
423 let millis = clock();
424 let a = LocalProperty::new(0.0);
425 let b = LocalProperty::new(0.0);
426 let a_target = LocalProperty::new(0.0);
427 let b_target = LocalProperty::new(0.0);
428 let first = SettingsMotion::new(
429 &a,
430 a_target.clone(),
431 owner(),
432 false,
433 Rc::new(f64::interpolate),
434 )
435 .unwrap();
436 let _second = SettingsMotion::new(
437 &b,
438 b_target.clone(),
439 owner(),
440 false,
441 Rc::new(f64::interpolate),
442 )
443 .unwrap();
444 a_target.set(10.0);
445 b_target.set(10.0);
446 settle();
447 millis.set(40);
448 first.target.replace_with(LocalProperty::new(20.0));
449 settle();
450 millis.set(90);
451 assert_eq!(a.get(), 12.0);
452 assert_eq!(b.get(), 9.0);
453 }
454
455 #[test]
456 fn disabling_snaps_and_detaches_clock() {
457 let millis = clock();
458 let output = LocalProperty::new(0.0);
459 let target = LocalProperty::new(0.0);
460 let policy = owner();
461 let motion = SettingsMotion::new(
462 &output,
463 target.clone(),
464 policy.clone(),
465 false,
466 Rc::new(f64::interpolate),
467 )
468 .unwrap();
469 target.set(10.0);
470 settle();
471 millis.set(40);
472 policy.update(|policy| policy.config = None);
473 settle();
474 assert_eq!(output.get(), 10.0);
475 assert!(!property_has_direct_outbound(
476 &millis.untyped(),
477 &motion.progress.untyped()
478 ));
479 }
480
481 #[test]
482 fn easing_overshoot_is_not_mistaken_for_completion() {
483 let millis = clock();
484 let output = LocalProperty::new(0.0);
485 let target = LocalProperty::new(0.0);
486 let policy = owner();
487 policy.update(|policy| policy.config.as_mut().unwrap().curve = "OutBack");
488 let _motion = SettingsMotion::new(
489 &output,
490 target.clone(),
491 policy,
492 false,
493 Rc::new(f64::interpolate),
494 )
495 .unwrap();
496 target.set(10.0);
497 settle();
498 millis.set(70);
499 assert!(output.get() > 10.0);
500 millis.set(100);
501 assert_eq!(output.get(), 10.0);
502 }
503
504 #[test]
505 fn compound_endpoints_do_not_keep_live_property_handles() {
506 let graph = pax_runtime_api::properties::PropertyGraph::new(|| {});
507 let _entered = graph.enter();
508 use pax_runtime_api::{Size, Stroke};
509 let millis = clock();
510 let width = LocalProperty::new(Size::Pixels(10.into()));
511 let source = {
512 let width = width.clone();
513 let dependencies = [width.untyped()];
514 LocalProperty::computed(
515 move || {
516 let _ = width.get();
517 Stroke {
518 width: pax_runtime_api::PropertyBinding::from_local(width.clone()),
519 ..Stroke::default()
520 }
521 },
522 &dependencies,
523 )
524 };
525 let output = LocalProperty::new(Stroke::default());
526 let _motion = SettingsMotion::new(
527 &output,
528 source,
529 owner(),
530 false,
531 Rc::new(Stroke::interpolate),
532 )
533 .unwrap();
534 width.set(Size::Pixels(20.into()));
535 settle();
536 millis.set(50);
537 assert_eq!(output.get().width.get().expect_pixels().to_float(), 15.0);
538 width.set(Size::Pixels(40.into()));
539 settle();
540 assert_eq!(output.get().width.get().expect_pixels().to_float(), 15.0);
541 millis.set(100);
542 assert_eq!(output.get().width.get().expect_pixels().to_float(), 27.5);
543 }
544
545 #[test]
546 fn zero_duration_configuration_applies_pending_target_immediately() {
547 let millis = clock();
548 let output = LocalProperty::new(0.0);
549 let source = LocalProperty::new(0.0);
550 let policy = owner();
551 let motion = SettingsMotion::new(
552 &output,
553 source.clone(),
554 policy.clone(),
555 false,
556 Rc::new(f64::interpolate),
557 )
558 .unwrap();
559 source.set(10.0);
560 settle();
561 millis.set(40);
562 policy.update(|owner| {
563 owner.config.as_mut().unwrap().duration = Duration::Milliseconds(0.into())
564 });
565 settle();
566 assert_eq!(output.get(), 10.0);
567 assert!(!property_has_direct_outbound(
568 &millis.untyped(),
569 &motion.progress.untyped()
570 ));
571 }
572
573 #[test]
574 fn conditional_winner_controls_motion_not_the_last_structural_entry() {
575 use pax_manifest::{ExpressionInfo, TypeId};
576 use pax_runtime_api::Variable;
577 use std::collections::HashMap;
578 let millis = clock();
579 let condition = LocalProperty::new(false);
580 let base = LocalProperty::new(0.0);
581 let imported = LocalProperty::new(10.0);
582 let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
583 "enabled".into(),
584 Variable::new_from_typed_property(condition.clone()),
585 )]));
586 let condition_info = crate::RuntimeSettingsCondition {
587 positive: vec![ExpressionInfo::new(
588 pax_language::parse_pax_expression("enabled").unwrap(),
589 )],
590 negative: vec![],
591 };
592 let entries = vec![RuntimeResolvedPropertyEntry {
593 source: RuntimeSettingsSource::ImportedLayer {
594 provider_id: crate::ExpandedNodeIdentifier(1),
595 provider_type_id: TypeId::default(),
596 },
597 transition: Some(LocalProperty::new(owner().get().config)),
598 selector: None,
599 source_location: None,
600 source_stack: Some(stack.clone()),
601 value: ValueDefinition::Undefined,
602 condition: Some(condition_info.clone()),
603 axis_index: None,
604 }];
605 let target = crate::apply_runtime_settings_condition(
606 "value",
607 Some(&condition_info),
608 &stack,
609 base.clone(),
610 imported.clone(),
611 );
612 let output = LocalProperty::new(0.0);
613 let _motion = SettingsMotion::new(
614 &output,
615 target,
616 owner_property(&entries, &stack),
617 false,
618 Rc::new(f64::interpolate),
619 )
620 .unwrap();
621 base.set(2.0);
622 imported.set(20.0);
623 settle();
624 assert_eq!(output.get(), 2.0); condition.set(true);
626 settle();
627 millis.set(50);
628 assert_eq!(output.get(), 11.0);
629 condition.set(false);
630 settle();
631 millis.set(100);
632 assert_eq!(output.get(), 6.5); }
634}