1use std::cell::RefCell;
2use std::collections::hash_map::DefaultHasher;
3use std::hash::{Hash, Hasher};
4
5use crate::{Box, ViewExt, ZStack};
6use repose_core::*;
7
8use crate::anim::animate_f32_from;
9use crate::anim::animate_vec2_from;
10
11pub type ExpandFrom = f32;
13
14#[derive(Clone, Debug)]
16pub enum EnterTransition {
17 FadeIn,
19 SlideIn { offset_x: f32, offset_y: f32 },
21 ScaleIn { initial: f32 },
23 ExpandVertically {
26 clip: bool,
28 expand_from: ExpandFrom,
30 },
31 ExpandHorizontally {
33 clip: bool,
34 expand_from: ExpandFrom,
36 },
37 ExpandIn { clip: bool },
39 Composite(Vec<EnterTransition>),
41}
42
43impl Default for EnterTransition {
44 fn default() -> Self {
45 Self::fade_in().and(Self::expand_vertically())
46 }
47}
48
49impl EnterTransition {
50 pub fn fade_in() -> Self {
51 Self::FadeIn
52 }
53 pub fn expand_vertically() -> Self {
54 Self::ExpandVertically {
55 clip: true,
56 expand_from: 0.0,
57 }
58 }
59 pub fn expand_horizontally() -> Self {
60 Self::ExpandHorizontally {
61 clip: true,
62 expand_from: 0.0,
63 }
64 }
65 pub fn expand_in() -> Self {
66 Self::ExpandIn { clip: true }
67 }
68 pub fn slide_in(offset_x: f32, offset_y: f32) -> Self {
69 Self::SlideIn { offset_x, offset_y }
70 }
71 pub fn scale_in(initial: f32) -> Self {
72 Self::ScaleIn { initial }
73 }
74 pub fn and(self, other: Self) -> Self {
76 match (self, other) {
77 (Self::Composite(mut a), Self::Composite(b)) => {
78 a.extend(b);
79 Self::Composite(a)
80 }
81 (Self::Composite(mut a), b) => {
82 a.push(b);
83 Self::Composite(a)
84 }
85 (a, Self::Composite(mut b)) => {
86 let mut v = vec![a];
87 v.append(&mut b);
88 Self::Composite(v)
89 }
90 (a, b) => Self::Composite(vec![a, b]),
91 }
92 }
93}
94
95#[derive(Clone, Debug)]
97pub enum ExitTransition {
98 FadeOut,
100 SlideOut { offset_x: f32, offset_y: f32 },
102 ScaleOut { target: f32 },
104 ShrinkVertically {
106 clip: bool,
107 shrink_towards: ExpandFrom,
109 },
110 ShrinkHorizontally {
112 clip: bool,
113 shrink_towards: ExpandFrom,
114 },
115 ShrinkOut { clip: bool },
117 Composite(Vec<ExitTransition>),
119}
120
121impl Default for ExitTransition {
122 fn default() -> Self {
123 Self::fade_out().and(Self::shrink_vertically())
124 }
125}
126
127impl ExitTransition {
128 pub fn fade_out() -> Self {
129 Self::FadeOut
130 }
131 pub fn shrink_vertically() -> Self {
132 Self::ShrinkVertically {
133 clip: true,
134 shrink_towards: 0.0,
135 }
136 }
137 pub fn shrink_horizontally() -> Self {
138 Self::ShrinkHorizontally {
139 clip: true,
140 shrink_towards: 0.0,
141 }
142 }
143 pub fn shrink_out() -> Self {
144 Self::ShrinkOut { clip: true }
145 }
146 pub fn slide_out(offset_x: f32, offset_y: f32) -> Self {
147 Self::SlideOut { offset_x, offset_y }
148 }
149 pub fn scale_out(target: f32) -> Self {
150 Self::ScaleOut { target }
151 }
152 pub fn and(self, other: Self) -> Self {
154 match (self, other) {
155 (Self::Composite(mut a), Self::Composite(b)) => {
156 a.extend(b);
157 Self::Composite(a)
158 }
159 (Self::Composite(mut a), b) => {
160 a.push(b);
161 Self::Composite(a)
162 }
163 (a, Self::Composite(mut b)) => {
164 let mut v = vec![a];
165 v.append(&mut b);
166 Self::Composite(v)
167 }
168 (a, b) => Self::Composite(vec![a, b]),
169 }
170 }
171}
172
173#[derive(Clone)]
174pub struct CrossfadeConfig {
175 pub key: String,
176 pub spec: AnimationSpec,
177}
178
179impl Default for CrossfadeConfig {
180 fn default() -> Self {
181 Self {
182 key: "crossfade".into(),
183 spec: AnimationSpec::default(),
184 }
185 }
186}
187
188pub fn Crossfade<T, F>(target: T, config: CrossfadeConfig, content: F) -> View
193where
194 T: PartialEq + Clone + 'static,
195 F: Fn(T) -> View + 'static,
196{
197 let key = config.key;
198 let spec = config.spec;
199
200 let prev = remember_with_key(format!("cf_prev:{key}"), || RefCell::new(target.clone()));
201 let old_content =
202 remember_with_key(format!("cf_old_view:{key}"), || RefCell::new(None::<View>));
203 let version = remember_with_key(format!("cf_version:{key}"), || RefCell::new(0u64));
204
205 let is_new = *prev.borrow() != target;
206 if is_new {
207 let old_ver = *version.borrow();
208 let mut prev_view = content(prev.borrow().clone());
209 prev_view.scope_key = Some(format!("cf_{key}_old_v{old_ver}"));
210 prev_view.modifier.repaint_boundary = true;
211 old_content.borrow_mut().replace(prev_view);
212 prev.borrow_mut().clone_from(&target);
213 *version.borrow_mut() += 1;
214 }
215
216 let v = *version.borrow();
217 let mut new_view = content(target.clone());
218 new_view.scope_key = Some(format!("cf_{key}_v{v}"));
219 new_view.modifier.repaint_boundary = true;
220
221 let old_view = {
223 let mut oc = old_content.borrow_mut();
224 if let Some(ref ov) = *oc {
225 let exit_alpha = animate_f32_from(format!("cf_exit:{key}:v{v}"), 1.0, 0.0, spec);
226 if exit_alpha > 0.005 {
227 let mut exit_box =
228 Box(Modifier::new().fill_max_size().alpha(exit_alpha)).child(ov.clone());
229 exit_box.modifier.key = Some(transition_child_key(&key, v, "cf_exit"));
230 Some(exit_box)
231 } else {
232 *oc = None;
233 None
234 }
235 } else {
236 None
237 }
238 };
239
240 let enter_alpha = animate_f32_from(format!("cf_enter:{key}:v{v}"), 0.0, 1.0, spec);
242 let mut enter_box = Box(Modifier::new().fill_max_size().alpha(enter_alpha)).child(new_view);
243 enter_box.modifier.key = Some(transition_child_key(&key, v, "cf_enter"));
244
245 match old_view {
246 Some(ov) => ZStack(Modifier::new().fill_max_size()).child((ov, enter_box)),
247 None => enter_box,
248 }
249}
250
251#[derive(Clone)]
252pub struct AnimatedContentConfig {
253 pub key: String,
254 pub spec: AnimationSpec,
255 pub enter: EnterTransition,
256 pub exit: ExitTransition,
257}
258
259impl Default for AnimatedContentConfig {
260 fn default() -> Self {
261 Self {
262 key: "anim_content".into(),
263 spec: AnimationSpec::default(),
264 enter: EnterTransition::FadeIn,
265 exit: ExitTransition::FadeOut,
266 }
267 }
268}
269
270fn transition_child_key(key: &str, version: u64, tag: &str) -> u64 {
272 let mut h = DefaultHasher::new();
273 key.hash(&mut h);
274 version.hash(&mut h);
275 tag.hash(&mut h);
276 h.finish()
277}
278
279fn flow_mod() -> Modifier {
283 Modifier::new().fill_max_width()
284}
285
286#[derive(Clone, Copy)]
288enum SizeAxis {
289 Vertical,
290 Horizontal,
291 Both,
292}
293
294#[allow(clippy::too_many_arguments)]
302fn apply_size_fraction(
303 measure_key: &str,
304 anim_key: &str,
305 axis: SizeAxis,
306 from: f32,
307 to: f32,
308 clip: bool,
309 align: f32,
310 spec: AnimationSpec,
311 view: View,
312) -> View {
313 let full_w = remember_mutable_with_key(format!("{measure_key}:w"), || 0.0f32);
314 let full_h = remember_mutable_with_key(format!("{measure_key}:h"), || 0.0f32);
315
316 let have = match axis {
317 SizeAxis::Vertical => *full_h.get() > 0.5,
318 SizeAxis::Horizontal => *full_w.get() > 0.5,
319 SizeAxis::Both => *full_w.get() > 0.5 && *full_h.get() > 0.5,
320 };
321
322 let progress = if have {
323 animate_f32_from(anim_key, from, to, spec)
324 } else {
325 from
326 };
327
328 let capture = {
329 let fw = full_w.clone();
330 let fh = full_h.clone();
331 move |sz: Vec2| {
332 if sz.x > 0.5 {
333 fw.set_neq(sz.x);
334 }
335 if sz.y > 0.5 {
336 fh.set_neq(sz.y);
337 }
338 }
339 };
340
341 let settled = from < to && progress >= 0.999;
342
343 let full_w = *full_w.get();
344 let full_h = *full_h.get();
345
346 let shown_w = match axis {
347 SizeAxis::Vertical => full_w,
348 SizeAxis::Horizontal | SizeAxis::Both => full_w * progress,
349 };
350 let shown_h = match axis {
351 SizeAxis::Horizontal => full_h,
352 SizeAxis::Vertical | SizeAxis::Both => full_h * progress,
353 };
354
355 let mut outer = Modifier::new().fill_max_width().flex_shrink(0.0);
356 if !have || settled {
357 outer = outer.on_size_changed(capture);
362 } else {
363 match axis {
364 SizeAxis::Vertical => {
365 outer = outer.height(shown_h.max(0.0));
366 }
367 SizeAxis::Horizontal => {
368 outer = outer.width(shown_w.max(0.0));
369 }
370 SizeAxis::Both => {
371 outer = outer.size(shown_w.max(0.0), shown_h.max(0.0));
372 }
373 }
374 }
375 if clip {
376 outer = outer.overflow(repose_core::Overflow::Clip);
377 }
378
379 let align = align.clamp(0.0, 1.0);
383 let ox = match axis {
384 SizeAxis::Vertical => 0.0,
385 SizeAxis::Horizontal | SizeAxis::Both => (shown_w - full_w) * align,
386 };
387 let oy = match axis {
388 SizeAxis::Horizontal => 0.0,
389 SizeAxis::Vertical | SizeAxis::Both => (shown_h - full_h) * align,
390 };
391 let mut inner = Modifier::new().fill_max_width().flex_shrink(0.0);
392 if have && !settled {
393 match axis {
394 SizeAxis::Vertical => {
395 inner = inner.height(full_h);
396 }
397 SizeAxis::Horizontal => {
398 inner = inner.width(full_w);
399 }
400 SizeAxis::Both => {
401 inner = inner.size(full_w, full_h);
402 }
403 }
404 }
405 if ox.abs() > 0.01 || oy.abs() > 0.01 {
406 inner = inner.translate(dp_to_px(ox), dp_to_px(oy));
407 }
408
409 Box(outer).child(Box(inner).child(view))
410}
411
412pub fn AnimatedContent<T, F>(target_state: T, content: F, config: AnimatedContentConfig) -> View
419where
420 T: PartialEq + Clone + 'static,
421 F: Fn(T) -> View + 'static,
422{
423 let key = config.key;
424 let spec = config.spec;
425 let enter = config.enter;
426 let exit = config.exit;
427
428 let prev = remember_with_key(format!("ac_prev:{key}"), || {
429 RefCell::new(target_state.clone())
430 });
431 let old_content =
432 remember_with_key(format!("ac_old_view:{key}"), || RefCell::new(None::<View>));
433 let version = remember_with_key(format!("ac_version:{key}"), || RefCell::new(0u64));
434
435 let is_new = *prev.borrow() != target_state;
436 if is_new {
437 let old_ver = *version.borrow();
438 let mut prev_view = content(prev.borrow().clone());
439 prev_view.scope_key = Some(format!("ac_{key}_old_v{old_ver}"));
440 prev_view.modifier.repaint_boundary = true;
441 old_content.borrow_mut().replace(prev_view);
442 prev.borrow_mut().clone_from(&target_state);
443 *version.borrow_mut() += 1;
444 }
445
446 let v = *version.borrow();
447 let mut new_view = content(target_state.clone());
448 new_view.scope_key = Some(format!("ac_{key}_v{v}"));
449 new_view.modifier.repaint_boundary = true;
450 let mut new_view = apply_enter(&key, v, &enter, &spec, new_view);
451 new_view.modifier.key = Some(transition_child_key(&key, v, "ac_enter"));
452
453 let old_view = {
454 let mut oc = old_content.borrow_mut();
455 if let Some(ref ov) = *oc {
456 if exit_animation_done(&key, v, &exit) {
458 *oc = None;
459 None
460 } else {
461 let mut exit_view = apply_exit(&key, v, &exit, &spec, ov.clone());
462 exit_view.modifier.key = Some(transition_child_key(&key, v, "ac_exit"));
463 Some(exit_view)
464 }
465 } else {
466 None
467 }
468 };
469
470 match old_view {
471 Some(ov) => ZStack(Modifier::new().fill_max_size()).child((ov, new_view)),
472 None => new_view,
473 }
474}
475
476fn apply_enter(
477 key: &str,
478 version: u64,
479 enter: &EnterTransition,
480 spec: &AnimationSpec,
481 view: View,
482) -> View {
483 match enter {
484 EnterTransition::FadeIn => {
485 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
486 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
487 }
488 EnterTransition::SlideIn { offset_x, offset_y } => {
489 let offset = animate_vec2_from(
490 format!("{key}:v{version}:enter:slide"),
491 Vec2 {
492 x: dp_to_px(*offset_x),
493 y: dp_to_px(*offset_y),
494 },
495 Vec2::default(),
496 *spec,
497 );
498 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
499 }
500 EnterTransition::ScaleIn { initial } => {
501 let s = animate_f32_from(
502 format!("{key}:v{version}:enter:scale"),
503 *initial,
504 1.0,
505 *spec,
506 );
507 let a = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
508 Box(Modifier::new().fill_max_size().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
509 }
510 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
511 &format!("{key}:meas"),
512 &format!("{key}:v{version}:enter:expand_v"),
513 SizeAxis::Vertical,
514 0.0,
515 1.0,
516 *clip,
517 *expand_from,
518 *spec,
519 view,
520 ),
521 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
522 &format!("{key}:meas"),
523 &format!("{key}:v{version}:enter:expand_h"),
524 SizeAxis::Horizontal,
525 0.0,
526 1.0,
527 *clip,
528 *expand_from,
529 *spec,
530 view,
531 ),
532 EnterTransition::ExpandIn { clip } => apply_size_fraction(
533 &format!("{key}:meas"),
534 &format!("{key}:v{version}:enter:expand_in"),
535 SizeAxis::Both,
536 0.0,
537 1.0,
538 *clip,
539 0.0,
540 *spec,
541 view,
542 ),
543 EnterTransition::Composite(transitions) => {
544 let mut v = view;
545 for t in transitions {
546 v = apply_enter_single(key, version, t, spec, v);
547 }
548 v
549 }
550 }
551}
552
553fn apply_enter_single(
554 key: &str,
555 version: u64,
556 enter: &EnterTransition,
557 spec: &AnimationSpec,
558 view: View,
559) -> View {
560 match enter {
561 EnterTransition::FadeIn => {
562 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
563 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
564 }
565 EnterTransition::SlideIn { offset_x, offset_y } => {
566 let offset = animate_vec2_from(
567 format!("{key}:v{version}:enter:slide"),
568 Vec2 {
569 x: dp_to_px(*offset_x),
570 y: dp_to_px(*offset_y),
571 },
572 Vec2::default(),
573 *spec,
574 );
575 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
576 }
577 EnterTransition::ScaleIn { initial } => {
578 let s = animate_f32_from(
579 format!("{key}:v{version}:enter:scale"),
580 *initial,
581 1.0,
582 *spec,
583 );
584 Box(Modifier::new().fill_max_size().transform_origin(0.5, 0.5).scale(s)).child(view)
585 }
586 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
587 &format!("{key}:meas"),
588 &format!("{key}:v{version}:enter:expand_v"),
589 SizeAxis::Vertical,
590 0.0,
591 1.0,
592 *clip,
593 *expand_from,
594 *spec,
595 view,
596 ),
597 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
598 &format!("{key}:meas"),
599 &format!("{key}:v{version}:enter:expand_h"),
600 SizeAxis::Horizontal,
601 0.0,
602 1.0,
603 *clip,
604 *expand_from,
605 *spec,
606 view,
607 ),
608 EnterTransition::ExpandIn { clip } => apply_size_fraction(
609 &format!("{key}:meas"),
610 &format!("{key}:v{version}:enter:expand_in"),
611 SizeAxis::Both,
612 0.0,
613 1.0,
614 *clip,
615 0.0,
616 *spec,
617 view,
618 ),
619 EnterTransition::Composite(inner) => {
620 let mut v = view;
621 for t in inner {
622 v = apply_enter_single(key, version, t, spec, v);
623 }
624 v
625 }
626 }
627}
628
629fn apply_exit(
630 key: &str,
631 version: u64,
632 exit: &ExitTransition,
633 spec: &AnimationSpec,
634 view: View,
635) -> View {
636 match exit {
637 ExitTransition::FadeOut => {
638 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
639 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
640 }
641 ExitTransition::SlideOut { offset_x, offset_y } => {
642 let offset = animate_vec2_from(
643 format!("{key}:v{version}:exit:slide"),
644 Vec2::default(),
645 Vec2 {
646 x: dp_to_px(*offset_x),
647 y: dp_to_px(*offset_y),
648 },
649 *spec,
650 );
651 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
652 }
653 ExitTransition::ScaleOut { target } => {
654 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
655 let a = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
656 Box(Modifier::new().fill_max_size().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
657 }
658 ExitTransition::ShrinkVertically { clip, shrink_towards } => apply_size_fraction(
659 &format!("{key}:meas"),
660 &format!("{key}:v{version}:exit:expand_v"),
661 SizeAxis::Vertical,
662 1.0,
663 0.0,
664 *clip,
665 *shrink_towards,
666 *spec,
667 view,
668 ),
669 ExitTransition::ShrinkHorizontally { clip, shrink_towards } => apply_size_fraction(
670 &format!("{key}:meas"),
671 &format!("{key}:v{version}:exit:expand_h"),
672 SizeAxis::Horizontal,
673 1.0,
674 0.0,
675 *clip,
676 *shrink_towards,
677 *spec,
678 view,
679 ),
680 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
681 &format!("{key}:meas"),
682 &format!("{key}:v{version}:exit:expand_in"),
683 SizeAxis::Both,
684 1.0,
685 0.0,
686 *clip,
687 0.0,
688 *spec,
689 view,
690 ),
691 ExitTransition::Composite(transitions) => {
692 let mut v = view;
693 for t in transitions {
694 v = apply_exit_single(key, version, t, spec, v);
695 }
696 v
697 }
698 }
699}
700
701fn apply_exit_single(
702 key: &str,
703 version: u64,
704 exit: &ExitTransition,
705 spec: &AnimationSpec,
706 view: View,
707) -> View {
708 match exit {
709 ExitTransition::FadeOut => {
710 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
711 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
712 }
713 ExitTransition::SlideOut { offset_x, offset_y } => {
714 let offset = animate_vec2_from(
715 format!("{key}:v{version}:exit:slide"),
716 Vec2::default(),
717 Vec2 {
718 x: dp_to_px(*offset_x),
719 y: dp_to_px(*offset_y),
720 },
721 *spec,
722 );
723 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
724 }
725 ExitTransition::ScaleOut { target } => {
726 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
727 Box(Modifier::new().fill_max_size().transform_origin(0.5, 0.5).scale(s)).child(view)
728 }
729 ExitTransition::ShrinkVertically { clip, shrink_towards } => apply_size_fraction(
730 &format!("{key}:meas"),
731 &format!("{key}:v{version}:exit:expand_v"),
732 SizeAxis::Vertical,
733 1.0,
734 0.0,
735 *clip,
736 *shrink_towards,
737 *spec,
738 view,
739 ),
740 ExitTransition::ShrinkHorizontally { clip, shrink_towards } => apply_size_fraction(
741 &format!("{key}:meas"),
742 &format!("{key}:v{version}:exit:expand_h"),
743 SizeAxis::Horizontal,
744 1.0,
745 0.0,
746 *clip,
747 *shrink_towards,
748 *spec,
749 view,
750 ),
751 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
752 &format!("{key}:meas"),
753 &format!("{key}:v{version}:exit:expand_in"),
754 SizeAxis::Both,
755 1.0,
756 0.0,
757 *clip,
758 0.0,
759 *spec,
760 view,
761 ),
762 ExitTransition::Composite(inner) => {
763 let mut v = view;
764 for t in inner {
765 v = apply_exit_single(key, version, t, spec, v);
766 }
767 v
768 }
769 }
770}
771
772fn apply_enter_inflow(
776 key: &str,
777 version: u64,
778 enter: &EnterTransition,
779 spec: &AnimationSpec,
780 view: View,
781) -> View {
782 match enter {
783 EnterTransition::FadeIn => {
784 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
785 Box(flow_mod().alpha(val)).child(view)
786 }
787 EnterTransition::SlideIn { offset_x, offset_y } => {
788 let offset = animate_vec2_from(
789 format!("{key}:v{version}:enter:slide"),
790 Vec2 {
791 x: dp_to_px(*offset_x),
792 y: dp_to_px(*offset_y),
793 },
794 Vec2::default(),
795 *spec,
796 );
797 Box(flow_mod().translate_vec2(offset)).child(view)
798 }
799 EnterTransition::ScaleIn { initial } => {
800 let s = animate_f32_from(
801 format!("{key}:v{version}:enter:scale"),
802 *initial,
803 1.0,
804 *spec,
805 );
806 let a = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
807 Box(flow_mod().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
808 }
809 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
810 &format!("{key}:meas"),
811 &format!("{key}:v{version}:enter:expand_v"),
812 SizeAxis::Vertical,
813 0.0,
814 1.0,
815 *clip,
816 *expand_from,
817 *spec,
818 view,
819 ),
820 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
821 &format!("{key}:meas"),
822 &format!("{key}:v{version}:enter:expand_h"),
823 SizeAxis::Horizontal,
824 0.0,
825 1.0,
826 *clip,
827 *expand_from,
828 *spec,
829 view,
830 ),
831 EnterTransition::ExpandIn { clip } => apply_size_fraction(
832 &format!("{key}:meas"),
833 &format!("{key}:v{version}:enter:expand_in"),
834 SizeAxis::Both,
835 0.0,
836 1.0,
837 *clip,
838 0.0,
839 *spec,
840 view,
841 ),
842 EnterTransition::Composite(transitions) => {
843 let mut v = view;
844 for t in transitions {
845 v = apply_enter_inflow_single(key, version, t, spec, v);
846 }
847 v
848 }
849 }
850}
851
852fn apply_enter_inflow_single(
853 key: &str,
854 version: u64,
855 enter: &EnterTransition,
856 spec: &AnimationSpec,
857 view: View,
858) -> View {
859 match enter {
860 EnterTransition::FadeIn => {
861 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
862 Box(flow_mod().alpha(val)).child(view)
863 }
864 EnterTransition::SlideIn { offset_x, offset_y } => {
865 let offset = animate_vec2_from(
866 format!("{key}:v{version}:enter:slide"),
867 Vec2 {
868 x: dp_to_px(*offset_x),
869 y: dp_to_px(*offset_y),
870 },
871 Vec2::default(),
872 *spec,
873 );
874 Box(flow_mod().translate_vec2(offset)).child(view)
875 }
876 EnterTransition::ScaleIn { initial } => {
877 let s = animate_f32_from(
878 format!("{key}:v{version}:enter:scale"),
879 *initial,
880 1.0,
881 *spec,
882 );
883 Box(flow_mod().transform_origin(0.5, 0.5).scale(s)).child(view)
884 }
885 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
886 &format!("{key}:meas"),
887 &format!("{key}:v{version}:enter:expand_v"),
888 SizeAxis::Vertical,
889 0.0,
890 1.0,
891 *clip,
892 *expand_from,
893 *spec,
894 view,
895 ),
896 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
897 &format!("{key}:meas"),
898 &format!("{key}:v{version}:enter:expand_h"),
899 SizeAxis::Horizontal,
900 0.0,
901 1.0,
902 *clip,
903 *expand_from,
904 *spec,
905 view,
906 ),
907 EnterTransition::ExpandIn { clip } => apply_size_fraction(
908 &format!("{key}:meas"),
909 &format!("{key}:v{version}:enter:expand_in"),
910 SizeAxis::Both,
911 0.0,
912 1.0,
913 *clip,
914 0.0,
915 *spec,
916 view,
917 ),
918 EnterTransition::Composite(inner) => {
919 let mut v = view;
920 for t in inner {
921 v = apply_enter_inflow_single(key, version, t, spec, v);
922 }
923 v
924 }
925 }
926}
927
928fn apply_exit_inflow(
929 key: &str,
930 version: u64,
931 exit: &ExitTransition,
932 spec: &AnimationSpec,
933 view: View,
934) -> View {
935 match exit {
936 ExitTransition::FadeOut => {
937 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
938 Box(flow_mod().alpha(val)).child(view)
939 }
940 ExitTransition::SlideOut { offset_x, offset_y } => {
941 let offset = animate_vec2_from(
942 format!("{key}:v{version}:exit:slide"),
943 Vec2::default(),
944 Vec2 {
945 x: dp_to_px(*offset_x),
946 y: dp_to_px(*offset_y),
947 },
948 *spec,
949 );
950 Box(flow_mod().translate_vec2(offset)).child(view)
951 }
952 ExitTransition::ScaleOut { target } => {
953 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
954 let a = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
955 Box(flow_mod().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
956 }
957 ExitTransition::ShrinkVertically { clip, shrink_towards } => apply_size_fraction(
958 &format!("{key}:meas"),
959 &format!("{key}:v{version}:exit:expand_v"),
960 SizeAxis::Vertical,
961 1.0,
962 0.0,
963 *clip,
964 *shrink_towards,
965 *spec,
966 view,
967 ),
968 ExitTransition::ShrinkHorizontally { clip, shrink_towards } => apply_size_fraction(
969 &format!("{key}:meas"),
970 &format!("{key}:v{version}:exit:expand_h"),
971 SizeAxis::Horizontal,
972 1.0,
973 0.0,
974 *clip,
975 *shrink_towards,
976 *spec,
977 view,
978 ),
979 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
980 &format!("{key}:meas"),
981 &format!("{key}:v{version}:exit:expand_in"),
982 SizeAxis::Both,
983 1.0,
984 0.0,
985 *clip,
986 0.0,
987 *spec,
988 view,
989 ),
990 ExitTransition::Composite(transitions) => {
991 let mut v = view;
992 for t in transitions {
993 v = apply_exit_inflow_single(key, version, t, spec, v);
994 }
995 v
996 }
997 }
998}
999
1000fn apply_exit_inflow_single(
1001 key: &str,
1002 version: u64,
1003 exit: &ExitTransition,
1004 spec: &AnimationSpec,
1005 view: View,
1006) -> View {
1007 match exit {
1008 ExitTransition::FadeOut => {
1009 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
1010 Box(flow_mod().alpha(val)).child(view)
1011 }
1012 ExitTransition::SlideOut { offset_x, offset_y } => {
1013 let offset = animate_vec2_from(
1014 format!("{key}:v{version}:exit:slide"),
1015 Vec2::default(),
1016 Vec2 {
1017 x: dp_to_px(*offset_x),
1018 y: dp_to_px(*offset_y),
1019 },
1020 *spec,
1021 );
1022 Box(flow_mod().translate_vec2(offset)).child(view)
1023 }
1024 ExitTransition::ScaleOut { target } => {
1025 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
1026 Box(flow_mod().transform_origin(0.5, 0.5).scale(s)).child(view)
1027 }
1028 ExitTransition::ShrinkVertically { clip, shrink_towards } => apply_size_fraction(
1029 &format!("{key}:meas"),
1030 &format!("{key}:v{version}:exit:expand_v"),
1031 SizeAxis::Vertical,
1032 1.0,
1033 0.0,
1034 *clip,
1035 *shrink_towards,
1036 *spec,
1037 view,
1038 ),
1039 ExitTransition::ShrinkHorizontally { clip, shrink_towards } => apply_size_fraction(
1040 &format!("{key}:meas"),
1041 &format!("{key}:v{version}:exit:expand_h"),
1042 SizeAxis::Horizontal,
1043 1.0,
1044 0.0,
1045 *clip,
1046 *shrink_towards,
1047 *spec,
1048 view,
1049 ),
1050 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
1051 &format!("{key}:meas"),
1052 &format!("{key}:v{version}:exit:expand_in"),
1053 SizeAxis::Both,
1054 1.0,
1055 0.0,
1056 *clip,
1057 0.0,
1058 *spec,
1059 view,
1060 ),
1061 ExitTransition::Composite(inner) => {
1062 let mut v = view;
1063 for t in inner {
1064 v = apply_exit_inflow_single(key, version, t, spec, v);
1065 }
1066 v
1067 }
1068 }
1069}
1070
1071fn read_anim_value(key: &str, default: f32) -> Option<f32> {
1074 let anim = remember_state_with_key::<AnimatedValue<f32>>(format!("anim:f32:{key}"), || {
1075 AnimatedValue::new(default, AnimationSpec::default())
1076 });
1077 let a = anim.borrow();
1078 if a.is_animating() {
1079 None
1080 } else {
1081 Some(*a.get())
1082 }
1083}
1084
1085fn read_anim_vec2_value(key: &str, default: Vec2) -> Option<Vec2> {
1086 let anim = remember_state_with_key::<AnimatedValue<Vec2>>(format!("anim:vec2:{key}"), || {
1087 AnimatedValue::new(default, AnimationSpec::default())
1088 });
1089 let a = anim.borrow();
1090 if a.is_animating() {
1091 None
1092 } else {
1093 Some(*a.get())
1094 }
1095}
1096
1097fn exit_animation_done(key: &str, version: u64, exit: &ExitTransition) -> bool {
1099 match exit {
1100 ExitTransition::FadeOut => read_anim_value(&format!("{key}:v{version}:exit:fade"), 1.0)
1101 .map(|v| v < 0.005)
1102 .unwrap_or(false),
1103 ExitTransition::SlideOut { offset_x, offset_y } => {
1104 let target = Vec2 {
1105 x: dp_to_px(*offset_x),
1106 y: dp_to_px(*offset_y),
1107 };
1108 read_anim_vec2_value(&format!("{key}:v{version}:exit:slide"), Vec2::default())
1109 .map(|v| (v.x - target.x).abs() < 0.5 && (v.y - target.y).abs() < 0.5)
1110 .unwrap_or(false)
1111 }
1112 ExitTransition::ScaleOut { target } => {
1113 let done_scale = read_anim_value(&format!("{key}:v{version}:exit:scale"), 1.0)
1114 .map(|v| (v - target).abs() < 0.005)
1115 .unwrap_or(false);
1116 let done_fade = read_anim_value(&format!("{key}:v{version}:exit:fade"), 1.0)
1117 .map(|v| v < 0.005)
1118 .unwrap_or(false);
1119 done_scale && done_fade
1120 }
1121 ExitTransition::ShrinkVertically { .. } => read_anim_value(
1122 &format!("{key}:v{version}:exit:expand_v"),
1123 1.0,
1124 )
1125 .map(|v| v < 0.005)
1126 .unwrap_or(false),
1127 ExitTransition::ShrinkHorizontally { .. } => read_anim_value(
1128 &format!("{key}:v{version}:exit:expand_h"),
1129 1.0,
1130 )
1131 .map(|v| v < 0.005)
1132 .unwrap_or(false),
1133 ExitTransition::ShrinkOut { .. } => read_anim_value(
1134 &format!("{key}:v{version}:exit:expand_in"),
1135 1.0,
1136 )
1137 .map(|v| v < 0.005)
1138 .unwrap_or(false),
1139 ExitTransition::Composite(ts) => ts.iter().all(|t| exit_animation_done(key, version, t)),
1140 }
1141}
1142
1143#[derive(Clone)]
1144pub struct AnimatedVisibilityConfig {
1145 pub key: String,
1146 pub spec: AnimationSpec,
1147 pub enter: EnterTransition,
1148 pub exit: ExitTransition,
1149}
1150
1151impl Default for AnimatedVisibilityConfig {
1152 fn default() -> Self {
1153 Self {
1154 key: "anim_vis".into(),
1155 spec: AnimationSpec::default(),
1156 enter: EnterTransition::default(),
1157 exit: ExitTransition::default(),
1158 }
1159 }
1160}
1161
1162impl AnimatedVisibilityConfig {
1163 pub fn with_key(key: impl Into<String>) -> Self {
1164 Self {
1165 key: key.into(),
1166 ..Default::default()
1167 }
1168 }
1169}
1170
1171pub fn AnimatedVisibility(visible: bool, content: View, config: AnimatedVisibilityConfig) -> View {
1177 let key = config.key;
1178 let spec = config.spec;
1179 let enter = config.enter;
1180 let exit = config.exit;
1181
1182 let old_content = remember_with_key(format!("av_old:{key}"), || RefCell::new(None::<View>));
1183 let version = remember_with_key(format!("av_ver:{key}"), || RefCell::new(0u64));
1184 let prev = remember_with_key(format!("av_prev:{key}"), || RefCell::new(visible));
1185
1186 if *prev.borrow() != visible {
1188 if !visible {
1189 let mut captured = content.clone();
1191 captured.scope_key = Some(format!("av_{key}_old"));
1192 captured.modifier.repaint_boundary = true;
1193 old_content.borrow_mut().replace(captured);
1194 } else {
1195 *old_content.borrow_mut() = None;
1197 }
1198 *version.borrow_mut() += 1;
1199 prev.borrow_mut().clone_from(&visible);
1200 }
1201
1202 let v = *version.borrow();
1203
1204 let exiting = {
1206 let mut oc = old_content.borrow_mut();
1207 if let Some(ref old) = *oc {
1208 if exit_animation_done(&key, v, &exit) {
1209 *oc = None;
1210 None
1211 } else {
1212 let mut exit_view = apply_exit_inflow(&key, v, &exit, &spec, old.clone());
1213 exit_view.modifier.key = Some(transition_child_key(&key, v, "av_exit"));
1214 Some(exit_view)
1215 }
1216 } else {
1217 None
1218 }
1219 };
1220
1221 if visible {
1222 let mut content = content;
1223 content.scope_key = Some(format!("av_{key}_content"));
1224 content.modifier.repaint_boundary = true;
1225 let mut entering = if v > 0 {
1226 apply_enter_inflow(&key, v, &enter, &spec, content)
1227 } else {
1228 content
1229 };
1230 entering.modifier.key = Some(transition_child_key(&key, v, "av_enter"));
1231 entering
1232 } else {
1233 exiting.unwrap_or_else(|| Box(Modifier::new().height(0.0)))
1234 }
1235}