1use std::cell::{OnceCell, RefCell};
12
13use frus_core::animation::Curve;
14use frus_core::{Alignment, Rect, Scene};
15use frus_layout::Style;
16
17use crate::interaction::Status;
18use crate::theme::Theme;
19use crate::widget::Widget;
20
21const FIRST: u8 = 0;
23const SECOND: u8 = 1;
24
25pub struct AnimatedCrossFade<Msg = crate::callback::Callback> {
42 first: RefCell<Option<Box<dyn Widget<Msg>>>>,
43 second: RefCell<Option<Box<dyn Widget<Msg>>>>,
44 show_first: bool,
45 duration: f32,
46 first_curve: Curve,
47 second_curve: Curve,
48 size_curve: Curve,
49 alignment: Alignment,
50 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
53}
54
55impl<Msg: Clone + 'static> AnimatedCrossFade<Msg> {
56 pub fn new(
59 first: impl Widget<Msg> + 'static,
60 second: impl Widget<Msg> + 'static,
61 show_first: bool,
62 duration: f32,
63 ) -> Self {
64 Self {
65 first: RefCell::new(Some(Box::new(first))),
66 second: RefCell::new(Some(Box::new(second))),
67 show_first,
68 duration,
69 first_curve: Curve::Linear,
70 second_curve: Curve::Linear,
71 size_curve: Curve::Linear,
72 alignment: Alignment::TOP_CENTER,
73 built: OnceCell::new(),
74 }
75 }
76
77 pub fn first_curve(mut self, curve: Curve) -> Self {
79 self.first_curve = curve;
80 self
81 }
82
83 pub fn second_curve(mut self, curve: Curve) -> Self {
85 self.second_curve = curve;
86 self
87 }
88
89 pub fn size_curve(mut self, curve: Curve) -> Self {
91 self.size_curve = curve;
92 self
93 }
94
95 pub fn alignment(mut self, alignment: Alignment) -> Self {
98 self.alignment = alignment;
99 self
100 }
101
102 fn slot(&self, which: u8, child: Box<dyn Widget<Msg>>, curve: Curve) -> Box<dyn Widget<Msg>> {
108 let shown = (which == FIRST) == self.show_first;
109 let placed: Box<dyn Widget<Msg>> = if shown {
110 child
111 } else {
112 Box::new(
113 crate::OverflowBox::new(child)
114 .natural_height()
115 .alignment(self.alignment),
116 )
117 };
118 let faded = crate::AnimatedOpacity::new(
119 if shown { 1.0 } else { 0.0 },
120 self.duration,
121 curve,
122 FillWidth {
123 children: vec![placed],
124 },
125 );
126 Box::new(crate::Keyed::new(
127 ("cross-fade", which),
128 crate::IgnorePointer::new(crate::ExcludeSemantics::new(faded).excluding(!shown))
129 .ignoring(!shown),
130 ))
131 }
132
133 fn assemble(&self) -> Vec<Box<dyn Widget<Msg>>> {
134 let first = self.first.borrow_mut().take().expect("assembled once");
135 let second = self.second.borrow_mut().take().expect("assembled once");
136 let first = self.slot(FIRST, first, self.first_curve.clone());
137 let second = self.slot(SECOND, second, self.second_curve.clone());
138 let layers = if self.show_first {
140 vec![second, first]
141 } else {
142 vec![first, second]
143 };
144 vec![Box::new(
145 crate::AnimatedSize::new(self.duration, Cell { layers })
146 .curve(self.size_curve.clone())
147 .alignment(self.alignment),
148 )]
149 }
150}
151
152impl<Msg: Clone + 'static> Widget<Msg> for AnimatedCrossFade<Msg> {
153 fn style(&self) -> Style {
154 Style::default()
155 }
156
157 fn build_themed(&self, _theme: &Theme) {
158 self.built.get_or_init(|| self.assemble());
159 }
160
161 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
162 self.built.get().map(|v| &v[..]).unwrap_or(&[])
163 }
164
165 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
166
167 fn on_click(&self) -> Option<Msg> {
168 None
169 }
170
171 fn debug_name(&self) -> &'static str {
172 "AnimatedCrossFade"
173 }
174}
175
176struct Cell<Msg> {
179 layers: Vec<Box<dyn Widget<Msg>>>,
180}
181
182impl<Msg: Clone> Widget<Msg> for Cell<Msg> {
183 fn style(&self) -> Style {
184 Style {
185 overlap: true,
186 align: frus_layout::Align::Stretch,
187 ..Style::default()
188 }
189 }
190
191 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
192 &self.layers
193 }
194
195 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
196
197 fn on_click(&self) -> Option<Msg> {
198 None
199 }
200
201 fn debug_name(&self) -> &'static str {
202 "AnimatedCrossFade"
203 }
204}
205
206struct FillWidth<Msg> {
210 children: Vec<Box<dyn Widget<Msg>>>,
211}
212
213impl<Msg: Clone> Widget<Msg> for FillWidth<Msg> {
214 fn style(&self) -> Style {
215 Style {
216 flex_direction: frus_layout::FlexDirection::Column,
217 align: frus_layout::Align::Stretch,
218 ..Style::default()
219 }
220 }
221
222 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
223 crate::widget::FillAxes {
224 horizontal: true,
225 vertical: false,
226 }
227 }
228
229 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
230 &self.children
231 }
232
233 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
234
235 fn on_click(&self) -> Option<Msg> {
236 None
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::{build_ui, Container, Runtime, Size};
244 use frus_core::Color;
245
246 #[derive(Clone, Debug, PartialEq)]
247 enum Msg {
248 First,
249 Second,
250 }
251
252 const SIZE: Size = Size::new(300.0, 600.0);
253 const BLUE: Color = Color::rgb(0.2, 0.4, 0.8);
254 const RED: Color = Color::rgb(0.8, 0.2, 0.2);
255
256 fn page(show_first: bool) -> crate::Flex<Msg> {
259 let block = |height: f32, color: Color, msg: Msg| {
263 crate::GestureDetector::new(
264 Container::<Msg>::new()
265 .color(color)
266 .child(Container::<Msg>::new().height(height)),
267 )
268 .on_tap(msg)
269 };
270 crate::Flex::<Msg>::column()
271 .child(AnimatedCrossFade::new(
272 block(40.0, BLUE, Msg::First),
273 block(100.0, RED, Msg::Second),
274 show_first,
275 0.2,
276 ))
277 .child(
278 Container::<Msg>::new()
279 .height(10.0)
280 .color(Color::rgb(0.1, 0.9, 0.1)),
281 )
282 }
283
284 fn bottom(ui: &crate::Ui<Msg>) -> f32 {
286 ui.scene()
287 .primitives()
288 .iter()
289 .find_map(|p| match p {
290 frus_core::Primitive::Rect { rect, color, .. }
291 if *color == Color::rgb(0.1, 0.9, 0.1) =>
292 {
293 Some(rect.y)
294 }
295 _ => None,
296 })
297 .expect("the marker")
298 }
299
300 fn frame(rt: &mut Runtime, show_first: bool, dt: f32) -> crate::Ui<Msg> {
303 let tree = page(show_first);
304 let theme = Theme::default();
305 crate::build_deferred(&tree, &theme, rt);
306 let ui = build_ui(&tree, SIZE, rt, &theme);
307 rt.advance(dt);
308 rt.advance_values(&tree, dt);
309 ui
310 }
311
312 fn tap(ui: &crate::Ui<Msg>) -> Option<Msg> {
314 ui.hit(frus_core::Point::new(150.0, 20.0))
315 .and_then(|id| ui.msg_for(id))
316 }
317
318 fn painted(ui: &crate::Ui<Msg>, color: Color) -> Option<f32> {
320 fn walk(primitives: &[frus_core::Primitive], color: Color, alpha: f32) -> Option<f32> {
321 for p in primitives {
322 match p {
323 frus_core::Primitive::Rect { color: c, .. } if *c == color => {
324 return Some(alpha)
325 }
326 frus_core::Primitive::Layer {
327 primitives,
328 opacity,
329 ..
330 } => {
331 if let Some(found) = walk(primitives, color, alpha * opacity) {
332 return Some(found);
333 }
334 }
335 _ => {}
336 }
337 }
338 None
339 }
340 walk(ui.scene().primitives(), color, 1.0).filter(|a| *a > 0.0)
341 }
342
343 #[test]
346 fn at_rest_only_the_shown_child_counts() {
347 let rt = Runtime::default();
348 let ui = build_ui(&page(true), SIZE, &rt, &Theme::default());
349 assert_eq!(bottom(&ui), 40.0);
350 assert_eq!(painted(&ui, BLUE), Some(1.0));
351 assert_eq!(painted(&ui, RED), None);
352 assert_eq!(tap(&ui), Some(Msg::First));
353 }
354
355 #[test]
359 fn a_switch_cross_fades_and_moves_the_box() {
360 let mut rt = Runtime::default();
361 frame(&mut rt, true, 0.016);
362 frame(&mut rt, false, 0.1);
363 let half = frame(&mut rt, false, 0.016);
364 let b = bottom(&half);
365 assert!(b > 45.0 && b < 95.0, "between the two sizes: {b}");
366 let (blue, red) = (painted(&half, BLUE), painted(&half, RED));
367 assert!(
368 blue.is_some_and(|a| a < 1.0) && red.is_some_and(|a| a < 1.0),
369 "both on the way: {blue:?} {red:?}"
370 );
371 assert_eq!(
372 tap(&half),
373 Some(Msg::Second),
374 "the one leaving takes nothing"
375 );
376 let order: Vec<Color> = {
378 fn walk(primitives: &[frus_core::Primitive], out: &mut Vec<Color>) {
379 for p in primitives {
380 match p {
381 frus_core::Primitive::Rect { color, .. } => out.push(*color),
382 frus_core::Primitive::Layer { primitives, .. } => walk(primitives, out),
383 _ => {}
384 }
385 }
386 }
387 let mut out = Vec::new();
388 walk(half.scene().primitives(), &mut out);
389 out
390 };
391 let blue_at = order.iter().position(|c| *c == BLUE);
392 let red_at = order.iter().position(|c| *c == RED);
393 assert!(red_at > blue_at, "the red one over the blue one: {order:?}");
394 for _ in 0..20 {
395 frame(&mut rt, false, 0.016);
396 }
397 let end = frame(&mut rt, false, 0.016);
398 assert_eq!(bottom(&end), 100.0);
399 assert_eq!(painted(&end, BLUE), None);
400 assert_eq!(painted(&end, RED), Some(1.0));
401 }
402
403 #[test]
407 fn the_hidden_child_is_neither_touched_nor_read() {
408 let hidden = crate::Semantics::new(
409 frus_core::SemanticsProperties::default(),
410 crate::GestureDetector::new(
413 Container::<Msg>::new()
414 .width(SIZE.width)
415 .height(100.0)
416 .color(RED),
417 )
418 .on_tap(Msg::Second),
419 )
420 .label("hidden");
421 let tree = AnimatedCrossFade::new(
422 Container::<Msg>::new().height(40.0).color(BLUE),
423 hidden,
424 true,
425 0.2,
426 );
427 let rt = Runtime::default();
428 let ui = build_ui(&tree, SIZE, &rt, &Theme::default());
429 assert_eq!(tap(&ui), None, "nothing to tap on the shown child");
430 assert!(
431 !ui.semantics()
432 .iter()
433 .any(|(_, _, props)| props.label.as_deref() == Some("hidden")),
434 "the hidden child is not read out"
435 );
436 }
437
438 #[test]
442 fn the_leaving_child_is_cut_to_the_box() {
443 let mut rt = Runtime::default();
444 frame(&mut rt, false, 0.016);
445 frame(&mut rt, true, 0.1);
446 let half = frame(&mut rt, true, 0.016);
447 let edge = bottom(&half);
448 fn find(primitives: &[frus_core::Primitive]) -> Option<(Rect, Rect)> {
449 primitives.iter().find_map(|p| match p {
450 frus_core::Primitive::Rect {
451 rect, clip, color, ..
452 } if *color == RED => Some((*rect, *clip)),
453 frus_core::Primitive::Layer { primitives, .. } => find(primitives),
454 _ => None,
455 })
456 }
457 let (rect, clip) = find(half.scene().primitives()).expect("the red block, fading");
458 assert_eq!(rect.height, 100.0, "its own height");
459 assert!(
460 clip.y + clip.height <= edge + 0.5,
461 "cut at {edge}: {clip:?}"
462 );
463 assert!(edge < 95.0, "the box is on its way down: {edge}");
464 }
465}