1use std::hash::{Hash, Hasher};
28
29use frus_core::{Rect, Scene};
30use frus_layout::Style;
31
32use crate::interaction::{Status, WidgetId};
33use crate::theme::Theme;
34use crate::widget::{sizing_of, Widget};
35
36#[derive(Copy, Clone, Debug, PartialEq)]
39pub struct HeroSpot {
40 pub tag: u64,
42 pub screen: u8,
45 pub id: WidgetId,
47 pub rect: Rect,
49}
50
51pub fn lerp_rect(from: Rect, to: Rect, t: f32) -> Rect {
57 let mix = |a: f32, b: f32| a + (b - a) * t;
58 Rect::new(
59 mix(from.x, to.x),
60 mix(from.y, to.y),
61 mix(from.width, to.width),
62 mix(from.height, to.height),
63 )
64}
65
66pub struct Hero<Msg = crate::callback::Callback> {
69 tag: u64,
70 children: Vec<Box<dyn Widget<Msg>>>,
71}
72
73impl<Msg> Hero<Msg> {
74 pub fn new(tag: impl Hash, child: impl Widget<Msg> + 'static) -> Self {
77 let mut hasher = std::collections::hash_map::DefaultHasher::new();
78 tag.hash(&mut hasher);
79 Self {
80 tag: hasher.finish(),
81 children: vec![Box::new(child)],
82 }
83 }
84
85 pub fn tag(&self) -> u64 {
87 self.tag
88 }
89}
90
91impl<Msg: Clone> Widget<Msg> for Hero<Msg> {
92 fn style(&self) -> Style {
93 sizing_of(
94 self.children
95 .first()
96 .map(|child| child.style())
97 .unwrap_or_default(),
98 )
99 }
100
101 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
102 &self.children
103 }
104
105 fn stack(&self) -> bool {
108 self.children.first().is_some_and(|child| child.stack())
109 }
110
111 fn continuous(&self) -> bool {
112 self.children
113 .first()
114 .is_some_and(|child| child.continuous())
115 }
116
117 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
118 }
120
121 fn on_click(&self) -> Option<Msg> {
122 None
123 }
124
125 fn hero_tag(&self) -> Option<u64> {
126 Some(self.tag)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::{build_ui, Container, Navigator, Runtime};
134 use frus_core::{Color, Primitive, Size};
135
136 const RED: Color = Color {
137 r: 1.0,
138 g: 0.0,
139 b: 0.0,
140 a: 1.0,
141 };
142
143 fn flight(progress: f32, tags: (u64, u64)) -> Vec<Rect> {
146 let leaving = Container::<()>::new()
147 .width(400.0)
148 .height(300.0)
149 .child(Hero::new(
150 tags.0,
151 Container::new().width(40.0).height(40.0).color(RED),
152 ));
153 let entering = Container::<()>::new()
154 .width(400.0)
155 .height(300.0)
156 .child(Hero::new(
157 tags.1,
158 Container::new().width(200.0).height(200.0).color(RED),
159 ));
160 let navigator = Navigator::new("entering", entering)
161 .size(400.0, 300.0)
162 .from("leaving", leaving, progress, true);
163 let runtime = Runtime::default();
164 let ui = build_ui(
165 &navigator,
166 Size::new(400.0, 300.0),
167 &runtime,
168 &Theme::dark().with_platform(frus_core::TargetPlatform::Linux),
171 );
172 fn red(primitives: &[Primitive], out: &mut Vec<Rect>) {
174 for p in primitives {
175 match p {
176 Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
177 out.push(*rect)
178 }
179 Primitive::Layer { primitives, .. } => red(primitives, out),
180 _ => {}
181 }
182 }
183 }
184 let mut found = Vec::new();
185 red(ui.scene().primitives(), &mut found);
186 found
187 }
188
189 #[test]
190 fn a_matched_pair_flies_as_one_thing() {
191 let mid = flight(0.5, (7, 7));
194 assert_eq!(mid.len(), 1, "one thing in one place: {mid:?}");
195 let size = mid[0].width;
196 assert!(
197 size > 45.0 && size < 195.0,
198 "between the two sizes, not at either: {size}"
199 );
200 }
201
202 #[test]
203 fn the_flight_starts_where_it_was_and_ends_where_it_is_going() {
204 let start = flight(0.0, (7, 7));
205 assert_eq!(start.len(), 1);
206 assert!(
207 (start[0].width - 40.0).abs() < 1.0,
208 "starts at the source's size: {:?}",
209 start[0]
210 );
211 let end = flight(1.0, (7, 7));
212 assert_eq!(end.len(), 1);
213 assert!(
214 (end[0].width - 200.0).abs() < 1.0,
215 "ends at the destination's size: {:?}",
216 end[0]
217 );
218 }
219
220 #[test]
221 fn heroes_that_do_not_match_are_left_alone() {
222 let mid = flight(0.5, (7, 8));
225 assert_eq!(mid.len(), 2, "both drawn as usual: {mid:?}");
226 }
227
228 #[test]
229 fn a_tag_is_whatever_says_the_same() {
230 let by_number = Hero::<()>::new(42u64, Container::new());
231 let same = Hero::<()>::new(42u64, Container::new());
232 let other = Hero::<()>::new(43u64, Container::new());
233 assert_eq!(by_number.tag(), same.tag());
234 assert_ne!(by_number.tag(), other.tag());
235 let by_name = Hero::<()>::new("avatar", Container::new());
237 assert_eq!(
238 by_name.tag(),
239 Hero::<()>::new("avatar", Container::new()).tag()
240 );
241 }
242}