1use std::collections::{HashMap, HashSet};
14
15use frus_core::animation::Curve;
16use frus_core::{Alignment, Rect, Scene, Size};
17use frus_layout::Style;
18
19use crate::constraints::{AxisConstraint, ConstraintsTransform};
20use crate::interaction::{Status, WidgetId};
21use crate::theme::Theme;
22use crate::widget::Widget;
23
24#[derive(Clone, Debug)]
27pub struct SizeAnimation {
28 pub duration: f32,
30 pub curve: Curve,
32 pub alignment: Alignment,
34 pub clip: bool,
37}
38
39#[derive(Clone, Debug)]
41struct Moving {
42 from: Size,
43 to: Size,
44 elapsed: f32,
45 duration: f32,
46 curve: Curve,
47 seen: Option<(Size, SizeAnimation)>,
50 fresh: bool,
52}
53
54impl Moving {
55 fn settled(size: Size) -> Self {
56 Self {
57 from: size,
58 to: size,
59 elapsed: 0.0,
60 duration: 0.0,
61 curve: Curve::Linear,
62 seen: None,
63 fresh: false,
64 }
65 }
66
67 fn done(&self) -> bool {
68 self.elapsed >= self.duration
69 }
70
71 fn current(&self) -> Size {
72 if self.done() {
73 return self.to;
74 }
75 let t = self
76 .curve
77 .transform((self.elapsed / self.duration).clamp(0.0, 1.0));
78 Size::new(
79 self.from.width + (self.to.width - self.from.width) * t,
80 self.from.height + (self.to.height - self.from.height) * t,
81 )
82 }
83}
84
85#[derive(Default, Debug)]
87pub(crate) struct SizeAnims {
88 moving: HashMap<WidgetId, Moving>,
89 seen: HashSet<WidgetId>,
91}
92
93fn same(a: Size, b: Size) -> bool {
96 (a.width - b.width).abs() < 0.01 && (a.height - b.height).abs() < 0.01
97}
98
99impl SizeAnims {
100 pub(crate) fn observe(
115 &mut self,
116 id: WidgetId,
117 child: Size,
118 spec: &SizeAnimation,
119 record: bool,
120 ) -> Size {
121 self.seen.insert(id);
122 match self.moving.get_mut(&id) {
123 None => {
124 if record {
125 let mut fresh = Moving::settled(child);
126 fresh.fresh = true;
127 self.moving.insert(id, fresh);
128 }
129 child
130 }
131 Some(moving) => {
132 if record {
133 if moving.fresh {
134 moving.from = child;
136 moving.to = child;
137 } else {
138 moving.seen = Some((child, spec.clone()));
139 }
140 }
141 moving.current()
142 }
143 }
144 }
145
146 fn take_up(moving: &mut Moving) {
149 moving.fresh = false;
150 let Some((child, spec)) = moving.seen.take() else {
151 return;
152 };
153 if !same(moving.to, child) {
154 *moving = Moving {
155 from: moving.current(),
156 to: child,
157 elapsed: 0.0,
158 duration: spec.duration.max(0.0),
159 curve: spec.curve,
160 seen: None,
161 fresh: false,
162 };
163 }
164 }
165
166 pub(crate) fn fingerprint(&mut self, id: WidgetId) -> Option<(Size, Size)> {
170 self.seen.insert(id);
171 self.moving.get(&id).map(|m| (m.current(), m.to))
172 }
173
174 #[cfg(test)]
176 fn target(&self, id: WidgetId) -> Option<Size> {
177 self.moving.get(&id).map(|m| m.to)
178 }
179
180 pub(crate) fn advance(&mut self, dt: f32, still: bool) -> bool {
184 let seen = std::mem::take(&mut self.seen);
185 self.moving.retain(|id, _| seen.contains(id));
186 let mut animating = false;
187 for moving in self.moving.values_mut() {
188 Self::take_up(moving);
189 if moving.done() {
190 continue;
191 }
192 moving.elapsed = if still {
193 moving.duration
194 } else {
195 moving.elapsed + dt
196 };
197 animating |= !moving.done();
198 }
199 animating
200 }
201}
202
203pub struct AnimatedSize<Msg = crate::callback::Callback> {
225 transform: ConstraintsTransform,
226 spec: SizeAnimation,
227 children: Vec<Box<dyn Widget<Msg>>>,
228}
229
230impl<Msg> AnimatedSize<Msg> {
231 pub fn new(duration: f32, child: impl Widget<Msg> + 'static) -> Self {
234 Self {
235 transform: ConstraintsTransform {
236 width: AxisConstraint::AsGiven,
237 height: AxisConstraint::Unbounded,
238 alignment: Alignment::CENTER,
239 report: false,
240 },
241 spec: SizeAnimation {
242 duration,
243 curve: Curve::Linear,
244 alignment: Alignment::CENTER,
245 clip: true,
246 },
247 children: vec![Box::new(child)],
248 }
249 }
250
251 pub fn curve(mut self, curve: Curve) -> Self {
253 self.spec.curve = curve;
254 self
255 }
256
257 pub fn alignment(mut self, alignment: Alignment) -> Self {
259 self.spec.alignment = alignment;
260 self.transform.alignment = alignment;
261 self
262 }
263
264 pub fn clip(mut self, clip: bool) -> Self {
266 self.spec.clip = clip;
267 self
268 }
269
270 pub fn width(mut self, width: AxisConstraint) -> Self {
272 self.transform.width = width;
273 self
274 }
275
276 pub fn height(mut self, height: AxisConstraint) -> Self {
278 self.transform.height = height;
279 self
280 }
281}
282
283impl<Msg: Clone> Widget<Msg> for AnimatedSize<Msg> {
284 fn style(&self) -> Style {
285 Style::default()
286 }
287
288 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
289 &self.children
290 }
291
292 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
293
294 fn on_click(&self) -> Option<Msg> {
295 None
296 }
297
298 fn constraints_transform(&self) -> Option<ConstraintsTransform> {
299 Some(self.transform)
300 }
301
302 fn animated_size(&self) -> Option<SizeAnimation> {
303 Some(self.spec.clone())
304 }
305
306 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
310 crate::widget::FillAxes {
311 horizontal: self.transform.width == AxisConstraint::AsGiven,
312 vertical: self.transform.height == AxisConstraint::AsGiven,
313 }
314 }
315
316 fn debug_name(&self) -> &'static str {
317 "AnimatedSize"
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::{build_ui, Container, Flex, Runtime};
325
326 fn spec(duration: f32) -> SizeAnimation {
327 SizeAnimation {
328 duration,
329 curve: Curve::Linear,
330 alignment: Alignment::CENTER,
331 clip: true,
332 }
333 }
334
335 const ID: WidgetId = WidgetId::ROOT;
336
337 #[test]
339 fn a_new_box_starts_at_its_childs_size() {
340 let mut anims = SizeAnims::default();
341 let shown = anims.observe(ID, Size::new(100.0, 40.0), &spec(0.2), true);
342 assert_eq!(shown, Size::new(100.0, 40.0));
343 assert!(!anims.advance(0.016, false), "nothing is moving");
344 }
345
346 #[test]
349 fn a_grown_child_is_followed_over_the_duration() {
350 let mut anims = SizeAnims::default();
351 anims.observe(ID, Size::new(100.0, 40.0), &spec(0.2), true);
352 anims.advance(0.0, false);
354 let now = anims.observe(ID, Size::new(100.0, 140.0), &spec(0.2), true);
355 assert_eq!(now.height, 40.0, "it starts where it was");
356 assert!(anims.advance(0.1, false));
357 let half = anims.observe(ID, Size::new(100.0, 140.0), &spec(0.2), true);
358 assert!((half.height - 90.0).abs() < 0.01, "{half:?}");
359 assert!(!anims.advance(0.1, false));
360 assert_eq!(
361 anims.observe(ID, Size::new(100.0, 140.0), &spec(0.2), true),
362 Size::new(100.0, 140.0)
363 );
364 }
365
366 #[test]
368 fn a_change_mid_move_starts_from_where_it_is() {
369 let mut anims = SizeAnims::default();
370 anims.observe(ID, Size::new(100.0, 0.0), &spec(0.2), true);
371 anims.advance(0.0, false);
372 anims.observe(ID, Size::new(100.0, 200.0), &spec(0.2), true);
373 anims.advance(0.1, false);
374 let back = anims.observe(ID, Size::new(100.0, 0.0), &spec(0.2), true);
375 assert!((back.height - 100.0).abs() < 0.01, "{back:?}");
376 anims.advance(0.1, false);
377 let later = anims.observe(ID, Size::new(100.0, 0.0), &spec(0.2), true);
378 assert!((later.height - 50.0).abs() < 0.01, "{later:?}");
379 }
380
381 #[test]
386 fn the_questions_of_one_frame_are_not_a_change() {
387 let mut anims = SizeAnims::default();
388 let asked = |anims: &mut SizeAnims| {
389 for width in [297.0, 481.0, 312.0] {
390 anims.observe(ID, Size::new(width, 22.0), &spec(0.2), true);
391 }
392 };
393 asked(&mut anims);
394 assert!(!anims.advance(0.016, false), "it appeared at its size");
395 for _ in 0..3 {
396 asked(&mut anims);
397 assert!(
398 !anims.advance(0.016, false),
399 "the same answers: nothing to follow"
400 );
401 }
402 for width in [297.0, 481.0, 350.0] {
404 anims.observe(ID, Size::new(width, 22.0), &spec(0.2), true);
405 }
406 assert!(anims.advance(0.016, false), "a new last answer is followed");
407 }
408
409 #[test]
411 fn a_probe_is_not_a_change() {
412 let mut anims = SizeAnims::default();
413 anims.observe(ID, Size::new(100.0, 40.0), &spec(0.2), true);
414 let probe = anims.observe(ID, Size::new(0.0, 900.0), &spec(0.2), false);
415 assert_eq!(probe, Size::new(100.0, 40.0));
416 assert!(!anims.advance(0.016, false));
417 }
418
419 #[test]
421 fn reduced_motion_arrives_at_once() {
422 let mut anims = SizeAnims::default();
423 anims.observe(ID, Size::new(100.0, 40.0), &spec(0.2), true);
424 anims.observe(ID, Size::new(100.0, 140.0), &spec(0.2), true);
425 assert!(!anims.advance(0.001, true));
426 assert_eq!(
427 anims.observe(ID, Size::new(100.0, 140.0), &spec(0.2), true),
428 Size::new(100.0, 140.0)
429 );
430 }
431
432 #[test]
434 fn a_box_that_has_gone_is_forgotten() {
435 let mut anims = SizeAnims::default();
436 anims.observe(ID, Size::new(100.0, 40.0), &spec(0.2), true);
437 anims.advance(0.016, false);
438 assert!(anims.target(ID).is_some());
439 anims.advance(0.016, false);
440 assert!(anims.target(ID).is_none());
441 }
442
443 fn page(rows: usize) -> Flex<()> {
444 let mut body = Flex::<()>::column();
445 for _ in 0..rows {
446 body = body.child(Container::<()>::new().height(40.0));
447 }
448 Flex::<()>::column()
449 .child(AnimatedSize::new(0.2, body))
450 .child(Container::<()>::new().height(10.0))
451 }
452
453 fn heights(rt: &Runtime, rows: usize) -> (f32, f32) {
455 let tree = page(rows);
456 let ui = build_ui(&tree, Size::new(300.0, 600.0), rt, &Theme::default());
457 let _ = ui;
458 let (rects, _) = rt.layout_cache.borrow_mut().rects(
459 WidgetId::ROOT,
460 &tree,
461 rt,
462 &Theme::default(),
463 crate::relayout::Constraints::definite(Size::new(300.0, 600.0)),
464 );
465 (rects[1].height, rects[2].y)
466 }
467
468 #[test]
472 fn a_page_follows_a_growing_section() {
473 let mut rt = Runtime::default();
474 assert_eq!(heights(&rt, 1), (40.0, 40.0));
475 rt.advance(0.0);
476 assert_eq!(heights(&rt, 3).0, 40.0, "the frame the change is seen");
477 assert!(rt.advance(0.1));
478 let (half, below) = heights(&rt, 3);
479 assert!((half - 80.0).abs() < 0.5, "half-way: {half}");
480 assert!(
481 (below - half).abs() < 0.5,
482 "the next thing follows: {below}"
483 );
484 while rt.advance(0.05) {
485 heights(&rt, 3);
486 }
487 assert_eq!(heights(&rt, 3), (120.0, 120.0));
488 }
489
490 #[test]
495 fn the_child_is_laid_out_at_its_new_size_at_once() {
496 let page = |width: f32| {
497 Container::<()>::new().width(width).child(
498 AnimatedSize::new(
499 0.2,
500 Container::<()>::new()
501 .height(20.0)
502 .color(frus_core::Color::rgb(0.2, 0.4, 0.8)),
503 )
504 .alignment(Alignment::TOP_LEFT),
505 )
506 };
507 let bar = |ui: &crate::Ui<()>| {
508 ui.scene()
509 .primitives()
510 .iter()
511 .find_map(|p| match p {
512 frus_core::Primitive::Rect { rect, .. } => Some(*rect),
513 _ => None,
514 })
515 .expect("the bar")
516 };
517 let mut rt = Runtime::default();
518 let size = Size::new(400.0, 300.0);
519 assert_eq!(
520 bar(&build_ui(&page(200.0), size, &rt, &Theme::default())).width,
521 200.0
522 );
523 build_ui(&page(300.0), size, &rt, &Theme::default());
524 rt.advance(0.1);
525 let ui = build_ui(&page(300.0), size, &rt, &Theme::default());
526 assert_eq!(bar(&ui).width, 300.0, "laid out at where it is going");
527 }
528
529 #[test]
534 fn a_row_asking_how_narrow_does_not_start_a_move() {
535 let words = "Some words that wrap once they are given a width to wrap in.";
536 let tree = crate::Row::<()>::new().child(AnimatedSize::new(0.2, crate::Text::new(words)));
537 let mut rt = Runtime::default();
538 let size = Size::new(300.0, 600.0);
539 let first = build_ui(&tree, size, &rt, &Theme::default());
540 let _ = first;
541 let (rects, _) = rt.layout_cache.borrow_mut().rects(
542 WidgetId::ROOT,
543 &tree,
544 &rt,
545 &Theme::default(),
546 crate::relayout::Constraints::definite(size),
547 );
548 let settled = rects[1].height;
549 assert!(
550 settled < 100.0,
551 "the paragraph at the width it gets: {settled}"
552 );
553 assert!(!rt.advance(0.05), "nothing to follow");
554 }
555
556 #[test]
559 fn the_child_is_cut_to_the_box_on_the_way() {
560 let coloured = |rows: usize| {
561 let mut body = Flex::<()>::column();
562 for _ in 0..rows {
563 body = body.child(
564 Container::<()>::new()
565 .height(40.0)
566 .color(frus_core::Color::rgb(0.2, 0.4, 0.8)),
567 );
568 }
569 Flex::<()>::column().child(AnimatedSize::new(0.2, body).alignment(Alignment::TOP_LEFT))
570 };
571 let mut rt = Runtime::default();
572 let size = Size::new(300.0, 600.0);
573 build_ui(&coloured(1), size, &rt, &Theme::default());
574 rt.advance(0.0);
575 build_ui(&coloured(3), size, &rt, &Theme::default());
576 rt.advance(0.1);
577 let ui = build_ui(&coloured(3), size, &rt, &Theme::default());
578 let painted: Vec<Rect> = ui
579 .scene()
580 .primitives()
581 .iter()
582 .filter_map(|p| match p {
583 frus_core::Primitive::Rect { rect, clip, .. } => Some(rect.intersect(*clip)),
584 _ => None,
585 })
586 .filter(|r| r.width > 0.0 && r.height > 0.0)
587 .collect();
588 assert_eq!(
589 painted.len(),
590 2,
591 "two rows show, the third is cut away: {painted:?}"
592 );
593 let bottom = painted.iter().map(|r| r.y + r.height).fold(0.0, f32::max);
594 assert!((bottom - 80.0).abs() < 0.5, "cut at the box: {painted:?}");
595 }
596}