1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
5use heapless::Vec;
6
7#[cfg(not(feature = "std"))]
8use crate::math::F32Ext as _;
9use crate::{
10 animation::{Animation, AnimationError, AnimationId, AnimationManager, Easing, PathPoint},
11 context::{GuiContext, GuiError},
12 widget::WidgetId,
13};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum WidgetAnimationError {
17 AnimationsFull,
18 BindingsFull,
19 ConflictIgnored,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum AnimatedProperty {
24 Progress,
25 Meter,
26 SliderValue,
27 ScrollOffsetY,
28 TabSelected,
29 DropdownSelected,
30 RollerSelected,
31 GaugeValue,
32 SpinnerPhase,
33 CornerRadius,
34 AccentR,
35 AccentG,
36 AccentB,
37 WidgetX,
38 WidgetY,
39 WidgetWidth,
40 WidgetHeight,
41 Opacity,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct WidgetKeyframeState {
46 pub x: i32,
47 pub y: i32,
48 pub opacity: u8,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct WidgetPropertyKeyframe {
53 pub x: Option<i32>,
54 pub y: Option<i32>,
55 pub opacity: Option<u8>,
56 pub duration_ms: u32,
57 pub easing: Easing,
58}
59
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
61pub enum AnimationConflictPolicy {
62 #[default]
63 Replace,
64 Ignore,
65 Queue,
66}
67
68#[derive(Clone, Copy, Debug, Default)]
69pub struct WidgetAnimationCallbacks {
70 pub on_start: Option<fn(AnimationId, WidgetId, AnimatedProperty)>,
71 pub on_repeat: Option<fn(AnimationId, WidgetId, AnimatedProperty)>,
72 pub on_complete: Option<fn(AnimationId, WidgetId, AnimatedProperty)>,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76struct Binding {
77 animation_id: AnimationId,
78 widget_id: WidgetId,
79 property: AnimatedProperty,
80 last_iteration: u16,
81 queued: bool,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct BindingSnapshot {
86 pub animation_id: AnimationId,
87 pub widget_id: WidgetId,
88 pub property: AnimatedProperty,
89 pub queued: bool,
90}
91
92#[derive(Clone, Copy, Debug)]
93pub struct WidgetAnimator<const TRACKS: usize, const BINDINGS: usize> {
94 animations: AnimationManager<TRACKS>,
95 bindings: [Option<Binding>; BINDINGS],
96 callbacks: WidgetAnimationCallbacks,
97}
98
99impl<const TRACKS: usize, const BINDINGS: usize> Default for WidgetAnimator<TRACKS, BINDINGS> {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl<const TRACKS: usize, const BINDINGS: usize> WidgetAnimator<TRACKS, BINDINGS> {
106 pub const fn new() -> Self {
107 Self {
108 animations: AnimationManager::new(),
109 bindings: [None; BINDINGS],
110 callbacks: WidgetAnimationCallbacks {
111 on_start: None,
112 on_repeat: None,
113 on_complete: None,
114 },
115 }
116 }
117
118 pub fn set_callbacks(&mut self, callbacks: WidgetAnimationCallbacks) {
119 self.callbacks = callbacks;
120 }
121
122 pub fn animate_progress(
123 &mut self,
124 widget_id: WidgetId,
125 from: f32,
126 to: f32,
127 duration_ms: u32,
128 easing: Easing,
129 ) -> Result<AnimationId, WidgetAnimationError> {
130 self.bind_property(
131 widget_id,
132 AnimatedProperty::Progress,
133 Animation::new(from, to, duration_ms, easing),
134 )
135 }
136
137 pub fn animate_meter(
138 &mut self,
139 widget_id: WidgetId,
140 from: f32,
141 to: f32,
142 duration_ms: u32,
143 easing: Easing,
144 ) -> Result<AnimationId, WidgetAnimationError> {
145 self.bind_property(
146 widget_id,
147 AnimatedProperty::Meter,
148 Animation::new(from, to, duration_ms, easing),
149 )
150 }
151
152 pub fn animate_slider_value(
153 &mut self,
154 widget_id: WidgetId,
155 from: f32,
156 to: f32,
157 duration_ms: u32,
158 easing: Easing,
159 ) -> Result<AnimationId, WidgetAnimationError> {
160 self.animate_slider_value_with_policy(
161 widget_id,
162 from,
163 to,
164 duration_ms,
165 easing,
166 AnimationConflictPolicy::Replace,
167 )
168 }
169
170 pub fn animate_slider_value_with_policy(
171 &mut self,
172 widget_id: WidgetId,
173 from: f32,
174 to: f32,
175 duration_ms: u32,
176 easing: Easing,
177 policy: AnimationConflictPolicy,
178 ) -> Result<AnimationId, WidgetAnimationError> {
179 self.bind_property_with_policy(
180 widget_id,
181 AnimatedProperty::SliderValue,
182 Animation::new(from, to, duration_ms, easing),
183 policy,
184 )
185 }
186
187 pub fn animate_scroll_offset_y(
188 &mut self,
189 widget_id: WidgetId,
190 from: i32,
191 to: i32,
192 duration_ms: u32,
193 easing: Easing,
194 ) -> Result<AnimationId, WidgetAnimationError> {
195 self.animate_scroll_offset_y_with_policy(
196 widget_id,
197 from,
198 to,
199 duration_ms,
200 easing,
201 AnimationConflictPolicy::Replace,
202 )
203 }
204
205 pub fn animate_scroll_offset_y_with_policy(
206 &mut self,
207 widget_id: WidgetId,
208 from: i32,
209 to: i32,
210 duration_ms: u32,
211 easing: Easing,
212 policy: AnimationConflictPolicy,
213 ) -> Result<AnimationId, WidgetAnimationError> {
214 self.bind_property_with_policy(
215 widget_id,
216 AnimatedProperty::ScrollOffsetY,
217 Animation::new(from as f32, to as f32, duration_ms, easing),
218 policy,
219 )
220 }
221
222 pub fn animate_tab_selected(
223 &mut self,
224 widget_id: WidgetId,
225 from: usize,
226 to: usize,
227 duration_ms: u32,
228 easing: Easing,
229 ) -> Result<AnimationId, WidgetAnimationError> {
230 self.bind_property(
231 widget_id,
232 AnimatedProperty::TabSelected,
233 Animation::new(from as f32, to as f32, duration_ms, easing),
234 )
235 }
236
237 pub fn animate_dropdown_selected(
238 &mut self,
239 widget_id: WidgetId,
240 from: usize,
241 to: usize,
242 duration_ms: u32,
243 easing: Easing,
244 ) -> Result<AnimationId, WidgetAnimationError> {
245 self.bind_property(
246 widget_id,
247 AnimatedProperty::DropdownSelected,
248 Animation::new(from as f32, to as f32, duration_ms, easing),
249 )
250 }
251
252 pub fn animate_roller_selected(
253 &mut self,
254 widget_id: WidgetId,
255 from: usize,
256 to: usize,
257 duration_ms: u32,
258 easing: Easing,
259 ) -> Result<AnimationId, WidgetAnimationError> {
260 self.bind_property(
261 widget_id,
262 AnimatedProperty::RollerSelected,
263 Animation::new(from as f32, to as f32, duration_ms, easing),
264 )
265 }
266
267 pub fn animate_gauge_value(
268 &mut self,
269 widget_id: WidgetId,
270 from: f32,
271 to: f32,
272 duration_ms: u32,
273 easing: Easing,
274 ) -> Result<AnimationId, WidgetAnimationError> {
275 self.bind_property(
276 widget_id,
277 AnimatedProperty::GaugeValue,
278 Animation::new(from, to, duration_ms, easing),
279 )
280 }
281
282 pub fn animate_spinner_phase(
283 &mut self,
284 widget_id: WidgetId,
285 from: f32,
286 to: f32,
287 duration_ms: u32,
288 easing: Easing,
289 ) -> Result<AnimationId, WidgetAnimationError> {
290 self.bind_property(
291 widget_id,
292 AnimatedProperty::SpinnerPhase,
293 Animation::new(from, to, duration_ms, easing),
294 )
295 }
296
297 pub fn animate_widget_x(
298 &mut self,
299 widget_id: WidgetId,
300 from: i32,
301 to: i32,
302 duration_ms: u32,
303 easing: Easing,
304 ) -> Result<AnimationId, WidgetAnimationError> {
305 self.animate_widget_x_with_policy(
306 widget_id,
307 from,
308 to,
309 duration_ms,
310 easing,
311 AnimationConflictPolicy::Replace,
312 )
313 }
314
315 pub fn animate_widget_x_with_policy(
316 &mut self,
317 widget_id: WidgetId,
318 from: i32,
319 to: i32,
320 duration_ms: u32,
321 easing: Easing,
322 policy: AnimationConflictPolicy,
323 ) -> Result<AnimationId, WidgetAnimationError> {
324 self.bind_property_with_policy(
325 widget_id,
326 AnimatedProperty::WidgetX,
327 Animation::new(from as f32, to as f32, duration_ms, easing),
328 policy,
329 )
330 }
331
332 #[allow(clippy::too_many_arguments)]
333 pub fn animate_widget_x_with_custom_interpolator(
334 &mut self,
335 widget_id: WidgetId,
336 from: i32,
337 to: i32,
338 duration_ms: u32,
339 easing: Easing,
340 interpolator: fn(f32, f32, f32) -> f32,
341 policy: AnimationConflictPolicy,
342 ) -> Result<AnimationId, WidgetAnimationError> {
343 let animation = Animation::new(from as f32, to as f32, duration_ms, easing)
344 .with_custom_interpolator(interpolator);
345 self.bind_property_with_policy(widget_id, AnimatedProperty::WidgetX, animation, policy)
346 }
347
348 pub fn animate_widget_y(
349 &mut self,
350 widget_id: WidgetId,
351 from: i32,
352 to: i32,
353 duration_ms: u32,
354 easing: Easing,
355 ) -> Result<AnimationId, WidgetAnimationError> {
356 self.animate_widget_y_with_policy(
357 widget_id,
358 from,
359 to,
360 duration_ms,
361 easing,
362 AnimationConflictPolicy::Replace,
363 )
364 }
365
366 pub fn animate_widget_y_with_policy(
367 &mut self,
368 widget_id: WidgetId,
369 from: i32,
370 to: i32,
371 duration_ms: u32,
372 easing: Easing,
373 policy: AnimationConflictPolicy,
374 ) -> Result<AnimationId, WidgetAnimationError> {
375 self.bind_property_with_policy(
376 widget_id,
377 AnimatedProperty::WidgetY,
378 Animation::new(from as f32, to as f32, duration_ms, easing),
379 policy,
380 )
381 }
382
383 #[allow(clippy::too_many_arguments)]
384 pub fn animate_widget_y_with_custom_curve(
385 &mut self,
386 widget_id: WidgetId,
387 from: i32,
388 to: i32,
389 duration_ms: u32,
390 easing: Easing,
391 curve: fn(f32) -> f32,
392 policy: AnimationConflictPolicy,
393 ) -> Result<AnimationId, WidgetAnimationError> {
394 let animation =
395 Animation::new(from as f32, to as f32, duration_ms, easing).with_custom_curve(curve);
396 self.bind_property_with_policy(widget_id, AnimatedProperty::WidgetY, animation, policy)
397 }
398
399 pub fn animate_widget_width(
400 &mut self,
401 widget_id: WidgetId,
402 from: u32,
403 to: u32,
404 duration_ms: u32,
405 easing: Easing,
406 ) -> Result<AnimationId, WidgetAnimationError> {
407 self.animate_widget_width_with_policy(
408 widget_id,
409 from,
410 to,
411 duration_ms,
412 easing,
413 AnimationConflictPolicy::Replace,
414 )
415 }
416
417 pub fn animate_widget_width_with_policy(
418 &mut self,
419 widget_id: WidgetId,
420 from: u32,
421 to: u32,
422 duration_ms: u32,
423 easing: Easing,
424 policy: AnimationConflictPolicy,
425 ) -> Result<AnimationId, WidgetAnimationError> {
426 self.bind_property_with_policy(
427 widget_id,
428 AnimatedProperty::WidgetWidth,
429 Animation::new(from as f32, to as f32, duration_ms, easing),
430 policy,
431 )
432 }
433
434 pub fn animate_widget_height(
435 &mut self,
436 widget_id: WidgetId,
437 from: u32,
438 to: u32,
439 duration_ms: u32,
440 easing: Easing,
441 ) -> Result<AnimationId, WidgetAnimationError> {
442 self.animate_widget_height_with_policy(
443 widget_id,
444 from,
445 to,
446 duration_ms,
447 easing,
448 AnimationConflictPolicy::Replace,
449 )
450 }
451
452 pub fn animate_widget_height_with_policy(
453 &mut self,
454 widget_id: WidgetId,
455 from: u32,
456 to: u32,
457 duration_ms: u32,
458 easing: Easing,
459 policy: AnimationConflictPolicy,
460 ) -> Result<AnimationId, WidgetAnimationError> {
461 self.bind_property_with_policy(
462 widget_id,
463 AnimatedProperty::WidgetHeight,
464 Animation::new(from as f32, to as f32, duration_ms, easing),
465 policy,
466 )
467 }
468
469 pub fn animate_opacity(
470 &mut self,
471 widget_id: WidgetId,
472 from: u8,
473 to: u8,
474 duration_ms: u32,
475 easing: Easing,
476 ) -> Result<AnimationId, WidgetAnimationError> {
477 self.animate_opacity_with_policy(
478 widget_id,
479 from,
480 to,
481 duration_ms,
482 easing,
483 AnimationConflictPolicy::Replace,
484 )
485 }
486
487 pub fn animate_opacity_with_policy(
488 &mut self,
489 widget_id: WidgetId,
490 from: u8,
491 to: u8,
492 duration_ms: u32,
493 easing: Easing,
494 policy: AnimationConflictPolicy,
495 ) -> Result<AnimationId, WidgetAnimationError> {
496 self.bind_property_with_policy(
497 widget_id,
498 AnimatedProperty::Opacity,
499 Animation::new(from as f32, to as f32, duration_ms, easing),
500 policy,
501 )
502 }
503
504 #[allow(clippy::too_many_arguments)]
505 pub fn animate_opacity_with_custom_interpolator(
506 &mut self,
507 widget_id: WidgetId,
508 from: u8,
509 to: u8,
510 duration_ms: u32,
511 easing: Easing,
512 interpolator: fn(f32, f32, f32) -> f32,
513 policy: AnimationConflictPolicy,
514 ) -> Result<AnimationId, WidgetAnimationError> {
515 let animation = Animation::new(from as f32, to as f32, duration_ms, easing)
516 .with_custom_interpolator(interpolator);
517 self.bind_property_with_policy(widget_id, AnimatedProperty::Opacity, animation, policy)
518 }
519
520 pub fn animate_widget_keyframes(
521 &mut self,
522 widget_id: WidgetId,
523 initial: WidgetKeyframeState,
524 keyframes: &[WidgetPropertyKeyframe],
525 policy: AnimationConflictPolicy,
526 ) -> Result<usize, WidgetAnimationError> {
527 let mut created = 0usize;
528 let mut delay_ms = 0u32;
529 let mut state = initial;
530 for (idx, keyframe) in keyframes.iter().copied().enumerate() {
531 let step_policy = if idx == 0 {
532 policy
533 } else {
534 AnimationConflictPolicy::Queue
535 };
536 if let Some(next_x) = keyframe.x {
537 let anim = Animation::new(
538 state.x as f32,
539 next_x as f32,
540 keyframe.duration_ms,
541 keyframe.easing,
542 )
543 .with_delay(delay_ms);
544 self.bind_property_with_policy(
545 widget_id,
546 AnimatedProperty::WidgetX,
547 anim,
548 step_policy,
549 )?;
550 created += 1;
551 state.x = next_x;
552 }
553 if let Some(next_y) = keyframe.y {
554 let anim = Animation::new(
555 state.y as f32,
556 next_y as f32,
557 keyframe.duration_ms,
558 keyframe.easing,
559 )
560 .with_delay(delay_ms);
561 self.bind_property_with_policy(
562 widget_id,
563 AnimatedProperty::WidgetY,
564 anim,
565 step_policy,
566 )?;
567 created += 1;
568 state.y = next_y;
569 }
570 if let Some(next_opacity) = keyframe.opacity {
571 let anim = Animation::new(
572 state.opacity as f32,
573 next_opacity as f32,
574 keyframe.duration_ms,
575 keyframe.easing,
576 )
577 .with_delay(delay_ms);
578 self.bind_property_with_policy(
579 widget_id,
580 AnimatedProperty::Opacity,
581 anim,
582 step_policy,
583 )?;
584 created += 1;
585 state.opacity = next_opacity;
586 }
587 delay_ms = delay_ms.saturating_add(keyframe.duration_ms);
588 }
589 Ok(created)
590 }
591
592 pub fn pulse_opacity(
593 &mut self,
594 widget_id: WidgetId,
595 low: u8,
596 high: u8,
597 duration_ms: u32,
598 easing: Easing,
599 ) -> Result<AnimationId, WidgetAnimationError> {
600 let animation = Animation::new(low as f32, high as f32, duration_ms, easing)
601 .with_repeat_mode(crate::animation::RepeatMode::PingPong)
602 .with_repeat_count(None);
603 self.bind_property(widget_id, AnimatedProperty::Opacity, animation)
604 }
605
606 pub fn ping_pong_progress(
607 &mut self,
608 widget_id: WidgetId,
609 from: f32,
610 to: f32,
611 duration_ms: u32,
612 easing: Easing,
613 ) -> Result<AnimationId, WidgetAnimationError> {
614 let animation = Animation::new(from, to, duration_ms, easing)
615 .with_repeat_mode(crate::animation::RepeatMode::PingPong)
616 .with_repeat_count(None);
617 self.bind_property(widget_id, AnimatedProperty::Progress, animation)
618 }
619
620 pub fn animate_corner_radius(
621 &mut self,
622 widget_id: WidgetId,
623 from: u8,
624 to: u8,
625 duration_ms: u32,
626 easing: Easing,
627 ) -> Result<AnimationId, WidgetAnimationError> {
628 self.bind_property(
629 widget_id,
630 AnimatedProperty::CornerRadius,
631 Animation::new(from as f32, to as f32, duration_ms, easing),
632 )
633 }
634
635 pub fn animate_accent_color(
636 &mut self,
637 widget_id: WidgetId,
638 from: Rgb565,
639 to: Rgb565,
640 duration_ms: u32,
641 easing: Easing,
642 ) -> Result<[AnimationId; 3], WidgetAnimationError> {
643 let mut ids = [AnimationId::new(0); 3];
644 let mut started = Vec::<AnimationId, 3>::new();
645 let r = self.bind_property(
646 widget_id,
647 AnimatedProperty::AccentR,
648 Animation::new(from.r() as f32, to.r() as f32, duration_ms, easing),
649 )?;
650 ids[0] = r;
651 let _ = started.push(r);
652 let g = match self.bind_property(
653 widget_id,
654 AnimatedProperty::AccentG,
655 Animation::new(from.g() as f32, to.g() as f32, duration_ms, easing),
656 ) {
657 Ok(v) => v,
658 Err(err) => {
659 for id in started {
660 let _ = self.stop(id);
661 }
662 return Err(err);
663 }
664 };
665 ids[1] = g;
666 let _ = started.push(g);
667 let b = match self.bind_property(
668 widget_id,
669 AnimatedProperty::AccentB,
670 Animation::new(from.b() as f32, to.b() as f32, duration_ms, easing),
671 ) {
672 Ok(v) => v,
673 Err(err) => {
674 for id in started {
675 let _ = self.stop(id);
676 }
677 return Err(err);
678 }
679 };
680 ids[2] = b;
681 Ok(ids)
682 }
683
684 pub fn animate_widget_path(
685 &mut self,
686 widget_id: WidgetId,
687 points: &[PathPoint],
688 duration_ms: u32,
689 easing: Easing,
690 ) -> Result<(AnimationId, AnimationId), WidgetAnimationError> {
691 self.animate_widget_path_with_policy(
692 widget_id,
693 points,
694 duration_ms,
695 easing,
696 AnimationConflictPolicy::Replace,
697 )
698 }
699
700 pub fn animate_widget_path_with_policy(
701 &mut self,
702 widget_id: WidgetId,
703 points: &[PathPoint],
704 duration_ms: u32,
705 easing: Easing,
706 policy: AnimationConflictPolicy,
707 ) -> Result<(AnimationId, AnimationId), WidgetAnimationError> {
708 if points.len() < 2 {
709 return Err(WidgetAnimationError::ConflictIgnored);
710 }
711 let segs = (points.len() - 1) as u32;
712 let seg_duration = (duration_ms / segs).max(1);
713 let mut ids = Vec::<AnimationId, BINDINGS>::new();
714 let mut first_x = AnimationId::new(0);
715 let mut first_y = AnimationId::new(0);
716
717 for i in 0..(points.len() - 1) {
718 let from = points[i];
719 let to = points[i + 1];
720 let delay = seg_duration.saturating_mul(i as u32);
721 let x_anim = Animation::new(from.x, to.x, seg_duration, easing).with_delay(delay);
722 let y_anim = Animation::new(from.y, to.y, seg_duration, easing).with_delay(delay);
723 let step_policy = if i == 0 {
724 policy
725 } else {
726 AnimationConflictPolicy::Queue
727 };
728 let x_id = match self.bind_property_with_policy(
729 widget_id,
730 AnimatedProperty::WidgetX,
731 x_anim,
732 step_policy,
733 ) {
734 Ok(id) => id,
735 Err(err) => {
736 for id in ids {
737 let _ = self.stop(id);
738 }
739 return Err(err);
740 }
741 };
742 let _ = ids.push(x_id);
743 let y_id = match self.bind_property_with_policy(
744 widget_id,
745 AnimatedProperty::WidgetY,
746 y_anim,
747 step_policy,
748 ) {
749 Ok(id) => id,
750 Err(err) => {
751 for id in ids {
752 let _ = self.stop(id);
753 }
754 return Err(err);
755 }
756 };
757 let _ = ids.push(y_id);
758 if i == 0 {
759 first_x = x_id;
760 first_y = y_id;
761 }
762 }
763 Ok((first_x, first_y))
764 }
765
766 pub fn stagger_widget_x(
767 &mut self,
768 widget_ids: &[WidgetId],
769 from: i32,
770 to: i32,
771 duration_ms: u32,
772 stagger_ms: u32,
773 easing: Easing,
774 ) -> Result<usize, WidgetAnimationError> {
775 let mut created = 0usize;
776 let mut started = Vec::<AnimationId, BINDINGS>::new();
777 for (idx, id) in widget_ids.iter().copied().enumerate() {
778 let delay = stagger_ms.saturating_mul(idx as u32);
779 let animation =
780 Animation::new(from as f32, to as f32, duration_ms, easing).with_delay(delay);
781 match self.bind_property_with_policy(
782 id,
783 AnimatedProperty::WidgetX,
784 animation,
785 AnimationConflictPolicy::Replace,
786 ) {
787 Ok(track) => {
788 let _ = started.push(track);
789 created += 1;
790 }
791 Err(err) => {
792 for track in started {
793 let _ = self.stop(track);
794 }
795 return Err(err);
796 }
797 }
798 }
799 Ok(created)
800 }
801
802 pub fn preset_fade_in_up(
803 &mut self,
804 widget_id: WidgetId,
805 from_y: i32,
806 to_y: i32,
807 duration_ms: u32,
808 ) -> Result<(AnimationId, AnimationId), WidgetAnimationError> {
809 let y = self.animate_widget_y(widget_id, from_y, to_y, duration_ms, Easing::OutCubic)?;
810 let alpha = self.animate_opacity(widget_id, 0, 255, duration_ms, Easing::OutSine)?;
811 Ok((y, alpha))
812 }
813
814 pub fn preset_attention_shake(
815 &mut self,
816 widget_id: WidgetId,
817 base_x: i32,
818 amplitude: i32,
819 duration_ms: u32,
820 ) -> Result<(AnimationId, AnimationId), WidgetAnimationError> {
821 let step = (duration_ms / 3).max(1);
822 let a = self.bind_property_with_policy(
823 widget_id,
824 AnimatedProperty::WidgetX,
825 Animation::new(
826 base_x as f32,
827 (base_x + amplitude) as f32,
828 step,
829 Easing::InOutSine,
830 ),
831 AnimationConflictPolicy::Replace,
832 )?;
833 let b = self.bind_property_with_policy(
834 widget_id,
835 AnimatedProperty::WidgetX,
836 Animation::new(
837 (base_x + amplitude) as f32,
838 (base_x - amplitude) as f32,
839 step,
840 Easing::InOutSine,
841 )
842 .with_delay(step),
843 AnimationConflictPolicy::Queue,
844 )?;
845 self.bind_property_with_policy(
846 widget_id,
847 AnimatedProperty::WidgetX,
848 Animation::new(
849 (base_x - amplitude) as f32,
850 base_x as f32,
851 step,
852 Easing::InOutSine,
853 )
854 .with_delay(step.saturating_mul(2)),
855 AnimationConflictPolicy::Queue,
856 )?;
857 Ok((a, b))
858 }
859
860 pub fn preset_selection_bump_settle(
861 &mut self,
862 widget_id: WidgetId,
863 base_y: i32,
864 bump_px: i32,
865 duration_ms: u32,
866 ) -> Result<(AnimationId, AnimationId), WidgetAnimationError> {
867 let up_ms = (duration_ms / 3).max(1);
868 let settle_ms = duration_ms.saturating_sub(up_ms).max(1);
869 let bump_y = base_y - bump_px.abs();
870 let up = self.bind_property_with_policy(
871 widget_id,
872 AnimatedProperty::WidgetY,
873 Animation::new(base_y as f32, bump_y as f32, up_ms, Easing::OutCubic),
874 AnimationConflictPolicy::Replace,
875 )?;
876 let settle = self.bind_property_with_policy(
877 widget_id,
878 AnimatedProperty::WidgetY,
879 Animation::new(bump_y as f32, base_y as f32, settle_ms, Easing::OutBounce)
880 .with_delay(up_ms),
881 AnimationConflictPolicy::Queue,
882 )?;
883 Ok((up, settle))
884 }
885
886 pub fn bind_property_with_policy(
887 &mut self,
888 widget_id: WidgetId,
889 property: AnimatedProperty,
890 animation: Animation,
891 policy: AnimationConflictPolicy,
892 ) -> Result<AnimationId, WidgetAnimationError> {
893 match policy {
894 AnimationConflictPolicy::Ignore
895 if self
896 .bindings
897 .iter()
898 .flatten()
899 .any(|b| b.widget_id == widget_id && b.property == property) =>
900 {
901 return Err(WidgetAnimationError::ConflictIgnored);
902 }
903 AnimationConflictPolicy::Replace => {
904 let ids_to_stop: Vec<AnimationId, BINDINGS> = self
905 .bindings
906 .iter()
907 .flatten()
908 .filter(|b| b.widget_id == widget_id && b.property == property)
909 .map(|b| b.animation_id)
910 .collect();
911 for id in ids_to_stop {
912 let _ = self.stop(id);
913 }
914 }
915 AnimationConflictPolicy::Queue | AnimationConflictPolicy::Ignore => {}
916 }
917
918 let animation_id = self
919 .animations
920 .start(animation)
921 .map_err(|_| WidgetAnimationError::AnimationsFull)?;
922
923 let has_existing = self
924 .bindings
925 .iter()
926 .flatten()
927 .any(|b| b.widget_id == widget_id && b.property == property);
928
929 if let Some(slot) = self.bindings.iter_mut().find(|slot| slot.is_none()) {
930 *slot = Some(Binding {
931 animation_id,
932 widget_id,
933 property,
934 last_iteration: 0,
935 queued: has_existing && policy == AnimationConflictPolicy::Queue,
936 });
937 if let Some(cb) = self.callbacks.on_start {
938 cb(animation_id, widget_id, property);
939 }
940 Ok(animation_id)
941 } else {
942 let _ = self.animations.stop(animation_id);
943 Err(WidgetAnimationError::BindingsFull)
944 }
945 }
946
947 pub fn bind_property(
948 &mut self,
949 widget_id: WidgetId,
950 property: AnimatedProperty,
951 animation: Animation,
952 ) -> Result<AnimationId, WidgetAnimationError> {
953 self.bind_property_with_policy(
954 widget_id,
955 property,
956 animation,
957 AnimationConflictPolicy::Replace,
958 )
959 }
960
961 pub fn stop(&mut self, animation_id: AnimationId) -> bool {
962 let stopped = self.animations.stop(animation_id);
963 for slot in &mut self.bindings {
964 if slot
965 .as_ref()
966 .is_some_and(|binding| binding.animation_id == animation_id)
967 {
968 *slot = None;
969 }
970 }
971 stopped
972 }
973
974 pub fn stop_widget(&mut self, widget_id: WidgetId) -> usize {
975 let ids: Vec<AnimationId, BINDINGS> = self
976 .bindings
977 .iter()
978 .flatten()
979 .filter(|b| b.widget_id == widget_id)
980 .map(|b| b.animation_id)
981 .collect();
982 let count = ids.len();
983 for id in ids {
984 let _ = self.stop(id);
985 }
986 count
987 }
988
989 pub fn stop_widget_property(
990 &mut self,
991 widget_id: WidgetId,
992 property: AnimatedProperty,
993 ) -> usize {
994 let ids: Vec<AnimationId, BINDINGS> = self
995 .bindings
996 .iter()
997 .flatten()
998 .filter(|b| b.widget_id == widget_id && b.property == property)
999 .map(|b| b.animation_id)
1000 .collect();
1001 let count = ids.len();
1002 for id in ids {
1003 let _ = self.stop(id);
1004 }
1005 count
1006 }
1007
1008 pub fn is_animating_widget(&self, widget_id: WidgetId) -> bool {
1009 self.bindings
1010 .iter()
1011 .flatten()
1012 .any(|b| b.widget_id == widget_id)
1013 }
1014
1015 pub fn is_animating_widget_property(
1016 &self,
1017 widget_id: WidgetId,
1018 property: AnimatedProperty,
1019 ) -> bool {
1020 self.bindings
1021 .iter()
1022 .flatten()
1023 .any(|b| b.widget_id == widget_id && b.property == property)
1024 }
1025
1026 pub fn handles_for_widget<const M: usize>(
1027 &self,
1028 widget_id: WidgetId,
1029 out: &mut Vec<AnimationId, M>,
1030 ) -> usize {
1031 out.clear();
1032 for binding in self
1033 .bindings
1034 .iter()
1035 .flatten()
1036 .filter(|b| b.widget_id == widget_id)
1037 {
1038 let _ = out.push(binding.animation_id);
1039 }
1040 out.len()
1041 }
1042
1043 pub fn active_bindings<const M: usize>(&self, out: &mut Vec<BindingSnapshot, M>) -> usize {
1044 out.clear();
1045 for binding in self.bindings.iter().flatten() {
1046 let _ = out.push(BindingSnapshot {
1047 animation_id: binding.animation_id,
1048 widget_id: binding.widget_id,
1049 property: binding.property,
1050 queued: binding.queued,
1051 });
1052 }
1053 out.len()
1054 }
1055
1056 pub fn tick<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>(
1057 &mut self,
1058 dt_ms: u32,
1059 gui: &mut GuiContext<'a, NODES, EVENTS, DIRTY>,
1060 ) -> Result<(), GuiError> {
1061 for idx in 0..self.bindings.len() {
1062 let Some(binding) = self.bindings[idx] else {
1063 continue;
1064 };
1065 if binding.queued
1066 && self.bindings.iter().enumerate().any(|(other_idx, other)| {
1067 other_idx != idx
1068 && other.as_ref().is_some_and(|other| {
1069 other.widget_id == binding.widget_id
1070 && other.property == binding.property
1071 && other.animation_id != binding.animation_id
1072 })
1073 })
1074 {
1075 continue;
1076 }
1077
1078 let Some((value, iteration, done)) = ({
1079 if let Some(anim) = self.animations.animation_mut(binding.animation_id) {
1080 anim.tick(dt_ms);
1081 Some((anim.value(), anim.iteration(), anim.is_done()))
1082 } else {
1083 None
1084 }
1085 }) else {
1086 if let Some(cb) = self.callbacks.on_complete {
1087 cb(binding.animation_id, binding.widget_id, binding.property);
1088 }
1089 self.bindings[idx] = None;
1090 continue;
1091 };
1092
1093 if iteration > binding.last_iteration {
1094 if let Some(cb) = self.callbacks.on_repeat {
1095 cb(binding.animation_id, binding.widget_id, binding.property);
1096 }
1097 if let Some(slot_binding) = self.bindings[idx].as_mut() {
1098 slot_binding.last_iteration = iteration;
1099 }
1100 }
1101
1102 match binding.property {
1103 AnimatedProperty::Progress => gui.set_progress(binding.widget_id, value)?,
1104 AnimatedProperty::Meter => gui.set_meter_value(binding.widget_id, value)?,
1105 AnimatedProperty::SliderValue => gui.set_slider_value(binding.widget_id, value)?,
1106 AnimatedProperty::ScrollOffsetY => {
1107 gui.set_scroll_offset(binding.widget_id, value.round() as i32)?
1108 }
1109 AnimatedProperty::TabSelected => {
1110 gui.set_tab_selected(binding.widget_id, value.max(0.0).round() as usize)?
1111 }
1112 AnimatedProperty::DropdownSelected => {
1113 gui.set_dropdown_selected(binding.widget_id, value.max(0.0).round() as usize)?
1114 }
1115 AnimatedProperty::RollerSelected => {
1116 gui.set_roller_selected(binding.widget_id, value.max(0.0).round() as usize)?
1117 }
1118 AnimatedProperty::GaugeValue => gui.set_gauge_value(binding.widget_id, value)?,
1119 AnimatedProperty::SpinnerPhase => {
1120 gui.set_spinner_phase(binding.widget_id, value)?
1121 }
1122 AnimatedProperty::CornerRadius => gui.set_widget_corner_radius(
1123 binding.widget_id,
1124 value.clamp(0.0, 255.0).round() as u8,
1125 )?,
1126 AnimatedProperty::AccentR
1127 | AnimatedProperty::AccentG
1128 | AnimatedProperty::AccentB => {
1129 let node = gui
1130 .widgets()
1131 .iter()
1132 .find(|node| node.id == binding.widget_id)
1133 .ok_or(GuiError::NotFound)?;
1134 let mut accent = node.style.normal.accent;
1135 match binding.property {
1136 AnimatedProperty::AccentR => {
1137 accent = Rgb565::new(
1138 value.clamp(0.0, 31.0).round() as u8,
1139 accent.g(),
1140 accent.b(),
1141 );
1142 }
1143 AnimatedProperty::AccentG => {
1144 accent = Rgb565::new(
1145 accent.r(),
1146 value.clamp(0.0, 63.0).round() as u8,
1147 accent.b(),
1148 );
1149 }
1150 AnimatedProperty::AccentB => {
1151 accent = Rgb565::new(
1152 accent.r(),
1153 accent.g(),
1154 value.clamp(0.0, 31.0).round() as u8,
1155 );
1156 }
1157 _ => {}
1158 }
1159 gui.set_widget_accent(binding.widget_id, accent)?;
1160 }
1161 AnimatedProperty::WidgetX => {
1162 gui.set_widget_x(binding.widget_id, value.round() as i32)?
1163 }
1164 AnimatedProperty::WidgetY => {
1165 gui.set_widget_y(binding.widget_id, value.round() as i32)?
1166 }
1167 AnimatedProperty::WidgetWidth => {
1168 gui.set_widget_width(binding.widget_id, value.max(1.0).round() as u32)?
1169 }
1170 AnimatedProperty::WidgetHeight => {
1171 gui.set_widget_height(binding.widget_id, value.max(1.0).round() as u32)?
1172 }
1173 AnimatedProperty::Opacity => {
1174 gui.set_widget_opacity(binding.widget_id, value.clamp(0.0, 255.0) as u8)?
1175 }
1176 }
1177 if done {
1178 let _ = self.animations.stop(binding.animation_id);
1179 if let Some(cb) = self.callbacks.on_complete {
1180 cb(binding.animation_id, binding.widget_id, binding.property);
1181 }
1182 self.bindings[idx] = None;
1183 }
1184 }
1185 Ok(())
1186 }
1187
1188 pub fn active_count(&self) -> usize {
1189 self.bindings.iter().flatten().count()
1190 }
1191}
1192
1193impl From<AnimationError> for WidgetAnimationError {
1194 fn from(_: AnimationError) -> Self {
1195 Self::AnimationsFull
1196 }
1197}
1198
1199pub mod presets {
1200 use embedded_graphics_core::pixelcolor::Rgb565;
1201
1202 use super::{
1203 AnimationConflictPolicy, Easing, PathPoint, WidgetAnimationError, WidgetAnimator, WidgetId,
1204 };
1205 use crate::cinematic::{
1206 GlanceTileSpec, PeekRevealSpec, animate_glance_focus, animate_peek_reveal,
1207 };
1208
1209 pub fn entrance_fade_in_up<const TRACKS: usize, const BINDINGS: usize>(
1210 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1211 widget_id: WidgetId,
1212 from_y: i32,
1213 to_y: i32,
1214 duration_ms: u32,
1215 ) -> Result<(), WidgetAnimationError> {
1216 animator.preset_fade_in_up(widget_id, from_y, to_y, duration_ms)?;
1217 Ok(())
1218 }
1219
1220 pub fn attention_shake<const TRACKS: usize, const BINDINGS: usize>(
1221 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1222 widget_id: WidgetId,
1223 base_x: i32,
1224 amplitude: i32,
1225 duration_ms: u32,
1226 ) -> Result<(), WidgetAnimationError> {
1227 animator.preset_attention_shake(widget_id, base_x, amplitude, duration_ms)?;
1228 Ok(())
1229 }
1230
1231 pub fn selection_bump_settle<const TRACKS: usize, const BINDINGS: usize>(
1232 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1233 widget_id: WidgetId,
1234 base_y: i32,
1235 bump_px: i32,
1236 duration_ms: u32,
1237 ) -> Result<(), WidgetAnimationError> {
1238 animator.preset_selection_bump_settle(widget_id, base_y, bump_px, duration_ms)?;
1239 Ok(())
1240 }
1241
1242 pub fn style_breathe<const TRACKS: usize, const BINDINGS: usize>(
1243 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1244 widget_id: WidgetId,
1245 low_opacity: u8,
1246 high_opacity: u8,
1247 low_radius: u8,
1248 high_radius: u8,
1249 duration_ms: u32,
1250 ) -> Result<(), WidgetAnimationError> {
1251 animator.pulse_opacity(
1252 widget_id,
1253 low_opacity,
1254 high_opacity,
1255 duration_ms,
1256 Easing::InOutSine,
1257 )?;
1258 animator.animate_corner_radius(
1259 widget_id,
1260 low_radius,
1261 high_radius,
1262 duration_ms,
1263 Easing::InOutSine,
1264 )?;
1265 Ok(())
1266 }
1267
1268 pub fn style_accent_cycle<const TRACKS: usize, const BINDINGS: usize>(
1269 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1270 widget_id: WidgetId,
1271 from: Rgb565,
1272 to: Rgb565,
1273 duration_ms: u32,
1274 ) -> Result<(), WidgetAnimationError> {
1275 animator.animate_accent_color(widget_id, from, to, duration_ms, Easing::InOutSine)?;
1276 Ok(())
1277 }
1278
1279 pub fn path_float_loop<const TRACKS: usize, const BINDINGS: usize>(
1280 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1281 widget_id: WidgetId,
1282 center_x: i32,
1283 center_y: i32,
1284 radius: i32,
1285 duration_ms: u32,
1286 ) -> Result<(), WidgetAnimationError> {
1287 let points = [
1288 PathPoint::new(center_x as f32, (center_y - radius) as f32),
1289 PathPoint::new((center_x + radius) as f32, center_y as f32),
1290 PathPoint::new(center_x as f32, (center_y + radius) as f32),
1291 PathPoint::new((center_x - radius) as f32, center_y as f32),
1292 PathPoint::new(center_x as f32, (center_y - radius) as f32),
1293 ];
1294 animator.animate_widget_path(widget_id, &points, duration_ms, Easing::InOutSine)?;
1295 Ok(())
1296 }
1297
1298 pub fn orchestrate_stagger_x<const TRACKS: usize, const BINDINGS: usize>(
1299 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1300 widget_ids: &[WidgetId],
1301 from: i32,
1302 to: i32,
1303 duration_ms: u32,
1304 stagger_ms: u32,
1305 ) -> Result<usize, WidgetAnimationError> {
1306 animator.stagger_widget_x(
1307 widget_ids,
1308 from,
1309 to,
1310 duration_ms,
1311 stagger_ms,
1312 Easing::OutSine,
1313 )
1314 }
1315
1316 pub fn menu_focus_choreography<const TRACKS: usize, const BINDINGS: usize>(
1317 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1318 focused: WidgetId,
1319 base_x: i32,
1320 base_y: i32,
1321 ) -> Result<(), WidgetAnimationError> {
1322 animator.preset_selection_bump_settle(focused, base_y, 3, 120)?;
1323 animator.animate_widget_x_with_custom_interpolator(
1324 focused,
1325 base_x,
1326 base_x + 6,
1327 120,
1328 Easing::InOutSine,
1329 |from, to, t| {
1330 if t < 0.5 {
1331 from + (to - from) * (t * 1.6)
1332 } else {
1333 to - (to - from) * ((t - 0.5) * 1.2)
1334 }
1335 },
1336 AnimationConflictPolicy::Replace,
1337 )?;
1338 animator.animate_opacity(focused, 180, 255, 120, Easing::OutSine)?;
1339 Ok(())
1340 }
1341
1342 pub fn dialog_pop_choreography<const TRACKS: usize, const BINDINGS: usize>(
1343 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1344 dialog: WidgetId,
1345 base_y: i32,
1346 ) -> Result<(), WidgetAnimationError> {
1347 animator.preset_fade_in_up(dialog, base_y + 8, base_y, 180)?;
1348 animator.animate_corner_radius(dialog, 1, 4, 180, Easing::OutBack)?;
1349 animator.animate_opacity(dialog, 120, 255, 180, Easing::OutSine)?;
1350 Ok(())
1351 }
1352
1353 pub fn list_focus_with_neighbors<const TRACKS: usize, const BINDINGS: usize>(
1354 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1355 focused: WidgetId,
1356 neighbors: &[WidgetId],
1357 base_x: i32,
1358 base_y: i32,
1359 ) -> Result<(), WidgetAnimationError> {
1360 menu_focus_choreography(animator, focused, base_x, base_y)?;
1361 for neighbor in neighbors.iter().copied() {
1362 animator.animate_widget_x(
1363 neighbor,
1364 base_x,
1365 base_x.saturating_sub(2),
1366 120,
1367 Easing::OutSine,
1368 )?;
1369 animator.animate_opacity(neighbor, 255, 170, 120, Easing::OutSine)?;
1370 }
1371 Ok(())
1372 }
1373
1374 pub fn peek_reveal<const TRACKS: usize, const BINDINGS: usize>(
1375 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1376 icon_widget: WidgetId,
1377 title_widget: Option<WidgetId>,
1378 subtitle_widget: Option<WidgetId>,
1379 base_x: i32,
1380 base_y: i32,
1381 ) -> Result<(), WidgetAnimationError> {
1382 animate_peek_reveal(
1383 animator,
1384 icon_widget,
1385 title_widget,
1386 subtitle_widget,
1387 base_x,
1388 base_y,
1389 PeekRevealSpec::default(),
1390 )
1391 }
1392
1393 pub fn glance_focus<const TRACKS: usize, const BINDINGS: usize>(
1394 animator: &mut WidgetAnimator<TRACKS, BINDINGS>,
1395 focused: WidgetId,
1396 neighbors: &[WidgetId],
1397 base_x: i32,
1398 base_y: i32,
1399 ) -> Result<(), WidgetAnimationError> {
1400 animate_glance_focus(
1401 animator,
1402 focused,
1403 neighbors,
1404 base_x,
1405 base_y,
1406 GlanceTileSpec::default(),
1407 )
1408 }
1409}