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