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()
509 .fill_max_size()
510 .transform_origin(0.5, 0.5)
511 .scale(s)
512 .alpha(a))
513 .child(view)
514 }
515 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
516 &format!("{key}:meas"),
517 &format!("{key}:v{version}:enter:expand_v"),
518 SizeAxis::Vertical,
519 0.0,
520 1.0,
521 *clip,
522 *expand_from,
523 *spec,
524 view,
525 ),
526 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
527 &format!("{key}:meas"),
528 &format!("{key}:v{version}:enter:expand_h"),
529 SizeAxis::Horizontal,
530 0.0,
531 1.0,
532 *clip,
533 *expand_from,
534 *spec,
535 view,
536 ),
537 EnterTransition::ExpandIn { clip } => apply_size_fraction(
538 &format!("{key}:meas"),
539 &format!("{key}:v{version}:enter:expand_in"),
540 SizeAxis::Both,
541 0.0,
542 1.0,
543 *clip,
544 0.0,
545 *spec,
546 view,
547 ),
548 EnterTransition::Composite(transitions) => {
549 let mut v = view;
550 for t in transitions {
551 v = apply_enter_single(key, version, t, spec, v);
552 }
553 v
554 }
555 }
556}
557
558fn apply_enter_single(
559 key: &str,
560 version: u64,
561 enter: &EnterTransition,
562 spec: &AnimationSpec,
563 view: View,
564) -> View {
565 match enter {
566 EnterTransition::FadeIn => {
567 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
568 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
569 }
570 EnterTransition::SlideIn { offset_x, offset_y } => {
571 let offset = animate_vec2_from(
572 format!("{key}:v{version}:enter:slide"),
573 Vec2 {
574 x: dp_to_px(*offset_x),
575 y: dp_to_px(*offset_y),
576 },
577 Vec2::default(),
578 *spec,
579 );
580 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
581 }
582 EnterTransition::ScaleIn { initial } => {
583 let s = animate_f32_from(
584 format!("{key}:v{version}:enter:scale"),
585 *initial,
586 1.0,
587 *spec,
588 );
589 Box(Modifier::new()
590 .fill_max_size()
591 .transform_origin(0.5, 0.5)
592 .scale(s))
593 .child(view)
594 }
595 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
596 &format!("{key}:meas"),
597 &format!("{key}:v{version}:enter:expand_v"),
598 SizeAxis::Vertical,
599 0.0,
600 1.0,
601 *clip,
602 *expand_from,
603 *spec,
604 view,
605 ),
606 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
607 &format!("{key}:meas"),
608 &format!("{key}:v{version}:enter:expand_h"),
609 SizeAxis::Horizontal,
610 0.0,
611 1.0,
612 *clip,
613 *expand_from,
614 *spec,
615 view,
616 ),
617 EnterTransition::ExpandIn { clip } => apply_size_fraction(
618 &format!("{key}:meas"),
619 &format!("{key}:v{version}:enter:expand_in"),
620 SizeAxis::Both,
621 0.0,
622 1.0,
623 *clip,
624 0.0,
625 *spec,
626 view,
627 ),
628 EnterTransition::Composite(inner) => {
629 let mut v = view;
630 for t in inner {
631 v = apply_enter_single(key, version, t, spec, v);
632 }
633 v
634 }
635 }
636}
637
638fn apply_exit(
639 key: &str,
640 version: u64,
641 exit: &ExitTransition,
642 spec: &AnimationSpec,
643 view: View,
644) -> View {
645 match exit {
646 ExitTransition::FadeOut => {
647 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
648 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
649 }
650 ExitTransition::SlideOut { offset_x, offset_y } => {
651 let offset = animate_vec2_from(
652 format!("{key}:v{version}:exit:slide"),
653 Vec2::default(),
654 Vec2 {
655 x: dp_to_px(*offset_x),
656 y: dp_to_px(*offset_y),
657 },
658 *spec,
659 );
660 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
661 }
662 ExitTransition::ScaleOut { target } => {
663 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
664 let a = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
665 Box(Modifier::new()
666 .fill_max_size()
667 .transform_origin(0.5, 0.5)
668 .scale(s)
669 .alpha(a))
670 .child(view)
671 }
672 ExitTransition::ShrinkVertically {
673 clip,
674 shrink_towards,
675 } => apply_size_fraction(
676 &format!("{key}:meas"),
677 &format!("{key}:v{version}:exit:expand_v"),
678 SizeAxis::Vertical,
679 1.0,
680 0.0,
681 *clip,
682 *shrink_towards,
683 *spec,
684 view,
685 ),
686 ExitTransition::ShrinkHorizontally {
687 clip,
688 shrink_towards,
689 } => apply_size_fraction(
690 &format!("{key}:meas"),
691 &format!("{key}:v{version}:exit:expand_h"),
692 SizeAxis::Horizontal,
693 1.0,
694 0.0,
695 *clip,
696 *shrink_towards,
697 *spec,
698 view,
699 ),
700 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
701 &format!("{key}:meas"),
702 &format!("{key}:v{version}:exit:expand_in"),
703 SizeAxis::Both,
704 1.0,
705 0.0,
706 *clip,
707 0.0,
708 *spec,
709 view,
710 ),
711 ExitTransition::Composite(transitions) => {
712 let mut v = view;
713 for t in transitions {
714 v = apply_exit_single(key, version, t, spec, v);
715 }
716 v
717 }
718 }
719}
720
721fn apply_exit_single(
722 key: &str,
723 version: u64,
724 exit: &ExitTransition,
725 spec: &AnimationSpec,
726 view: View,
727) -> View {
728 match exit {
729 ExitTransition::FadeOut => {
730 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
731 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
732 }
733 ExitTransition::SlideOut { offset_x, offset_y } => {
734 let offset = animate_vec2_from(
735 format!("{key}:v{version}:exit:slide"),
736 Vec2::default(),
737 Vec2 {
738 x: dp_to_px(*offset_x),
739 y: dp_to_px(*offset_y),
740 },
741 *spec,
742 );
743 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
744 }
745 ExitTransition::ScaleOut { target } => {
746 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
747 Box(Modifier::new()
748 .fill_max_size()
749 .transform_origin(0.5, 0.5)
750 .scale(s))
751 .child(view)
752 }
753 ExitTransition::ShrinkVertically {
754 clip,
755 shrink_towards,
756 } => apply_size_fraction(
757 &format!("{key}:meas"),
758 &format!("{key}:v{version}:exit:expand_v"),
759 SizeAxis::Vertical,
760 1.0,
761 0.0,
762 *clip,
763 *shrink_towards,
764 *spec,
765 view,
766 ),
767 ExitTransition::ShrinkHorizontally {
768 clip,
769 shrink_towards,
770 } => apply_size_fraction(
771 &format!("{key}:meas"),
772 &format!("{key}:v{version}:exit:expand_h"),
773 SizeAxis::Horizontal,
774 1.0,
775 0.0,
776 *clip,
777 *shrink_towards,
778 *spec,
779 view,
780 ),
781 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
782 &format!("{key}:meas"),
783 &format!("{key}:v{version}:exit:expand_in"),
784 SizeAxis::Both,
785 1.0,
786 0.0,
787 *clip,
788 0.0,
789 *spec,
790 view,
791 ),
792 ExitTransition::Composite(inner) => {
793 let mut v = view;
794 for t in inner {
795 v = apply_exit_single(key, version, t, spec, v);
796 }
797 v
798 }
799 }
800}
801
802fn apply_enter_inflow(
806 key: &str,
807 version: u64,
808 enter: &EnterTransition,
809 spec: &AnimationSpec,
810 view: View,
811) -> View {
812 match enter {
813 EnterTransition::FadeIn => {
814 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
815 Box(flow_mod().alpha(val)).child(view)
816 }
817 EnterTransition::SlideIn { offset_x, offset_y } => {
818 let offset = animate_vec2_from(
819 format!("{key}:v{version}:enter:slide"),
820 Vec2 {
821 x: dp_to_px(*offset_x),
822 y: dp_to_px(*offset_y),
823 },
824 Vec2::default(),
825 *spec,
826 );
827 Box(flow_mod().translate_vec2(offset)).child(view)
828 }
829 EnterTransition::ScaleIn { initial } => {
830 let s = animate_f32_from(
831 format!("{key}:v{version}:enter:scale"),
832 *initial,
833 1.0,
834 *spec,
835 );
836 let a = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
837 Box(flow_mod().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
838 }
839 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
840 &format!("{key}:meas"),
841 &format!("{key}:v{version}:enter:expand_v"),
842 SizeAxis::Vertical,
843 0.0,
844 1.0,
845 *clip,
846 *expand_from,
847 *spec,
848 view,
849 ),
850 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
851 &format!("{key}:meas"),
852 &format!("{key}:v{version}:enter:expand_h"),
853 SizeAxis::Horizontal,
854 0.0,
855 1.0,
856 *clip,
857 *expand_from,
858 *spec,
859 view,
860 ),
861 EnterTransition::ExpandIn { clip } => apply_size_fraction(
862 &format!("{key}:meas"),
863 &format!("{key}:v{version}:enter:expand_in"),
864 SizeAxis::Both,
865 0.0,
866 1.0,
867 *clip,
868 0.0,
869 *spec,
870 view,
871 ),
872 EnterTransition::Composite(transitions) => {
873 let mut v = view;
874 for t in transitions {
875 v = apply_enter_inflow_single(key, version, t, spec, v);
876 }
877 v
878 }
879 }
880}
881
882fn apply_enter_inflow_single(
883 key: &str,
884 version: u64,
885 enter: &EnterTransition,
886 spec: &AnimationSpec,
887 view: View,
888) -> View {
889 match enter {
890 EnterTransition::FadeIn => {
891 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
892 Box(flow_mod().alpha(val)).child(view)
893 }
894 EnterTransition::SlideIn { offset_x, offset_y } => {
895 let offset = animate_vec2_from(
896 format!("{key}:v{version}:enter:slide"),
897 Vec2 {
898 x: dp_to_px(*offset_x),
899 y: dp_to_px(*offset_y),
900 },
901 Vec2::default(),
902 *spec,
903 );
904 Box(flow_mod().translate_vec2(offset)).child(view)
905 }
906 EnterTransition::ScaleIn { initial } => {
907 let s = animate_f32_from(
908 format!("{key}:v{version}:enter:scale"),
909 *initial,
910 1.0,
911 *spec,
912 );
913 Box(flow_mod().transform_origin(0.5, 0.5).scale(s)).child(view)
914 }
915 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
916 &format!("{key}:meas"),
917 &format!("{key}:v{version}:enter:expand_v"),
918 SizeAxis::Vertical,
919 0.0,
920 1.0,
921 *clip,
922 *expand_from,
923 *spec,
924 view,
925 ),
926 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
927 &format!("{key}:meas"),
928 &format!("{key}:v{version}:enter:expand_h"),
929 SizeAxis::Horizontal,
930 0.0,
931 1.0,
932 *clip,
933 *expand_from,
934 *spec,
935 view,
936 ),
937 EnterTransition::ExpandIn { clip } => apply_size_fraction(
938 &format!("{key}:meas"),
939 &format!("{key}:v{version}:enter:expand_in"),
940 SizeAxis::Both,
941 0.0,
942 1.0,
943 *clip,
944 0.0,
945 *spec,
946 view,
947 ),
948 EnterTransition::Composite(inner) => {
949 let mut v = view;
950 for t in inner {
951 v = apply_enter_inflow_single(key, version, t, spec, v);
952 }
953 v
954 }
955 }
956}
957
958fn apply_exit_inflow(
959 key: &str,
960 version: u64,
961 exit: &ExitTransition,
962 spec: &AnimationSpec,
963 view: View,
964) -> View {
965 match exit {
966 ExitTransition::FadeOut => {
967 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
968 Box(flow_mod().alpha(val)).child(view)
969 }
970 ExitTransition::SlideOut { offset_x, offset_y } => {
971 let offset = animate_vec2_from(
972 format!("{key}:v{version}:exit:slide"),
973 Vec2::default(),
974 Vec2 {
975 x: dp_to_px(*offset_x),
976 y: dp_to_px(*offset_y),
977 },
978 *spec,
979 );
980 Box(flow_mod().translate_vec2(offset)).child(view)
981 }
982 ExitTransition::ScaleOut { target } => {
983 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
984 let a = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
985 Box(flow_mod().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
986 }
987 ExitTransition::ShrinkVertically {
988 clip,
989 shrink_towards,
990 } => apply_size_fraction(
991 &format!("{key}:meas"),
992 &format!("{key}:v{version}:exit:expand_v"),
993 SizeAxis::Vertical,
994 1.0,
995 0.0,
996 *clip,
997 *shrink_towards,
998 *spec,
999 view,
1000 ),
1001 ExitTransition::ShrinkHorizontally {
1002 clip,
1003 shrink_towards,
1004 } => apply_size_fraction(
1005 &format!("{key}:meas"),
1006 &format!("{key}:v{version}:exit:expand_h"),
1007 SizeAxis::Horizontal,
1008 1.0,
1009 0.0,
1010 *clip,
1011 *shrink_towards,
1012 *spec,
1013 view,
1014 ),
1015 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
1016 &format!("{key}:meas"),
1017 &format!("{key}:v{version}:exit:expand_in"),
1018 SizeAxis::Both,
1019 1.0,
1020 0.0,
1021 *clip,
1022 0.0,
1023 *spec,
1024 view,
1025 ),
1026 ExitTransition::Composite(transitions) => {
1027 let mut v = view;
1028 for t in transitions {
1029 v = apply_exit_inflow_single(key, version, t, spec, v);
1030 }
1031 v
1032 }
1033 }
1034}
1035
1036fn apply_exit_inflow_single(
1037 key: &str,
1038 version: u64,
1039 exit: &ExitTransition,
1040 spec: &AnimationSpec,
1041 view: View,
1042) -> View {
1043 match exit {
1044 ExitTransition::FadeOut => {
1045 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
1046 Box(flow_mod().alpha(val)).child(view)
1047 }
1048 ExitTransition::SlideOut { offset_x, offset_y } => {
1049 let offset = animate_vec2_from(
1050 format!("{key}:v{version}:exit:slide"),
1051 Vec2::default(),
1052 Vec2 {
1053 x: dp_to_px(*offset_x),
1054 y: dp_to_px(*offset_y),
1055 },
1056 *spec,
1057 );
1058 Box(flow_mod().translate_vec2(offset)).child(view)
1059 }
1060 ExitTransition::ScaleOut { target } => {
1061 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
1062 Box(flow_mod().transform_origin(0.5, 0.5).scale(s)).child(view)
1063 }
1064 ExitTransition::ShrinkVertically {
1065 clip,
1066 shrink_towards,
1067 } => apply_size_fraction(
1068 &format!("{key}:meas"),
1069 &format!("{key}:v{version}:exit:expand_v"),
1070 SizeAxis::Vertical,
1071 1.0,
1072 0.0,
1073 *clip,
1074 *shrink_towards,
1075 *spec,
1076 view,
1077 ),
1078 ExitTransition::ShrinkHorizontally {
1079 clip,
1080 shrink_towards,
1081 } => apply_size_fraction(
1082 &format!("{key}:meas"),
1083 &format!("{key}:v{version}:exit:expand_h"),
1084 SizeAxis::Horizontal,
1085 1.0,
1086 0.0,
1087 *clip,
1088 *shrink_towards,
1089 *spec,
1090 view,
1091 ),
1092 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
1093 &format!("{key}:meas"),
1094 &format!("{key}:v{version}:exit:expand_in"),
1095 SizeAxis::Both,
1096 1.0,
1097 0.0,
1098 *clip,
1099 0.0,
1100 *spec,
1101 view,
1102 ),
1103 ExitTransition::Composite(inner) => {
1104 let mut v = view;
1105 for t in inner {
1106 v = apply_exit_inflow_single(key, version, t, spec, v);
1107 }
1108 v
1109 }
1110 }
1111}
1112
1113fn read_anim_value(key: &str, default: f32) -> Option<f32> {
1116 let anim = remember_state_with_key::<AnimatedValue<f32>>(format!("anim:f32:{key}"), || {
1117 AnimatedValue::new(default, AnimationSpec::default())
1118 });
1119 let a = anim.borrow();
1120 if a.is_animating() {
1121 None
1122 } else {
1123 Some(*a.get())
1124 }
1125}
1126
1127fn read_anim_vec2_value(key: &str, default: Vec2) -> Option<Vec2> {
1128 let anim = remember_state_with_key::<AnimatedValue<Vec2>>(format!("anim:vec2:{key}"), || {
1129 AnimatedValue::new(default, AnimationSpec::default())
1130 });
1131 let a = anim.borrow();
1132 if a.is_animating() {
1133 None
1134 } else {
1135 Some(*a.get())
1136 }
1137}
1138
1139fn exit_animation_done(key: &str, version: u64, exit: &ExitTransition) -> bool {
1141 match exit {
1142 ExitTransition::FadeOut => read_anim_value(&format!("{key}:v{version}:exit:fade"), 1.0)
1143 .map(|v| v < 0.005)
1144 .unwrap_or(false),
1145 ExitTransition::SlideOut { offset_x, offset_y } => {
1146 let target = Vec2 {
1147 x: dp_to_px(*offset_x),
1148 y: dp_to_px(*offset_y),
1149 };
1150 read_anim_vec2_value(&format!("{key}:v{version}:exit:slide"), Vec2::default())
1151 .map(|v| (v.x - target.x).abs() < 0.5 && (v.y - target.y).abs() < 0.5)
1152 .unwrap_or(false)
1153 }
1154 ExitTransition::ScaleOut { target } => {
1155 let done_scale = read_anim_value(&format!("{key}:v{version}:exit:scale"), 1.0)
1156 .map(|v| (v - target).abs() < 0.005)
1157 .unwrap_or(false);
1158 let done_fade = read_anim_value(&format!("{key}:v{version}:exit:fade"), 1.0)
1159 .map(|v| v < 0.005)
1160 .unwrap_or(false);
1161 done_scale && done_fade
1162 }
1163 ExitTransition::ShrinkVertically { .. } => {
1164 read_anim_value(&format!("{key}:v{version}:exit:expand_v"), 1.0)
1165 .map(|v| v < 0.005)
1166 .unwrap_or(false)
1167 }
1168 ExitTransition::ShrinkHorizontally { .. } => {
1169 read_anim_value(&format!("{key}:v{version}:exit:expand_h"), 1.0)
1170 .map(|v| v < 0.005)
1171 .unwrap_or(false)
1172 }
1173 ExitTransition::ShrinkOut { .. } => {
1174 read_anim_value(&format!("{key}:v{version}:exit:expand_in"), 1.0)
1175 .map(|v| v < 0.005)
1176 .unwrap_or(false)
1177 }
1178 ExitTransition::Composite(ts) => ts.iter().all(|t| exit_animation_done(key, version, t)),
1179 }
1180}
1181
1182#[derive(Clone)]
1183pub struct AnimatedVisibilityConfig {
1184 pub key: String,
1185 pub spec: AnimationSpec,
1186 pub enter: EnterTransition,
1187 pub exit: ExitTransition,
1188}
1189
1190impl Default for AnimatedVisibilityConfig {
1191 fn default() -> Self {
1192 Self {
1193 key: "anim_vis".into(),
1194 spec: AnimationSpec::default(),
1195 enter: EnterTransition::default(),
1196 exit: ExitTransition::default(),
1197 }
1198 }
1199}
1200
1201impl AnimatedVisibilityConfig {
1202 pub fn with_key(key: impl Into<String>) -> Self {
1203 Self {
1204 key: key.into(),
1205 ..Default::default()
1206 }
1207 }
1208}
1209
1210pub fn AnimatedVisibility(visible: bool, content: View, config: AnimatedVisibilityConfig) -> View {
1216 let key = config.key;
1217 let spec = config.spec;
1218 let enter = config.enter;
1219 let exit = config.exit;
1220
1221 let old_content = remember_with_key(format!("av_old:{key}"), || RefCell::new(None::<View>));
1222 let version = remember_with_key(format!("av_ver:{key}"), || RefCell::new(0u64));
1223 let prev = remember_with_key(format!("av_prev:{key}"), || RefCell::new(visible));
1224
1225 if *prev.borrow() != visible {
1227 if !visible {
1228 let mut captured = content.clone();
1230 captured.scope_key = Some(format!("av_{key}_old"));
1231 captured.modifier.repaint_boundary = true;
1232 old_content.borrow_mut().replace(captured);
1233 } else {
1234 *old_content.borrow_mut() = None;
1236 }
1237 *version.borrow_mut() += 1;
1238 prev.borrow_mut().clone_from(&visible);
1239 }
1240
1241 let v = *version.borrow();
1242
1243 let exiting = {
1245 let mut oc = old_content.borrow_mut();
1246 if let Some(ref old) = *oc {
1247 if exit_animation_done(&key, v, &exit) {
1248 *oc = None;
1249 None
1250 } else {
1251 let mut exit_view = apply_exit_inflow(&key, v, &exit, &spec, old.clone());
1252 exit_view.modifier.key = Some(transition_child_key(&key, v, "av_exit"));
1253 Some(exit_view)
1254 }
1255 } else {
1256 None
1257 }
1258 };
1259
1260 if visible {
1261 let mut content = content;
1262 content.scope_key = Some(format!("av_{key}_content"));
1263 content.modifier.repaint_boundary = true;
1264 let mut entering = if v > 0 {
1265 apply_enter_inflow(&key, v, &enter, &spec, content)
1266 } else {
1267 content
1268 };
1269 entering.modifier.key = Some(transition_child_key(&key, v, "av_enter"));
1270 entering
1271 } else {
1272 exiting.unwrap_or_else(|| Box(Modifier::new().height(0.0)))
1273 }
1274}