Skip to main content

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}