frust_widgets/nav/hero.rs
1//! Shared-element ("hero") transition wrapper: a transparent single-child
2//! container that lets a [`navigator`](super::navigator) morph a tagged
3//! element between two pages during a page transition.
4//!
5//! # How it works
6//!
7//! [`hero(tag, child)`](hero) wraps `child` and, on every paint, reports its
8//! own absolute bounds under `tag` to whatever tagged-rect reporter the
9//! navigator installed over the page
10//! ([`PaintCtx::report_hero`](frust_core::PaintCtx::report_hero) — a no-op
11//! returning [`HeroDirective::Normal`] the rest of the time, so a hero outside
12//! a transition is a zero-cost transparent wrapper). When a transition is in
13//! flight and the **same** tag is present on both the outgoing and incoming
14//! page, the navigator hands one endpoint a [`HeroDirective::Morph`] (repaint
15//! the child under a rect→rect transform onto the interpolated morph rect) and
16//! the other a [`HeroDirective::Suppress`] (skip painting, so the element does
17//! not also show at its rest position). The morph is painted by the hero on the
18//! page drawn last (on top), keeping the overlay above both page layers whether
19//! the transition is a push or a pop — see [`super::navigator`]'s
20//! `paint_transition`.
21//!
22//! # Contract
23//!
24//! A hero is a *transparent* layout wrapper: it takes its child's size and
25//! places it at the origin, forwards events and semantics to the child
26//! unchanged, and never mutates app state. Heroes activate on any push/pop
27//! transition where both pages carry the tag — no `Route`/`Router` change is
28//! required.
29
30use frust_core::{
31 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, HeroDirective,
32 InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
33};
34use kurbo::{Point, Rect, Size};
35
36use super::transition::rect_to_rect;
37
38/// A declarative shared-element wrapper. See the [module docs](self).
39pub struct HeroView<State: 'static> {
40 tag: String,
41 child: AnyView<State>,
42}
43
44/// Tag `child` as a shared element under `tag`, so a [`navigator`](super::navigator)
45/// morphs it between two pages that both carry the same tag during a page
46/// transition. Outside a transition this is a transparent wrapper.
47pub fn hero<State: 'static, V: View<State>>(tag: impl Into<String>, child: V) -> HeroView<State> {
48 HeroView {
49 tag: tag.into(),
50 child: any(child),
51 }
52}
53
54/// The retained widget for a [`HeroView`].
55pub struct HeroWidget {
56 tag: String,
57 child: ChildPod,
58}
59
60impl<State: 'static> View<State> for HeroView<State> {
61 type Element = HeroWidget;
62
63 fn build(&self, ctx: &mut BuildCtx<'_>) -> HeroWidget {
64 HeroWidget {
65 tag: self.tag.clone(),
66 child: crate::authoring::build_child(&self.child, ctx),
67 }
68 }
69
70 fn rebuild(
71 &self,
72 prev: &Self,
73 element: &mut HeroWidget,
74 ctx: &mut BuildCtx<'_>,
75 ) -> ChangeFlags {
76 let mut flags = ChangeFlags::NONE;
77 if prev.tag != self.tag {
78 element.tag = self.tag.clone();
79 // A tag change only affects which heroes match during a transition —
80 // no relayout, just a repaint.
81 flags |= ChangeFlags::PAINT;
82 }
83 flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
84 flags
85 }
86
87 fn teardown(&self, element: &mut HeroWidget, ctx: &mut BuildCtx<'_>) {
88 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
89 }
90}
91
92impl Widget for HeroWidget {
93 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
94 // Transparent wrapper: the hero is exactly its child, placed at the
95 // origin — so the bounds it reports on paint are the child's own.
96 let size = self.child.layout_child(ctx, bc);
97 self.child.set_origin(Point::ZERO);
98 size
99 }
100
101 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
102 let bounds = Rect::from_origin_size(ctx.origin(), ctx.size());
103 match ctx.report_hero(&self.tag, bounds) {
104 // Common case (no flight, or a tag on one side only): paint normally.
105 HeroDirective::Normal => self.child.paint_child(ctx, scene),
106 // This endpoint is being morphed by its counterpart on the other
107 // page — do not also paint it at its rest position.
108 HeroDirective::Suppress => {}
109 // Paint the morph overlay: repaint the retained child subtree under
110 // a transform mapping our own paint bounds onto the interpolated
111 // destination rect (position + scale).
112 HeroDirective::Morph { dest } => {
113 scene.push_transform(rect_to_rect(bounds, dest));
114 self.child.paint_child(ctx, scene);
115 scene.pop_transform();
116 }
117 }
118 }
119
120 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
121 crate::authoring::route_event_single(&mut self.child, ctx, event)
122 }
123
124 fn semantics(&self, ctx: &mut SemanticsCtx) {
125 // Transparent wrapper: forward to the single child.
126 self.child.semantics_child(ctx);
127 }
128
129 crate::authoring::visit_children!(child);
130}