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 = Box(Modifier::new()
228 .fill_max_size()
229 .alpha(exit_alpha)
230 .hit_passthrough())
231 .child(ov.clone());
232 exit_box.modifier.key = Some(transition_child_key(&key, v, "cf_exit"));
233 Some(exit_box)
234 } else {
235 *oc = None;
236 None
237 }
238 } else {
239 None
240 }
241 };
242
243 let enter_alpha = animate_f32_from(format!("cf_enter:{key}:v{v}"), 0.0, 1.0, spec);
245 let mut enter_box = Box(Modifier::new().fill_max_size().alpha(enter_alpha)).child(new_view);
246 enter_box.modifier.key = Some(transition_child_key(&key, v, "cf_enter"));
247
248 match old_view {
249 Some(ov) => ZStack(Modifier::new().fill_max_size()).child((ov, enter_box)),
250 None => enter_box,
251 }
252}
253
254#[derive(Clone)]
255pub struct AnimatedContentConfig {
256 pub key: String,
257 pub spec: AnimationSpec,
258 pub enter: EnterTransition,
259 pub exit: ExitTransition,
260}
261
262impl Default for AnimatedContentConfig {
263 fn default() -> Self {
264 Self {
265 key: "anim_content".into(),
266 spec: AnimationSpec::default(),
267 enter: EnterTransition::FadeIn,
268 exit: ExitTransition::FadeOut,
269 }
270 }
271}
272
273fn transition_child_key(key: &str, version: u64, tag: &str) -> u64 {
275 let mut h = DefaultHasher::new();
276 key.hash(&mut h);
277 version.hash(&mut h);
278 tag.hash(&mut h);
279 h.finish()
280}
281
282fn flow_mod() -> Modifier {
286 Modifier::new().fill_max_width()
287}
288
289fn exit_mod_fill() -> Modifier {
292 Modifier::new().fill_max_size().hit_passthrough()
293}
294
295fn exit_mod_flow() -> Modifier {
297 Modifier::new().fill_max_width().hit_passthrough()
298}
299
300#[derive(Clone, Copy)]
302enum SizeAxis {
303 Vertical,
304 Horizontal,
305 Both,
306}
307
308#[allow(clippy::too_many_arguments)]
316fn apply_size_fraction(
317 measure_key: &str,
318 anim_key: &str,
319 axis: SizeAxis,
320 from: f32,
321 to: f32,
322 clip: bool,
323 align: f32,
324 spec: AnimationSpec,
325 view: View,
326) -> View {
327 let full_w = remember_mutable_with_key(format!("{measure_key}:w"), || 0.0f32);
328 let full_h = remember_mutable_with_key(format!("{measure_key}:h"), || 0.0f32);
329
330 let have = match axis {
331 SizeAxis::Vertical => *full_h.get() > 0.5,
332 SizeAxis::Horizontal => *full_w.get() > 0.5,
333 SizeAxis::Both => *full_w.get() > 0.5 && *full_h.get() > 0.5,
334 };
335
336 let progress = if have {
337 animate_f32_from(anim_key, from, to, spec)
338 } else {
339 from
340 };
341
342 let capture = {
343 let fw = full_w.clone();
344 let fh = full_h.clone();
345 move |sz: Vec2| {
346 if sz.x > 0.5 {
347 fw.set_neq(sz.x);
348 }
349 if sz.y > 0.5 {
350 fh.set_neq(sz.y);
351 }
352 }
353 };
354
355 let settled = from < to && progress >= 0.999;
356
357 let full_w = *full_w.get();
358 let full_h = *full_h.get();
359
360 let shown_w = match axis {
361 SizeAxis::Vertical => full_w,
362 SizeAxis::Horizontal | SizeAxis::Both => full_w * progress,
363 };
364 let shown_h = match axis {
365 SizeAxis::Horizontal => full_h,
366 SizeAxis::Vertical | SizeAxis::Both => full_h * progress,
367 };
368
369 let mut outer = Modifier::new().fill_max_width().flex_shrink(0.0);
370 if !have || settled {
371 outer = outer.on_size_changed(capture);
376 } else {
377 match axis {
378 SizeAxis::Vertical => {
379 outer = outer.height(shown_h.max(0.0));
380 }
381 SizeAxis::Horizontal => {
382 outer = outer.width(shown_w.max(0.0));
383 }
384 SizeAxis::Both => {
385 outer = outer.size(shown_w.max(0.0), shown_h.max(0.0));
386 }
387 }
388 }
389 if clip {
390 outer = outer.overflow(repose_core::Overflow::Clip);
391 }
392
393 let align = align.clamp(0.0, 1.0);
397 let ox = match axis {
398 SizeAxis::Vertical => 0.0,
399 SizeAxis::Horizontal | SizeAxis::Both => (shown_w - full_w) * align,
400 };
401 let oy = match axis {
402 SizeAxis::Horizontal => 0.0,
403 SizeAxis::Vertical | SizeAxis::Both => (shown_h - full_h) * align,
404 };
405 let mut inner = Modifier::new().fill_max_width().flex_shrink(0.0);
406 if have && !settled {
407 match axis {
408 SizeAxis::Vertical => {
409 inner = inner.height(full_h);
410 }
411 SizeAxis::Horizontal => {
412 inner = inner.width(full_w);
413 }
414 SizeAxis::Both => {
415 inner = inner.size(full_w, full_h);
416 }
417 }
418 }
419 if ox.abs() > 0.01 || oy.abs() > 0.01 {
420 inner = inner.translate(dp_to_px(ox), dp_to_px(oy));
421 }
422
423 Box(outer).child(Box(inner).child(view))
424}
425
426pub fn AnimatedContent<T, F>(target_state: T, content: F, config: AnimatedContentConfig) -> View
433where
434 T: PartialEq + Clone + 'static,
435 F: Fn(T) -> View + 'static,
436{
437 let key = config.key;
438 let spec = config.spec;
439 let enter = config.enter;
440 let exit = config.exit;
441
442 let prev = remember_with_key(format!("ac_prev:{key}"), || {
443 RefCell::new(target_state.clone())
444 });
445 let old_content =
446 remember_with_key(format!("ac_old_view:{key}"), || RefCell::new(None::<View>));
447 let version = remember_with_key(format!("ac_version:{key}"), || RefCell::new(0u64));
448
449 let is_new = *prev.borrow() != target_state;
450 if is_new {
451 let old_ver = *version.borrow();
452 let mut prev_view = content(prev.borrow().clone());
453 prev_view.scope_key = Some(format!("ac_{key}_old_v{old_ver}"));
454 prev_view.modifier.repaint_boundary = true;
455 old_content.borrow_mut().replace(prev_view);
456 prev.borrow_mut().clone_from(&target_state);
457 *version.borrow_mut() += 1;
458 }
459
460 let v = *version.borrow();
461 let mut new_view = content(target_state.clone());
462 new_view.scope_key = Some(format!("ac_{key}_v{v}"));
463 new_view.modifier.repaint_boundary = true;
464 let mut new_view = apply_enter(&key, v, &enter, &spec, new_view);
465 new_view.modifier.key = Some(transition_child_key(&key, v, "ac_enter"));
466
467 let old_view = {
468 let mut oc = old_content.borrow_mut();
469 if let Some(ref ov) = *oc {
470 if exit_animation_done(&key, v, &exit) {
472 *oc = None;
473 None
474 } else {
475 let mut exit_view = apply_exit(&key, v, &exit, &spec, ov.clone());
476 exit_view.modifier.key = Some(transition_child_key(&key, v, "ac_exit"));
477 Some(exit_view)
478 }
479 } else {
480 None
481 }
482 };
483
484 match old_view {
485 Some(ov) => ZStack(Modifier::new().fill_max_size()).child((ov, new_view)),
486 None => new_view,
487 }
488}
489
490fn apply_enter(
491 key: &str,
492 version: u64,
493 enter: &EnterTransition,
494 spec: &AnimationSpec,
495 view: View,
496) -> View {
497 match enter {
498 EnterTransition::FadeIn => {
499 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
500 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
501 }
502 EnterTransition::SlideIn { offset_x, offset_y } => {
503 let offset = animate_vec2_from(
504 format!("{key}:v{version}:enter:slide"),
505 Vec2 {
506 x: dp_to_px(*offset_x),
507 y: dp_to_px(*offset_y),
508 },
509 Vec2::default(),
510 *spec,
511 );
512 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
513 }
514 EnterTransition::ScaleIn { initial } => {
515 let s = animate_f32_from(
516 format!("{key}:v{version}:enter:scale"),
517 *initial,
518 1.0,
519 *spec,
520 );
521 let a = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
522 Box(Modifier::new()
523 .fill_max_size()
524 .transform_origin(0.5, 0.5)
525 .scale(s)
526 .alpha(a))
527 .child(view)
528 }
529 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
530 &format!("{key}:meas"),
531 &format!("{key}:v{version}:enter:expand_v"),
532 SizeAxis::Vertical,
533 0.0,
534 1.0,
535 *clip,
536 *expand_from,
537 *spec,
538 view,
539 ),
540 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
541 &format!("{key}:meas"),
542 &format!("{key}:v{version}:enter:expand_h"),
543 SizeAxis::Horizontal,
544 0.0,
545 1.0,
546 *clip,
547 *expand_from,
548 *spec,
549 view,
550 ),
551 EnterTransition::ExpandIn { clip } => apply_size_fraction(
552 &format!("{key}:meas"),
553 &format!("{key}:v{version}:enter:expand_in"),
554 SizeAxis::Both,
555 0.0,
556 1.0,
557 *clip,
558 0.0,
559 *spec,
560 view,
561 ),
562 EnterTransition::Composite(transitions) => {
563 let mut v = view;
564 for t in transitions {
565 v = apply_enter_single(key, version, t, spec, v);
566 }
567 v
568 }
569 }
570}
571
572fn apply_enter_single(
573 key: &str,
574 version: u64,
575 enter: &EnterTransition,
576 spec: &AnimationSpec,
577 view: View,
578) -> View {
579 match enter {
580 EnterTransition::FadeIn => {
581 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
582 Box(Modifier::new().fill_max_size().alpha(val)).child(view)
583 }
584 EnterTransition::SlideIn { offset_x, offset_y } => {
585 let offset = animate_vec2_from(
586 format!("{key}:v{version}:enter:slide"),
587 Vec2 {
588 x: dp_to_px(*offset_x),
589 y: dp_to_px(*offset_y),
590 },
591 Vec2::default(),
592 *spec,
593 );
594 Box(Modifier::new().fill_max_size().translate_vec2(offset)).child(view)
595 }
596 EnterTransition::ScaleIn { initial } => {
597 let s = animate_f32_from(
598 format!("{key}:v{version}:enter:scale"),
599 *initial,
600 1.0,
601 *spec,
602 );
603 Box(Modifier::new()
604 .fill_max_size()
605 .transform_origin(0.5, 0.5)
606 .scale(s))
607 .child(view)
608 }
609 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
610 &format!("{key}:meas"),
611 &format!("{key}:v{version}:enter:expand_v"),
612 SizeAxis::Vertical,
613 0.0,
614 1.0,
615 *clip,
616 *expand_from,
617 *spec,
618 view,
619 ),
620 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
621 &format!("{key}:meas"),
622 &format!("{key}:v{version}:enter:expand_h"),
623 SizeAxis::Horizontal,
624 0.0,
625 1.0,
626 *clip,
627 *expand_from,
628 *spec,
629 view,
630 ),
631 EnterTransition::ExpandIn { clip } => apply_size_fraction(
632 &format!("{key}:meas"),
633 &format!("{key}:v{version}:enter:expand_in"),
634 SizeAxis::Both,
635 0.0,
636 1.0,
637 *clip,
638 0.0,
639 *spec,
640 view,
641 ),
642 EnterTransition::Composite(inner) => {
643 let mut v = view;
644 for t in inner {
645 v = apply_enter_single(key, version, t, spec, v);
646 }
647 v
648 }
649 }
650}
651
652fn apply_exit(
653 key: &str,
654 version: u64,
655 exit: &ExitTransition,
656 spec: &AnimationSpec,
657 view: View,
658) -> View {
659 match exit {
660 ExitTransition::FadeOut => {
661 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
662 Box(exit_mod_fill().alpha(val)).child(view)
663 }
664 ExitTransition::SlideOut { offset_x, offset_y } => {
665 let offset = animate_vec2_from(
666 format!("{key}:v{version}:exit:slide"),
667 Vec2::default(),
668 Vec2 {
669 x: dp_to_px(*offset_x),
670 y: dp_to_px(*offset_y),
671 },
672 *spec,
673 );
674 Box(exit_mod_fill().translate_vec2(offset)).child(view)
675 }
676 ExitTransition::ScaleOut { target } => {
677 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
678 let a = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
679 Box(exit_mod_fill().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
680 }
681 ExitTransition::ShrinkVertically {
682 clip,
683 shrink_towards,
684 } => apply_size_fraction(
685 &format!("{key}:meas"),
686 &format!("{key}:v{version}:exit:expand_v"),
687 SizeAxis::Vertical,
688 1.0,
689 0.0,
690 *clip,
691 *shrink_towards,
692 *spec,
693 view,
694 ),
695 ExitTransition::ShrinkHorizontally {
696 clip,
697 shrink_towards,
698 } => apply_size_fraction(
699 &format!("{key}:meas"),
700 &format!("{key}:v{version}:exit:expand_h"),
701 SizeAxis::Horizontal,
702 1.0,
703 0.0,
704 *clip,
705 *shrink_towards,
706 *spec,
707 view,
708 ),
709 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
710 &format!("{key}:meas"),
711 &format!("{key}:v{version}:exit:expand_in"),
712 SizeAxis::Both,
713 1.0,
714 0.0,
715 *clip,
716 0.0,
717 *spec,
718 view,
719 ),
720 ExitTransition::Composite(transitions) => {
721 let mut v = view;
722 for t in transitions {
723 v = apply_exit_single(key, version, t, spec, v);
724 }
725 v
726 }
727 }
728}
729
730fn apply_exit_single(
731 key: &str,
732 version: u64,
733 exit: &ExitTransition,
734 spec: &AnimationSpec,
735 view: View,
736) -> View {
737 match exit {
738 ExitTransition::FadeOut => {
739 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
740 Box(exit_mod_fill().alpha(val)).child(view)
741 }
742 ExitTransition::SlideOut { offset_x, offset_y } => {
743 let offset = animate_vec2_from(
744 format!("{key}:v{version}:exit:slide"),
745 Vec2::default(),
746 Vec2 {
747 x: dp_to_px(*offset_x),
748 y: dp_to_px(*offset_y),
749 },
750 *spec,
751 );
752 Box(exit_mod_fill().translate_vec2(offset)).child(view)
753 }
754 ExitTransition::ScaleOut { target } => {
755 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
756 Box(exit_mod_fill().transform_origin(0.5, 0.5).scale(s)).child(view)
757 }
758 ExitTransition::ShrinkVertically {
759 clip,
760 shrink_towards,
761 } => apply_size_fraction(
762 &format!("{key}:meas"),
763 &format!("{key}:v{version}:exit:expand_v"),
764 SizeAxis::Vertical,
765 1.0,
766 0.0,
767 *clip,
768 *shrink_towards,
769 *spec,
770 view,
771 ),
772 ExitTransition::ShrinkHorizontally {
773 clip,
774 shrink_towards,
775 } => apply_size_fraction(
776 &format!("{key}:meas"),
777 &format!("{key}:v{version}:exit:expand_h"),
778 SizeAxis::Horizontal,
779 1.0,
780 0.0,
781 *clip,
782 *shrink_towards,
783 *spec,
784 view,
785 ),
786 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
787 &format!("{key}:meas"),
788 &format!("{key}:v{version}:exit:expand_in"),
789 SizeAxis::Both,
790 1.0,
791 0.0,
792 *clip,
793 0.0,
794 *spec,
795 view,
796 ),
797 ExitTransition::Composite(inner) => {
798 let mut v = view;
799 for t in inner {
800 v = apply_exit_single(key, version, t, spec, v);
801 }
802 v
803 }
804 }
805}
806
807fn apply_enter_inflow(
811 key: &str,
812 version: u64,
813 enter: &EnterTransition,
814 spec: &AnimationSpec,
815 view: View,
816) -> View {
817 match enter {
818 EnterTransition::FadeIn => {
819 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
820 Box(flow_mod().alpha(val)).child(view)
821 }
822 EnterTransition::SlideIn { offset_x, offset_y } => {
823 let offset = animate_vec2_from(
824 format!("{key}:v{version}:enter:slide"),
825 Vec2 {
826 x: dp_to_px(*offset_x),
827 y: dp_to_px(*offset_y),
828 },
829 Vec2::default(),
830 *spec,
831 );
832 Box(flow_mod().translate_vec2(offset)).child(view)
833 }
834 EnterTransition::ScaleIn { initial } => {
835 let s = animate_f32_from(
836 format!("{key}:v{version}:enter:scale"),
837 *initial,
838 1.0,
839 *spec,
840 );
841 let a = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
842 Box(flow_mod().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
843 }
844 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
845 &format!("{key}:meas"),
846 &format!("{key}:v{version}:enter:expand_v"),
847 SizeAxis::Vertical,
848 0.0,
849 1.0,
850 *clip,
851 *expand_from,
852 *spec,
853 view,
854 ),
855 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
856 &format!("{key}:meas"),
857 &format!("{key}:v{version}:enter:expand_h"),
858 SizeAxis::Horizontal,
859 0.0,
860 1.0,
861 *clip,
862 *expand_from,
863 *spec,
864 view,
865 ),
866 EnterTransition::ExpandIn { clip } => apply_size_fraction(
867 &format!("{key}:meas"),
868 &format!("{key}:v{version}:enter:expand_in"),
869 SizeAxis::Both,
870 0.0,
871 1.0,
872 *clip,
873 0.0,
874 *spec,
875 view,
876 ),
877 EnterTransition::Composite(transitions) => {
878 let mut v = view;
879 for t in transitions {
880 v = apply_enter_inflow_single(key, version, t, spec, v);
881 }
882 v
883 }
884 }
885}
886
887fn apply_enter_inflow_single(
888 key: &str,
889 version: u64,
890 enter: &EnterTransition,
891 spec: &AnimationSpec,
892 view: View,
893) -> View {
894 match enter {
895 EnterTransition::FadeIn => {
896 let val = animate_f32_from(format!("{key}:v{version}:enter:fade"), 0.0, 1.0, *spec);
897 Box(flow_mod().alpha(val)).child(view)
898 }
899 EnterTransition::SlideIn { offset_x, offset_y } => {
900 let offset = animate_vec2_from(
901 format!("{key}:v{version}:enter:slide"),
902 Vec2 {
903 x: dp_to_px(*offset_x),
904 y: dp_to_px(*offset_y),
905 },
906 Vec2::default(),
907 *spec,
908 );
909 Box(flow_mod().translate_vec2(offset)).child(view)
910 }
911 EnterTransition::ScaleIn { initial } => {
912 let s = animate_f32_from(
913 format!("{key}:v{version}:enter:scale"),
914 *initial,
915 1.0,
916 *spec,
917 );
918 Box(flow_mod().transform_origin(0.5, 0.5).scale(s)).child(view)
919 }
920 EnterTransition::ExpandVertically { clip, expand_from } => apply_size_fraction(
921 &format!("{key}:meas"),
922 &format!("{key}:v{version}:enter:expand_v"),
923 SizeAxis::Vertical,
924 0.0,
925 1.0,
926 *clip,
927 *expand_from,
928 *spec,
929 view,
930 ),
931 EnterTransition::ExpandHorizontally { clip, expand_from } => apply_size_fraction(
932 &format!("{key}:meas"),
933 &format!("{key}:v{version}:enter:expand_h"),
934 SizeAxis::Horizontal,
935 0.0,
936 1.0,
937 *clip,
938 *expand_from,
939 *spec,
940 view,
941 ),
942 EnterTransition::ExpandIn { clip } => apply_size_fraction(
943 &format!("{key}:meas"),
944 &format!("{key}:v{version}:enter:expand_in"),
945 SizeAxis::Both,
946 0.0,
947 1.0,
948 *clip,
949 0.0,
950 *spec,
951 view,
952 ),
953 EnterTransition::Composite(inner) => {
954 let mut v = view;
955 for t in inner {
956 v = apply_enter_inflow_single(key, version, t, spec, v);
957 }
958 v
959 }
960 }
961}
962
963fn apply_exit_inflow(
964 key: &str,
965 version: u64,
966 exit: &ExitTransition,
967 spec: &AnimationSpec,
968 view: View,
969) -> View {
970 match exit {
971 ExitTransition::FadeOut => {
972 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
973 Box(exit_mod_flow().alpha(val)).child(view)
974 }
975 ExitTransition::SlideOut { offset_x, offset_y } => {
976 let offset = animate_vec2_from(
977 format!("{key}:v{version}:exit:slide"),
978 Vec2::default(),
979 Vec2 {
980 x: dp_to_px(*offset_x),
981 y: dp_to_px(*offset_y),
982 },
983 *spec,
984 );
985 Box(exit_mod_flow().translate_vec2(offset)).child(view)
986 }
987 ExitTransition::ScaleOut { target } => {
988 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
989 let a = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
990 Box(exit_mod_flow().transform_origin(0.5, 0.5).scale(s).alpha(a)).child(view)
991 }
992 ExitTransition::ShrinkVertically {
993 clip,
994 shrink_towards,
995 } => apply_size_fraction(
996 &format!("{key}:meas"),
997 &format!("{key}:v{version}:exit:expand_v"),
998 SizeAxis::Vertical,
999 1.0,
1000 0.0,
1001 *clip,
1002 *shrink_towards,
1003 *spec,
1004 view,
1005 ),
1006 ExitTransition::ShrinkHorizontally {
1007 clip,
1008 shrink_towards,
1009 } => apply_size_fraction(
1010 &format!("{key}:meas"),
1011 &format!("{key}:v{version}:exit:expand_h"),
1012 SizeAxis::Horizontal,
1013 1.0,
1014 0.0,
1015 *clip,
1016 *shrink_towards,
1017 *spec,
1018 view,
1019 ),
1020 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
1021 &format!("{key}:meas"),
1022 &format!("{key}:v{version}:exit:expand_in"),
1023 SizeAxis::Both,
1024 1.0,
1025 0.0,
1026 *clip,
1027 0.0,
1028 *spec,
1029 view,
1030 ),
1031 ExitTransition::Composite(transitions) => {
1032 let mut v = view;
1033 for t in transitions {
1034 v = apply_exit_inflow_single(key, version, t, spec, v);
1035 }
1036 v
1037 }
1038 }
1039}
1040
1041fn apply_exit_inflow_single(
1042 key: &str,
1043 version: u64,
1044 exit: &ExitTransition,
1045 spec: &AnimationSpec,
1046 view: View,
1047) -> View {
1048 match exit {
1049 ExitTransition::FadeOut => {
1050 let val = animate_f32_from(format!("{key}:v{version}:exit:fade"), 1.0, 0.0, *spec);
1051 Box(exit_mod_flow().alpha(val)).child(view)
1052 }
1053 ExitTransition::SlideOut { offset_x, offset_y } => {
1054 let offset = animate_vec2_from(
1055 format!("{key}:v{version}:exit:slide"),
1056 Vec2::default(),
1057 Vec2 {
1058 x: dp_to_px(*offset_x),
1059 y: dp_to_px(*offset_y),
1060 },
1061 *spec,
1062 );
1063 Box(exit_mod_flow().translate_vec2(offset)).child(view)
1064 }
1065 ExitTransition::ScaleOut { target } => {
1066 let s = animate_f32_from(format!("{key}:v{version}:exit:scale"), 1.0, *target, *spec);
1067 Box(exit_mod_flow().transform_origin(0.5, 0.5).scale(s)).child(view)
1068 }
1069 ExitTransition::ShrinkVertically {
1070 clip,
1071 shrink_towards,
1072 } => apply_size_fraction(
1073 &format!("{key}:meas"),
1074 &format!("{key}:v{version}:exit:expand_v"),
1075 SizeAxis::Vertical,
1076 1.0,
1077 0.0,
1078 *clip,
1079 *shrink_towards,
1080 *spec,
1081 view,
1082 ),
1083 ExitTransition::ShrinkHorizontally {
1084 clip,
1085 shrink_towards,
1086 } => apply_size_fraction(
1087 &format!("{key}:meas"),
1088 &format!("{key}:v{version}:exit:expand_h"),
1089 SizeAxis::Horizontal,
1090 1.0,
1091 0.0,
1092 *clip,
1093 *shrink_towards,
1094 *spec,
1095 view,
1096 ),
1097 ExitTransition::ShrinkOut { clip } => apply_size_fraction(
1098 &format!("{key}:meas"),
1099 &format!("{key}:v{version}:exit:expand_in"),
1100 SizeAxis::Both,
1101 1.0,
1102 0.0,
1103 *clip,
1104 0.0,
1105 *spec,
1106 view,
1107 ),
1108 ExitTransition::Composite(inner) => {
1109 let mut v = view;
1110 for t in inner {
1111 v = apply_exit_inflow_single(key, version, t, spec, v);
1112 }
1113 v
1114 }
1115 }
1116}
1117
1118fn read_anim_value(key: &str, default: f32) -> Option<f32> {
1121 let anim = remember_state_with_key::<AnimatedValue<f32>>(format!("anim:f32:{key}"), || {
1122 AnimatedValue::new(default, AnimationSpec::default())
1123 });
1124 let a = anim.borrow();
1125 if a.is_animating() {
1126 None
1127 } else {
1128 Some(*a.get())
1129 }
1130}
1131
1132fn read_anim_vec2_value(key: &str, default: Vec2) -> Option<Vec2> {
1133 let anim = remember_state_with_key::<AnimatedValue<Vec2>>(format!("anim:vec2:{key}"), || {
1134 AnimatedValue::new(default, AnimationSpec::default())
1135 });
1136 let a = anim.borrow();
1137 if a.is_animating() {
1138 None
1139 } else {
1140 Some(*a.get())
1141 }
1142}
1143
1144fn exit_animation_done(key: &str, version: u64, exit: &ExitTransition) -> bool {
1146 match exit {
1147 ExitTransition::FadeOut => read_anim_value(&format!("{key}:v{version}:exit:fade"), 1.0)
1148 .map(|v| v < 0.005)
1149 .unwrap_or(false),
1150 ExitTransition::SlideOut { offset_x, offset_y } => {
1151 let target = Vec2 {
1152 x: dp_to_px(*offset_x),
1153 y: dp_to_px(*offset_y),
1154 };
1155 read_anim_vec2_value(&format!("{key}:v{version}:exit:slide"), Vec2::default())
1156 .map(|v| (v.x - target.x).abs() < 0.5 && (v.y - target.y).abs() < 0.5)
1157 .unwrap_or(false)
1158 }
1159 ExitTransition::ScaleOut { target } => {
1160 let done_scale = read_anim_value(&format!("{key}:v{version}:exit:scale"), 1.0)
1161 .map(|v| (v - target).abs() < 0.005)
1162 .unwrap_or(false);
1163 let done_fade = read_anim_value(&format!("{key}:v{version}:exit:fade"), 1.0)
1164 .map(|v| v < 0.005)
1165 .unwrap_or(false);
1166 done_scale && done_fade
1167 }
1168 ExitTransition::ShrinkVertically { .. } => {
1169 read_anim_value(&format!("{key}:v{version}:exit:expand_v"), 1.0)
1170 .map(|v| v < 0.005)
1171 .unwrap_or(false)
1172 }
1173 ExitTransition::ShrinkHorizontally { .. } => {
1174 read_anim_value(&format!("{key}:v{version}:exit:expand_h"), 1.0)
1175 .map(|v| v < 0.005)
1176 .unwrap_or(false)
1177 }
1178 ExitTransition::ShrinkOut { .. } => {
1179 read_anim_value(&format!("{key}:v{version}:exit:expand_in"), 1.0)
1180 .map(|v| v < 0.005)
1181 .unwrap_or(false)
1182 }
1183 ExitTransition::Composite(ts) => ts.iter().all(|t| exit_animation_done(key, version, t)),
1184 }
1185}
1186
1187#[derive(Clone)]
1188pub struct AnimatedVisibilityConfig {
1189 pub key: String,
1190 pub spec: AnimationSpec,
1191 pub enter: EnterTransition,
1192 pub exit: ExitTransition,
1193}
1194
1195impl Default for AnimatedVisibilityConfig {
1196 fn default() -> Self {
1197 Self {
1198 key: "anim_vis".into(),
1199 spec: AnimationSpec::default(),
1200 enter: EnterTransition::default(),
1201 exit: ExitTransition::default(),
1202 }
1203 }
1204}
1205
1206impl AnimatedVisibilityConfig {
1207 pub fn with_key(key: impl Into<String>) -> Self {
1208 Self {
1209 key: key.into(),
1210 ..Default::default()
1211 }
1212 }
1213}
1214
1215pub fn AnimatedVisibility(visible: bool, content: View, config: AnimatedVisibilityConfig) -> View {
1221 let key = config.key;
1222 let spec = config.spec;
1223 let enter = config.enter;
1224 let exit = config.exit;
1225
1226 let old_content = remember_with_key(format!("av_old:{key}"), || RefCell::new(None::<View>));
1227 let version = remember_with_key(format!("av_ver:{key}"), || RefCell::new(0u64));
1228 let prev = remember_with_key(format!("av_prev:{key}"), || RefCell::new(visible));
1229
1230 if *prev.borrow() != visible {
1232 if !visible {
1233 let mut captured = content.clone();
1235 captured.scope_key = Some(format!("av_{key}_old"));
1236 captured.modifier.repaint_boundary = true;
1237 old_content.borrow_mut().replace(captured);
1238 } else {
1239 *old_content.borrow_mut() = None;
1241 }
1242 *version.borrow_mut() += 1;
1243 prev.borrow_mut().clone_from(&visible);
1244 }
1245
1246 let v = *version.borrow();
1247
1248 let exiting = {
1250 let mut oc = old_content.borrow_mut();
1251 if let Some(ref old) = *oc {
1252 if exit_animation_done(&key, v, &exit) {
1253 *oc = None;
1254 None
1255 } else {
1256 let mut exit_view = apply_exit_inflow(&key, v, &exit, &spec, old.clone());
1257 exit_view.modifier.key = Some(transition_child_key(&key, v, "av_exit"));
1258 Some(exit_view)
1259 }
1260 } else {
1261 None
1262 }
1263 };
1264
1265 if visible {
1266 let mut content = content;
1267 content.scope_key = Some(format!("av_{key}_content"));
1268 content.modifier.repaint_boundary = true;
1269 let mut entering = if v > 0 {
1270 apply_enter_inflow(&key, v, &enter, &spec, content)
1271 } else {
1272 content
1273 };
1274 entering.modifier.key = Some(transition_child_key(&key, v, "av_enter"));
1275 entering
1276 } else {
1277 exiting.unwrap_or_else(|| Box(Modifier::new().height(0.0)))
1278 }
1279}