1use std::time::Duration;
50
51use frust_core::{
52 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, FrameTime,
53 InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, Tween, View, Widget, any,
54};
55use frust_theme::{MotionSpring, Theme};
56use kurbo::{Affine, Point, Size};
57
58use crate::nav::transition::{TransitionDriver, make_driver};
59use crate::{Alignment, Timing};
60
61const FALLBACK_EFFECTS_SPRING: MotionSpring = MotionSpring {
70 damping_ratio: 1.0,
71 stiffness: 1600.0,
72};
73
74const FALLBACK_SPATIAL_SPRING: MotionSpring = MotionSpring {
79 damping_ratio: 0.9,
80 stiffness: 700.0,
81};
82
83fn resolve_opacity_timing(theme: Option<&Theme>, explicit: Option<Timing>) -> Timing {
89 explicit.unwrap_or_else(|| {
90 let spring = match theme {
91 Some(theme) => theme.motion.default_effects,
92 None => FALLBACK_EFFECTS_SPRING,
93 };
94 Timing::Spring(spring)
95 })
96}
97
98fn resolve_scale_timing(theme: Option<&Theme>, explicit: Option<Timing>) -> Timing {
105 explicit.unwrap_or_else(|| {
106 let spring = match theme {
107 Some(theme) => theme.motion.default_spatial,
108 None => FALLBACK_SPATIAL_SPRING,
109 };
110 Timing::Spring(spring)
111 })
112}
113
114fn alignment_fraction(component: f64) -> f64 {
119 (component + 1.0) / 2.0
120}
121
122fn scale_about(pivot: Point, scale: f64) -> Affine {
127 Affine::translate((pivot.x, pivot.y))
128 * Affine::scale(scale)
129 * Affine::translate((-pivot.x, -pivot.y))
130}
131
132struct ImplicitAnim {
138 target: f64,
143 driving_target: f64,
147 tween: Tween<f64>,
151 driver: TransitionDriver,
154}
155
156impl ImplicitAnim {
157 fn new(initial: f64) -> Self {
161 let (driver, _) = make_driver(Timing::Duration(Duration::ZERO, frust_core::Curve::Linear));
166 Self {
167 target: initial,
168 driving_target: initial,
169 tween: Tween::new(initial, initial),
170 driver,
171 }
172 }
173
174 fn value(&self) -> f64 {
176 self.tween.lerp(self.driver.value())
177 }
178
179 fn set_target(&mut self, target: f64) {
184 self.target = target;
185 }
186
187 fn advance(&mut self, now: FrameTime, timing: Timing) -> bool {
193 if self.target != self.driving_target {
194 let from = self.value();
195 self.tween = Tween::new(from, self.target);
196 self.driving_target = self.target;
197 let (driver, _) = make_driver(timing);
198 self.driver = driver;
199 }
200 self.driver.advance(now).animating
201 }
202}
203
204pub struct AnimatedOpacityView<State: 'static> {
208 target: f64,
209 timing: Option<Timing>,
210 child: AnyView<State>,
211}
212
213pub fn animated_opacity<State: 'static, V: View<State>>(
218 target: f64,
219 child: V,
220) -> AnimatedOpacityView<State> {
221 AnimatedOpacityView {
222 target: target.clamp(0.0, 1.0),
223 timing: None,
224 child: any(child),
225 }
226}
227
228#[allow(non_snake_case)]
231pub fn AnimatedOpacity<State: 'static, V: View<State>>(
232 target: f64,
233 child: V,
234) -> AnimatedOpacityView<State> {
235 animated_opacity(target, child)
236}
237
238impl<State: 'static> AnimatedOpacityView<State> {
239 pub fn timing(mut self, timing: Timing) -> Self {
241 self.timing = Some(timing);
242 self
243 }
244}
245
246pub struct AnimatedOpacityWidget {
248 child: ChildPod,
249 explicit_timing: Option<Timing>,
250 anim: ImplicitAnim,
251}
252
253impl<State: 'static> View<State> for AnimatedOpacityView<State> {
254 type Element = AnimatedOpacityWidget;
255
256 fn build(&self, ctx: &mut BuildCtx<'_>) -> AnimatedOpacityWidget {
257 AnimatedOpacityWidget {
258 child: crate::authoring::build_child(&self.child, ctx),
259 explicit_timing: self.timing,
260 anim: ImplicitAnim::new(self.target),
261 }
262 }
263
264 fn rebuild(
265 &self,
266 prev: &Self,
267 element: &mut AnimatedOpacityWidget,
268 ctx: &mut BuildCtx<'_>,
269 ) -> ChangeFlags {
270 let mut flags = ChangeFlags::NONE;
271 if prev.target != self.target {
272 element.anim.set_target(self.target);
273 flags |= ChangeFlags::PAINT;
274 }
275 if prev.timing != self.timing {
276 element.explicit_timing = self.timing;
277 flags |= ChangeFlags::PAINT;
278 }
279 flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
280 flags
281 }
282
283 fn teardown(&self, element: &mut AnimatedOpacityWidget, ctx: &mut BuildCtx<'_>) {
284 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
285 }
286}
287
288impl Widget for AnimatedOpacityWidget {
289 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
290 let size = self.child.layout_child(ctx, bc);
292 self.child.set_origin(Point::ZERO);
293 size
294 }
295
296 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
297 let theme = Theme::from_paint_ctx(ctx);
298 let timing = resolve_opacity_timing(theme, self.explicit_timing);
299 if self.anim.advance(ctx.frame_time(), timing) {
300 ctx.request_frame();
301 }
302 let alpha = self.anim.value().clamp(0.0, 1.0) as f32;
307 let origin = ctx.origin();
308 let size = ctx.size();
309 scene.push_layer(origin, size, alpha);
310 self.child.paint_child(ctx, scene);
311 scene.pop_layer();
312 }
313
314 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
315 crate::authoring::route_event_single(&mut self.child, ctx, event)
316 }
317
318 fn semantics(&self, ctx: &mut SemanticsCtx) {
319 self.child.semantics_child(ctx);
320 }
321
322 crate::authoring::visit_children!(child);
323}
324
325pub struct AnimatedScaleView<State: 'static> {
329 target: f64,
330 alignment: Alignment,
331 timing: Option<Timing>,
332 child: AnyView<State>,
333}
334
335pub fn animated_scale<State: 'static, V: View<State>>(
342 target: f64,
343 child: V,
344) -> AnimatedScaleView<State> {
345 AnimatedScaleView {
346 target,
347 alignment: Alignment::CENTER,
348 timing: None,
349 child: any(child),
350 }
351}
352
353#[allow(non_snake_case)]
356pub fn AnimatedScale<State: 'static, V: View<State>>(
357 target: f64,
358 child: V,
359) -> AnimatedScaleView<State> {
360 animated_scale(target, child)
361}
362
363impl<State: 'static> AnimatedScaleView<State> {
364 pub fn alignment(mut self, alignment: Alignment) -> Self {
367 self.alignment = alignment;
368 self
369 }
370
371 pub fn timing(mut self, timing: Timing) -> Self {
373 self.timing = Some(timing);
374 self
375 }
376}
377
378pub struct AnimatedScaleWidget {
380 child: ChildPod,
381 alignment: Alignment,
382 explicit_timing: Option<Timing>,
383 anim: ImplicitAnim,
384}
385
386impl<State: 'static> View<State> for AnimatedScaleView<State> {
387 type Element = AnimatedScaleWidget;
388
389 fn build(&self, ctx: &mut BuildCtx<'_>) -> AnimatedScaleWidget {
390 AnimatedScaleWidget {
391 child: crate::authoring::build_child(&self.child, ctx),
392 alignment: self.alignment,
393 explicit_timing: self.timing,
394 anim: ImplicitAnim::new(self.target),
395 }
396 }
397
398 fn rebuild(
399 &self,
400 prev: &Self,
401 element: &mut AnimatedScaleWidget,
402 ctx: &mut BuildCtx<'_>,
403 ) -> ChangeFlags {
404 let mut flags = ChangeFlags::NONE;
405 if prev.target != self.target {
406 element.anim.set_target(self.target);
407 flags |= ChangeFlags::PAINT;
408 }
409 if prev.alignment != self.alignment {
410 element.alignment = self.alignment;
411 flags |= ChangeFlags::PAINT;
412 }
413 if prev.timing != self.timing {
414 element.explicit_timing = self.timing;
415 flags |= ChangeFlags::PAINT;
416 }
417 flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
418 flags
419 }
420
421 fn teardown(&self, element: &mut AnimatedScaleWidget, ctx: &mut BuildCtx<'_>) {
422 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
423 }
424}
425
426impl Widget for AnimatedScaleWidget {
427 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
428 let size = self.child.layout_child(ctx, bc);
431 self.child.set_origin(Point::ZERO);
432 size
433 }
434
435 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
436 let theme = Theme::from_paint_ctx(ctx);
437 let timing = resolve_scale_timing(theme, self.explicit_timing);
438 if self.anim.advance(ctx.frame_time(), timing) {
439 ctx.request_frame();
440 }
441 let scale = self.anim.value();
445
446 let origin = ctx.origin();
447 let size = ctx.size();
448 let fx = alignment_fraction(self.alignment.x);
449 let fy = alignment_fraction(self.alignment.y);
450 let pivot = Point::new(origin.x + size.width * fx, origin.y + size.height * fy);
451
452 scene.push_transform(scale_about(pivot, scale));
453 self.child.paint_child(ctx, scene);
454 scene.pop_transform();
455 }
456
457 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
458 crate::authoring::route_event_single(&mut self.child, ctx, event)
463 }
464
465 fn semantics(&self, ctx: &mut SemanticsCtx) {
466 self.child.semantics_child(ctx);
467 }
468
469 crate::authoring::visit_children!(child);
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::test_support::{RecordingScene, leaf_any};
476 use frust_core::Curve;
477
478 fn ft_ms(ms: f64) -> FrameTime {
479 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
480 }
481
482 fn build<S: 'static, V: View<S>>(view: &V) -> V::Element {
483 let mut counter = 0u64;
484 view.build(&mut BuildCtx::new(&mut counter))
485 }
486
487 #[test]
504 fn opacity_mid_animation_paint_emits_expected_interpolated_alpha() {
505 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
506 let view: AnimatedOpacityView<()> =
507 animated_opacity(0.0, leaf_any(10.0, 10.0)).timing(timing);
508 let mut w = build(&view);
509 let mut lctx = LayoutCtx::new();
510 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 50.0)));
511
512 w.anim.set_target(1.0);
516
517 let mut pctx = PaintCtx::new(Point::ZERO, size);
520 let mut scene = RecordingScene::default();
521 w.paint(&mut pctx, &mut scene);
522 assert_eq!(
523 scene.layers[0].2, 0.0,
524 "alpha must still be 0.0 on the seed frame"
525 );
526
527 assert!(
529 w.anim.advance(ft_ms(50.0), timing),
530 "must still be animating at the midpoint"
531 );
532
533 let mut pctx2 = PaintCtx::new(Point::ZERO, size);
538 let mut scene2 = RecordingScene::default();
539 w.paint(&mut pctx2, &mut scene2);
540
541 assert_eq!(scene2.layers.len(), 1, "expected one push_layer call");
542 let (layer_origin, layer_size, alpha) = scene2.layers[0];
543 assert_eq!(layer_origin, Point::ZERO);
544 assert_eq!(layer_size, size);
545 assert!(
546 (alpha - 0.5).abs() < 1e-4,
547 "expected ~0.5 alpha halfway through a linear fade, got {alpha}"
548 );
549 assert_eq!(
550 scene2.layer_pops, 1,
551 "push_layer must be popped exactly once"
552 );
553 }
554
555 #[test]
556 fn opacity_settles_at_target_and_stops_requesting_frames() {
557 let timing = Timing::Duration(Duration::from_millis(50), Curve::Linear);
558 let view: AnimatedOpacityView<()> =
559 animated_opacity(0.0, leaf_any(10.0, 10.0)).timing(timing);
560 let mut w = build(&view);
561 let mut lctx = LayoutCtx::new();
562 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 50.0)));
563
564 w.anim.set_target(1.0);
565 let mut running = true;
567 let mut t = 0.0;
568 for _ in 0..1000 {
569 running = w.anim.advance(ft_ms(t), timing);
570 if !running {
571 break;
572 }
573 t += 1000.0 / 120.0;
574 }
575 assert!(
576 !running,
577 "the fade must settle within a bounded number of steps"
578 );
579
580 let mut pctx = PaintCtx::new(Point::ZERO, size);
581 let mut scene = RecordingScene::default();
582 w.paint(&mut pctx, &mut scene);
583
584 assert!(
585 !pctx.needs_frame(),
586 "a settled animation must not request a frame"
587 );
588 let (_, _, alpha) = scene.layers[0];
589 assert!((alpha - 1.0).abs() < 1e-6);
590 }
591
592 #[test]
595 fn scale_emits_push_transform_and_pop_transform_bracketing_the_child() {
596 let view: AnimatedScaleView<()> = animated_scale(1.0, leaf_any(20.0, 20.0))
597 .timing(Timing::Duration(Duration::from_millis(50), Curve::Linear));
598 let mut w = build(&view);
599 let mut lctx = LayoutCtx::new();
600 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 50.0)));
601
602 let mut pctx = PaintCtx::new(Point::ZERO, size);
603 let mut scene = RecordingScene::default();
604 w.paint(&mut pctx, &mut scene);
605
606 assert_eq!(
607 scene.transforms.len(),
608 1,
609 "expected one push_transform call"
610 );
611 assert_eq!(
612 scene.transform_pops, 1,
613 "push_transform must be popped exactly once"
614 );
615 assert_eq!(
616 scene.rects.len(),
617 1,
618 "the child must paint (its fill_rect) between push/pop"
619 );
620 }
621
622 #[test]
623 fn scale_target_change_mid_flight_retargets_without_a_value_jump() {
624 let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
629 let view: AnimatedScaleView<()> = animated_scale(1.0, leaf_any(20.0, 20.0)).timing(timing);
630 let mut w = build(&view);
631 let mut lctx = LayoutCtx::new();
632 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 50.0)));
633
634 w.anim.set_target(2.0);
635 assert!(w.anim.advance(ft_ms(0.0), timing));
638 assert!(w.anim.advance(ft_ms(50.0), timing));
639 let value_before_retarget = w.anim.value();
640 assert!(
641 (value_before_retarget - 1.5).abs() < 1e-6,
642 "expected 1.5 halfway through 1.0 -> 2.0, got {value_before_retarget}"
643 );
644
645 w.anim.set_target(0.5);
647 assert!(w.anim.advance(ft_ms(50.0), timing));
648 let value_after_retarget = w.anim.value();
649
650 assert!(
651 (value_before_retarget - value_after_retarget).abs() < 1e-9,
652 "a retarget must not jump the value: before {value_before_retarget}, after {value_after_retarget}"
653 );
654
655 let mut pctx = PaintCtx::new(Point::ZERO, size);
657 let mut scene = RecordingScene::default();
658 w.paint(&mut pctx, &mut scene);
659 let painted = scene.transforms[0] * Point::new(1.0, 0.0);
660 let pivot_x = size.width / 2.0;
661 let expected_x = pivot_x + (1.0 - pivot_x) * value_after_retarget;
662 assert!(
663 (painted.x - expected_x).abs() < 1e-6,
664 "the painted transform must reflect the just-verified continuous value"
665 );
666 }
667
668 #[test]
669 fn scale_default_timing_resolves_unthemed_fallback_and_animates() {
670 let view: AnimatedScaleView<()> = animated_scale(1.0, leaf_any(10.0, 10.0));
674 let mut w = build(&view);
675 let mut lctx = LayoutCtx::new();
676 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 50.0)));
677
678 w.anim.set_target(2.0);
679 let mut pctx = PaintCtx::new(Point::ZERO, size);
680 let mut scene = RecordingScene::default();
681 w.paint(&mut pctx, &mut scene);
682
683 assert_eq!(scene.transforms.len(), 1);
684 assert!(
685 pctx.needs_frame(),
686 "a freshly retargeted spring must request another frame"
687 );
688 }
689
690 #[test]
691 fn rebuild_forwards_a_target_change_into_the_retained_anim() {
692 let view: AnimatedOpacityView<()> = animated_opacity(0.0, leaf_any(10.0, 10.0));
697 let mut w = build(&view);
698
699 let retarget: AnimatedOpacityView<()> = animated_opacity(1.0, leaf_any(10.0, 10.0));
700 let mut counter = 0u64;
701 let flags = retarget.rebuild(&view, &mut w, &mut BuildCtx::new(&mut counter));
702
703 assert_eq!(flags, ChangeFlags::PAINT);
704 assert_eq!(w.anim.target, 1.0, "rebuild must forward the new target");
705 }
706}