Skip to main content

embedded_gui/
screen_transition.rs

1use heapless::Vec;
2
3#[cfg(not(feature = "std"))]
4use crate::math::F32Ext as _;
5use crate::{
6    animation::{Animation, Easing},
7    animation_timing::{self, timing_half_phase, timing_shutter_phase},
8    context::GuiContext,
9    geometry::Rect,
10    screen::{ScreenCommand, ScreenId, ScreenLifecycleEvent, ScreenStack, ScreenStackError},
11};
12
13/// Screen transition visual effect.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum ScreenTransitionEffect {
16    #[default]
17    None,
18    Fade,
19    SlideLeft,
20    SlideRight,
21    SlideUp,
22    SlideDown,
23    /// Rectangular push: incoming from the right with moook easing.
24    PushMoook,
25    /// Rectangular pop: incoming from the left with moook easing.
26    PopMoook,
27    Zoom,
28    CircularReveal,
29    WipeLeft,
30    WipeRight,
31    WipeUp,
32    WipeDown,
33    /// Two-phase directional shutter wipe.
34    ShutterLeft,
35    ShutterRight,
36    ShutterUp,
37    ShutterDown,
38    /// Round-display card flip (vertical clip).
39    RoundFlipLeft,
40    RoundFlipRight,
41    /// Two-phase slide with a mid-transition seam.
42    PortHoleLeft,
43    PortHoleRight,
44    PortHoleUp,
45    PortHoleDown,
46    /// Modal overlay slide from top or bottom.
47    ModalSlideUp,
48    ModalSlideDown,
49}
50
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub enum ScreenTransitionOrigin {
53    #[default]
54    Center,
55    TopLeft,
56    Top,
57    TopRight,
58    Left,
59    Right,
60    BottomLeft,
61    Bottom,
62    BottomRight,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub struct ScreenTransitionSpec {
67    pub effect: ScreenTransitionEffect,
68    pub duration_ms: u32,
69    pub origin: ScreenTransitionOrigin,
70    pub easing: Easing,
71}
72
73impl ScreenTransitionSpec {
74    pub const fn none() -> Self {
75        Self {
76            effect: ScreenTransitionEffect::None,
77            duration_ms: 0,
78            origin: ScreenTransitionOrigin::Center,
79            easing: Easing::InOutSine,
80        }
81    }
82
83    pub const fn fade(duration_ms: u32) -> Self {
84        Self {
85            effect: ScreenTransitionEffect::Fade,
86            duration_ms,
87            origin: ScreenTransitionOrigin::Center,
88            easing: Easing::InOutSine,
89        }
90    }
91
92    pub const fn slide_left(duration_ms: u32) -> Self {
93        Self {
94            effect: ScreenTransitionEffect::SlideLeft,
95            duration_ms,
96            origin: ScreenTransitionOrigin::Center,
97            easing: Easing::InOutSine,
98        }
99    }
100
101    pub const fn slide_right(duration_ms: u32) -> Self {
102        Self {
103            effect: ScreenTransitionEffect::SlideRight,
104            duration_ms,
105            origin: ScreenTransitionOrigin::Center,
106            easing: Easing::InOutSine,
107        }
108    }
109
110    pub const fn slide_up(duration_ms: u32) -> Self {
111        Self {
112            effect: ScreenTransitionEffect::SlideUp,
113            duration_ms,
114            origin: ScreenTransitionOrigin::Center,
115            easing: Easing::InOutSine,
116        }
117    }
118
119    pub const fn slide_down(duration_ms: u32) -> Self {
120        Self {
121            effect: ScreenTransitionEffect::SlideDown,
122            duration_ms,
123            origin: ScreenTransitionOrigin::Center,
124            easing: Easing::InOutSine,
125        }
126    }
127
128    pub const fn push_moook(duration_ms: u32) -> Self {
129        Self {
130            effect: ScreenTransitionEffect::PushMoook,
131            duration_ms,
132            origin: ScreenTransitionOrigin::Center,
133            easing: Easing::Moook,
134        }
135    }
136
137    pub const fn pop_moook(duration_ms: u32) -> Self {
138        Self {
139            effect: ScreenTransitionEffect::PopMoook,
140            duration_ms,
141            origin: ScreenTransitionOrigin::Center,
142            easing: Easing::Moook,
143        }
144    }
145
146    pub const fn shutter_left(duration_ms: u32) -> Self {
147        Self {
148            effect: ScreenTransitionEffect::ShutterLeft,
149            duration_ms,
150            origin: ScreenTransitionOrigin::Center,
151            easing: Easing::EaseInOut,
152        }
153    }
154
155    pub const fn shutter_right(duration_ms: u32) -> Self {
156        Self {
157            effect: ScreenTransitionEffect::ShutterRight,
158            duration_ms,
159            origin: ScreenTransitionOrigin::Center,
160            easing: Easing::EaseInOut,
161        }
162    }
163
164    pub const fn shutter_up(duration_ms: u32) -> Self {
165        Self {
166            effect: ScreenTransitionEffect::ShutterUp,
167            duration_ms,
168            origin: ScreenTransitionOrigin::Center,
169            easing: Easing::EaseInOut,
170        }
171    }
172
173    pub const fn shutter_down(duration_ms: u32) -> Self {
174        Self {
175            effect: ScreenTransitionEffect::ShutterDown,
176            duration_ms,
177            origin: ScreenTransitionOrigin::Center,
178            easing: Easing::EaseInOut,
179        }
180    }
181
182    pub const fn round_flip_left(duration_ms: u32) -> Self {
183        Self {
184            effect: ScreenTransitionEffect::RoundFlipLeft,
185            duration_ms,
186            origin: ScreenTransitionOrigin::Center,
187            easing: Easing::Linear,
188        }
189    }
190
191    pub const fn round_flip_right(duration_ms: u32) -> Self {
192        Self {
193            effect: ScreenTransitionEffect::RoundFlipRight,
194            duration_ms,
195            origin: ScreenTransitionOrigin::Center,
196            easing: Easing::Linear,
197        }
198    }
199
200    pub const fn port_hole_left(duration_ms: u32) -> Self {
201        Self {
202            effect: ScreenTransitionEffect::PortHoleLeft,
203            duration_ms,
204            origin: ScreenTransitionOrigin::Center,
205            easing: Easing::EaseInOut,
206        }
207    }
208
209    pub const fn port_hole_right(duration_ms: u32) -> Self {
210        Self {
211            effect: ScreenTransitionEffect::PortHoleRight,
212            duration_ms,
213            origin: ScreenTransitionOrigin::Center,
214            easing: Easing::EaseInOut,
215        }
216    }
217
218    pub const fn port_hole_up(duration_ms: u32) -> Self {
219        Self {
220            effect: ScreenTransitionEffect::PortHoleUp,
221            duration_ms,
222            origin: ScreenTransitionOrigin::Center,
223            easing: Easing::EaseInOut,
224        }
225    }
226
227    pub const fn port_hole_down(duration_ms: u32) -> Self {
228        Self {
229            effect: ScreenTransitionEffect::PortHoleDown,
230            duration_ms,
231            origin: ScreenTransitionOrigin::Center,
232            easing: Easing::EaseInOut,
233        }
234    }
235
236    pub const fn modal_slide_up(duration_ms: u32) -> Self {
237        Self {
238            effect: ScreenTransitionEffect::ModalSlideUp,
239            duration_ms,
240            origin: ScreenTransitionOrigin::Bottom,
241            easing: Easing::EaseOut,
242        }
243    }
244
245    pub const fn modal_slide_down(duration_ms: u32) -> Self {
246        Self {
247            effect: ScreenTransitionEffect::ModalSlideDown,
248            duration_ms,
249            origin: ScreenTransitionOrigin::Top,
250            easing: Easing::EaseOut,
251        }
252    }
253
254    pub const fn zoom(duration_ms: u32) -> Self {
255        Self {
256            effect: ScreenTransitionEffect::Zoom,
257            duration_ms,
258            origin: ScreenTransitionOrigin::Center,
259            easing: Easing::InOutSine,
260        }
261    }
262
263    pub const fn circular_reveal(duration_ms: u32) -> Self {
264        Self {
265            effect: ScreenTransitionEffect::CircularReveal,
266            duration_ms,
267            origin: ScreenTransitionOrigin::Center,
268            easing: Easing::InOutSine,
269        }
270    }
271
272    pub const fn wipe_left(duration_ms: u32) -> Self {
273        Self {
274            effect: ScreenTransitionEffect::WipeLeft,
275            duration_ms,
276            origin: ScreenTransitionOrigin::Center,
277            easing: Easing::InOutSine,
278        }
279    }
280
281    pub const fn wipe_right(duration_ms: u32) -> Self {
282        Self {
283            effect: ScreenTransitionEffect::WipeRight,
284            duration_ms,
285            origin: ScreenTransitionOrigin::Center,
286            easing: Easing::InOutSine,
287        }
288    }
289
290    pub const fn wipe_up(duration_ms: u32) -> Self {
291        Self {
292            effect: ScreenTransitionEffect::WipeUp,
293            duration_ms,
294            origin: ScreenTransitionOrigin::Center,
295            easing: Easing::InOutSine,
296        }
297    }
298
299    pub const fn wipe_down(duration_ms: u32) -> Self {
300        Self {
301            effect: ScreenTransitionEffect::WipeDown,
302            duration_ms,
303            origin: ScreenTransitionOrigin::Center,
304            easing: Easing::InOutSine,
305        }
306    }
307
308    pub const fn with_origin(mut self, origin: ScreenTransitionOrigin) -> Self {
309        self.origin = origin;
310        self
311    }
312
313    pub const fn with_easing(mut self, easing: Easing) -> Self {
314        self.easing = easing;
315        self
316    }
317}
318
319#[derive(Clone, Copy, Debug, PartialEq)]
320pub struct ActiveScreenTransition {
321    pub from: Option<ScreenId>,
322    pub to: Option<ScreenId>,
323    pub effect: ScreenTransitionEffect,
324    pub origin: ScreenTransitionOrigin,
325    pub progress: f32,
326}
327
328impl ActiveScreenTransition {
329    pub fn opacity_u8(&self) -> u8 {
330        (self.progress.clamp(0.0, 1.0) * 255.0) as u8
331    }
332
333    pub fn slide_offset_x(&self, width: u32) -> i32 {
334        let t = eased_progress(self.progress, self.effect);
335        let px = (width as f32 * t).round() as i32;
336        match self.effect {
337            ScreenTransitionEffect::SlideLeft
338            | ScreenTransitionEffect::ShutterLeft
339            | ScreenTransitionEffect::PortHoleLeft => -px,
340            ScreenTransitionEffect::SlideRight
341            | ScreenTransitionEffect::PushMoook
342            | ScreenTransitionEffect::ShutterRight
343            | ScreenTransitionEffect::PortHoleRight
344            | ScreenTransitionEffect::RoundFlipRight => px,
345            ScreenTransitionEffect::PopMoook => px,
346            _ => 0,
347        }
348    }
349
350    pub fn slide_offset_y(&self, height: u32) -> i32 {
351        let t = eased_progress(self.progress, self.effect);
352        let px = (height as f32 * t).round() as i32;
353        match self.effect {
354            ScreenTransitionEffect::SlideUp
355            | ScreenTransitionEffect::ShutterUp
356            | ScreenTransitionEffect::PortHoleUp
357            | ScreenTransitionEffect::ModalSlideUp => -px,
358            ScreenTransitionEffect::SlideDown
359            | ScreenTransitionEffect::ShutterDown
360            | ScreenTransitionEffect::PortHoleDown
361            | ScreenTransitionEffect::ModalSlideDown => px,
362            _ => 0,
363        }
364    }
365}
366
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
368pub struct ScreenTransitionSample {
369    pub outgoing_offset_x: i32,
370    pub outgoing_offset_y: i32,
371    pub incoming_offset_x: i32,
372    pub incoming_offset_y: i32,
373    pub outgoing_opacity: u8,
374    pub incoming_opacity: u8,
375    pub outgoing_clip: Option<Rect>,
376    pub incoming_clip: Option<Rect>,
377}
378
379fn eased_progress(progress: f32, effect: ScreenTransitionEffect) -> f32 {
380    match effect {
381        ScreenTransitionEffect::PushMoook | ScreenTransitionEffect::PopMoook => {
382            animation_timing::moook_curve(progress)
383        }
384        _ => progress.clamp(0.0, 1.0),
385    }
386}
387
388fn shutter_offset(progress: f32, viewport: u32, horizontal: bool, negative: bool) -> (i32, i32) {
389    let (phase_t, first_half) = timing_shutter_phase(progress);
390    let span = viewport as i32;
391    let sign = if negative { -1 } else { 1 };
392    if horizontal {
393        if first_half {
394            (sign * -((span as f32 * phase_t).round() as i32), 0)
395        } else {
396            (
397                sign * span,
398                sign * (span - (span as f32 * phase_t).round() as i32),
399            )
400        }
401    } else if first_half {
402        (0, sign * -((span as f32 * phase_t).round() as i32))
403    } else {
404        (0, sign * (span - (span as f32 * phase_t).round() as i32))
405    }
406}
407
408fn port_hole_offsets(
409    progress: f32,
410    viewport_w: u32,
411    viewport_h: u32,
412    horizontal: bool,
413    negative: bool,
414) -> (i32, i32, i32, i32) {
415    let viewport = if horizontal { viewport_w } else { viewport_h };
416    let (out, inc) = {
417        let (phase_t, first_half) = timing_half_phase(progress);
418        let gap = (viewport as f32 * 80.0 / 180.0).round() as i32;
419        let full = viewport as i32;
420        let sign = if negative { -1 } else { 1 };
421        if first_half {
422            (sign * (full - (gap as f32 * phase_t) as i32), sign * full)
423        } else {
424            (sign * gap, sign * ((gap as f32 * (1.0 - phase_t)) as i32))
425        }
426    };
427    if horizontal {
428        (out, 0, inc, 0)
429    } else {
430        (0, out, 0, inc)
431    }
432}
433
434impl ActiveScreenTransition {
435    pub fn sample(&self, viewport_w: u32, viewport_h: u32) -> ScreenTransitionSample {
436        match self.effect {
437            ScreenTransitionEffect::Fade => {
438                let incoming = self.opacity_u8();
439                ScreenTransitionSample {
440                    outgoing_offset_x: 0,
441                    outgoing_offset_y: 0,
442                    incoming_offset_x: 0,
443                    incoming_offset_y: 0,
444                    outgoing_opacity: 255u8.saturating_sub(incoming),
445                    incoming_opacity: incoming,
446                    outgoing_clip: None,
447                    incoming_clip: None,
448                }
449            }
450            ScreenTransitionEffect::SlideLeft | ScreenTransitionEffect::PushMoook => {
451                let out = self.slide_offset_x(viewport_w);
452                ScreenTransitionSample {
453                    outgoing_offset_x: out,
454                    outgoing_offset_y: 0,
455                    incoming_offset_x: out + viewport_w as i32,
456                    incoming_offset_y: 0,
457                    outgoing_opacity: 255,
458                    incoming_opacity: 255,
459                    outgoing_clip: None,
460                    incoming_clip: None,
461                }
462            }
463            ScreenTransitionEffect::SlideRight | ScreenTransitionEffect::PopMoook => {
464                let out = self.slide_offset_x(viewport_w);
465                ScreenTransitionSample {
466                    outgoing_offset_x: out,
467                    outgoing_offset_y: 0,
468                    incoming_offset_x: out - viewport_w as i32,
469                    incoming_offset_y: 0,
470                    outgoing_opacity: 255,
471                    incoming_opacity: 255,
472                    outgoing_clip: None,
473                    incoming_clip: None,
474                }
475            }
476            ScreenTransitionEffect::SlideUp | ScreenTransitionEffect::ModalSlideUp => {
477                let out = self.slide_offset_y(viewport_h);
478                ScreenTransitionSample {
479                    outgoing_offset_x: 0,
480                    outgoing_offset_y: out,
481                    incoming_offset_x: 0,
482                    incoming_offset_y: out + viewport_h as i32,
483                    outgoing_opacity: 255,
484                    incoming_opacity: 255,
485                    outgoing_clip: None,
486                    incoming_clip: None,
487                }
488            }
489            ScreenTransitionEffect::SlideDown | ScreenTransitionEffect::ModalSlideDown => {
490                let out = self.slide_offset_y(viewport_h);
491                ScreenTransitionSample {
492                    outgoing_offset_x: 0,
493                    outgoing_offset_y: out,
494                    incoming_offset_x: 0,
495                    incoming_offset_y: out - viewport_h as i32,
496                    outgoing_opacity: 255,
497                    incoming_opacity: 255,
498                    outgoing_clip: None,
499                    incoming_clip: None,
500                }
501            }
502            ScreenTransitionEffect::ShutterLeft => {
503                let (ox, ix) = shutter_offset(self.progress, viewport_w, true, true);
504                ScreenTransitionSample {
505                    outgoing_offset_x: ox,
506                    outgoing_offset_y: 0,
507                    incoming_offset_x: ix,
508                    incoming_offset_y: 0,
509                    outgoing_opacity: 255,
510                    incoming_opacity: 255,
511                    outgoing_clip: None,
512                    incoming_clip: None,
513                }
514            }
515            ScreenTransitionEffect::ShutterRight => {
516                let (ox, ix) = shutter_offset(self.progress, viewport_w, true, false);
517                ScreenTransitionSample {
518                    outgoing_offset_x: ox,
519                    outgoing_offset_y: 0,
520                    incoming_offset_x: ix,
521                    incoming_offset_y: 0,
522                    outgoing_opacity: 255,
523                    incoming_opacity: 255,
524                    outgoing_clip: None,
525                    incoming_clip: None,
526                }
527            }
528            ScreenTransitionEffect::ShutterUp => {
529                let (oy, iy) = shutter_offset(self.progress, viewport_h, false, true);
530                ScreenTransitionSample {
531                    outgoing_offset_x: 0,
532                    outgoing_offset_y: oy,
533                    incoming_offset_x: 0,
534                    incoming_offset_y: iy,
535                    outgoing_opacity: 255,
536                    incoming_opacity: 255,
537                    outgoing_clip: None,
538                    incoming_clip: None,
539                }
540            }
541            ScreenTransitionEffect::ShutterDown => {
542                let (oy, iy) = shutter_offset(self.progress, viewport_h, false, false);
543                ScreenTransitionSample {
544                    outgoing_offset_x: 0,
545                    outgoing_offset_y: oy,
546                    incoming_offset_x: 0,
547                    incoming_offset_y: iy,
548                    outgoing_opacity: 255,
549                    incoming_opacity: 255,
550                    outgoing_clip: None,
551                    incoming_clip: None,
552                }
553            }
554            ScreenTransitionEffect::PortHoleLeft => {
555                let (ox, oy, ix, iy) =
556                    port_hole_offsets(self.progress, viewport_w, viewport_h, true, true);
557                ScreenTransitionSample {
558                    outgoing_offset_x: ox,
559                    outgoing_offset_y: oy,
560                    incoming_offset_x: ix,
561                    incoming_offset_y: iy,
562                    outgoing_opacity: 255,
563                    incoming_opacity: 255,
564                    outgoing_clip: None,
565                    incoming_clip: None,
566                }
567            }
568            ScreenTransitionEffect::PortHoleRight => {
569                let (ox, oy, ix, iy) =
570                    port_hole_offsets(self.progress, viewport_w, viewport_h, true, false);
571                ScreenTransitionSample {
572                    outgoing_offset_x: ox,
573                    outgoing_offset_y: oy,
574                    incoming_offset_x: ix,
575                    incoming_offset_y: iy,
576                    outgoing_opacity: 255,
577                    incoming_opacity: 255,
578                    outgoing_clip: None,
579                    incoming_clip: None,
580                }
581            }
582            ScreenTransitionEffect::PortHoleUp => {
583                let (ox, oy, ix, iy) =
584                    port_hole_offsets(self.progress, viewport_w, viewport_h, false, true);
585                ScreenTransitionSample {
586                    outgoing_offset_x: ox,
587                    outgoing_offset_y: oy,
588                    incoming_offset_x: ix,
589                    incoming_offset_y: iy,
590                    outgoing_opacity: 255,
591                    incoming_opacity: 255,
592                    outgoing_clip: None,
593                    incoming_clip: None,
594                }
595            }
596            ScreenTransitionEffect::PortHoleDown => {
597                let (ox, oy, ix, iy) =
598                    port_hole_offsets(self.progress, viewport_w, viewport_h, false, false);
599                ScreenTransitionSample {
600                    outgoing_offset_x: ox,
601                    outgoing_offset_y: oy,
602                    incoming_offset_x: ix,
603                    incoming_offset_y: iy,
604                    outgoing_opacity: 255,
605                    incoming_opacity: 255,
606                    outgoing_clip: None,
607                    incoming_clip: None,
608                }
609            }
610            ScreenTransitionEffect::RoundFlipLeft | ScreenTransitionEffect::RoundFlipRight => {
611                let (out_clip, in_clip) = round_flip_clip(viewport_w, viewport_h, self.progress);
612                ScreenTransitionSample {
613                    outgoing_offset_x: 0,
614                    outgoing_offset_y: 0,
615                    incoming_offset_x: 0,
616                    incoming_offset_y: 0,
617                    outgoing_opacity: if self.progress < 0.5 { 255 } else { 128 },
618                    incoming_opacity: if self.progress < 0.5 { 128 } else { 255 },
619                    outgoing_clip: out_clip,
620                    incoming_clip: in_clip,
621                }
622            }
623            ScreenTransitionEffect::None => ScreenTransitionSample {
624                outgoing_offset_x: 0,
625                outgoing_offset_y: 0,
626                incoming_offset_x: 0,
627                incoming_offset_y: 0,
628                outgoing_opacity: 255,
629                incoming_opacity: 255,
630                outgoing_clip: None,
631                incoming_clip: None,
632            },
633            ScreenTransitionEffect::Zoom => {
634                let incoming = self.opacity_u8();
635                ScreenTransitionSample {
636                    outgoing_offset_x: 0,
637                    outgoing_offset_y: 0,
638                    incoming_offset_x: 0,
639                    incoming_offset_y: 0,
640                    outgoing_opacity: 255u8.saturating_sub(incoming / 2),
641                    incoming_opacity: incoming,
642                    outgoing_clip: None,
643                    incoming_clip: None,
644                }
645            }
646            ScreenTransitionEffect::CircularReveal => {
647                let incoming = self.opacity_u8();
648                let clip = reveal_clip(viewport_w, viewport_h, self.progress, self.origin);
649                ScreenTransitionSample {
650                    outgoing_offset_x: 0,
651                    outgoing_offset_y: 0,
652                    incoming_offset_x: 0,
653                    incoming_offset_y: 0,
654                    outgoing_opacity: 255u8.saturating_sub(incoming / 4),
655                    incoming_opacity: incoming,
656                    outgoing_clip: None,
657                    incoming_clip: Some(clip),
658                }
659            }
660            ScreenTransitionEffect::WipeLeft
661            | ScreenTransitionEffect::WipeRight
662            | ScreenTransitionEffect::WipeUp
663            | ScreenTransitionEffect::WipeDown => {
664                let clip = wipe_clip(viewport_w, viewport_h, self.progress, self.effect);
665                ScreenTransitionSample {
666                    outgoing_offset_x: 0,
667                    outgoing_offset_y: 0,
668                    incoming_offset_x: 0,
669                    incoming_offset_y: 0,
670                    outgoing_opacity: 255,
671                    incoming_opacity: 255,
672                    outgoing_clip: None,
673                    incoming_clip: Some(clip),
674                }
675            }
676        }
677    }
678}
679
680fn round_flip_clip(
681    viewport_w: u32,
682    viewport_h: u32,
683    progress: f32,
684) -> (Option<Rect>, Option<Rect>) {
685    let h = viewport_h as i32;
686    let mid = h / 2;
687    let scale = if progress < 0.5 {
688        1.0 - progress * 2.0
689    } else {
690        (progress - 0.5) * 2.0
691    };
692    let visible = ((h as f32 * scale).round() as i32).max(1);
693    let top = mid - visible / 2;
694    let clip = Rect::new(0, top.max(0), viewport_w, visible.max(1) as u32);
695    if progress < 0.5 {
696        (None, Some(clip))
697    } else {
698        (Some(clip), Some(clip))
699    }
700}
701
702fn reveal_clip(
703    viewport_w: u32,
704    viewport_h: u32,
705    progress: f32,
706    origin: ScreenTransitionOrigin,
707) -> Rect {
708    let (cx, cy) = origin_point(viewport_w, viewport_h, origin);
709    let max_radius = (((viewport_w as f32).hypot(viewport_h as f32)) * 0.5).ceil() as i32;
710    let radius = ((max_radius as f32) * progress.clamp(0.0, 1.0)).ceil() as i32;
711    let left = (cx - radius).clamp(0, viewport_w as i32);
712    let top = (cy - radius).clamp(0, viewport_h as i32);
713    let right = (cx + radius).clamp(0, viewport_w as i32);
714    let bottom = (cy + radius).clamp(0, viewport_h as i32);
715    Rect::new(
716        left,
717        top,
718        (right - left).max(0) as u32,
719        (bottom - top).max(0) as u32,
720    )
721}
722
723fn origin_point(viewport_w: u32, viewport_h: u32, origin: ScreenTransitionOrigin) -> (i32, i32) {
724    let mid_x = viewport_w as i32 / 2;
725    let mid_y = viewport_h as i32 / 2;
726    let max_x = viewport_w as i32;
727    let max_y = viewport_h as i32;
728    match origin {
729        ScreenTransitionOrigin::Center => (mid_x, mid_y),
730        ScreenTransitionOrigin::TopLeft => (0, 0),
731        ScreenTransitionOrigin::Top => (mid_x, 0),
732        ScreenTransitionOrigin::TopRight => (max_x, 0),
733        ScreenTransitionOrigin::Left => (0, mid_y),
734        ScreenTransitionOrigin::Right => (max_x, mid_y),
735        ScreenTransitionOrigin::BottomLeft => (0, max_y),
736        ScreenTransitionOrigin::Bottom => (mid_x, max_y),
737        ScreenTransitionOrigin::BottomRight => (max_x, max_y),
738    }
739}
740
741fn wipe_clip(
742    viewport_w: u32,
743    viewport_h: u32,
744    progress: f32,
745    effect: ScreenTransitionEffect,
746) -> Rect {
747    let w = viewport_w as i32;
748    let h = viewport_h as i32;
749    let p = progress.clamp(0.0, 1.0);
750    match effect {
751        ScreenTransitionEffect::WipeLeft => {
752            let visible = (w as f32 * p).round() as i32;
753            Rect::new(0, 0, visible.max(0) as u32, viewport_h)
754        }
755        ScreenTransitionEffect::WipeRight => {
756            let visible = (w as f32 * p).round() as i32;
757            Rect::new((w - visible).max(0), 0, visible.max(0) as u32, viewport_h)
758        }
759        ScreenTransitionEffect::WipeUp => {
760            let visible = (h as f32 * p).round() as i32;
761            Rect::new(0, 0, viewport_w, visible.max(0) as u32)
762        }
763        ScreenTransitionEffect::WipeDown => {
764            let visible = (h as f32 * p).round() as i32;
765            Rect::new(0, (h - visible).max(0), viewport_w, visible.max(0) as u32)
766        }
767        _ => Rect::new(0, 0, viewport_w, viewport_h),
768    }
769}
770
771#[derive(Clone, Copy, Debug, PartialEq)]
772pub struct ScreenTransitionRunner {
773    animation: Option<Animation>,
774    active: Option<ActiveScreenTransition>,
775}
776
777impl ScreenTransitionRunner {
778    pub const fn new() -> Self {
779        Self {
780            animation: None,
781            active: None,
782        }
783    }
784
785    pub fn apply<const N: usize, const M: usize>(
786        &mut self,
787        stack: &mut ScreenStack<N>,
788        command: ScreenCommand,
789        spec: ScreenTransitionSpec,
790        lifecycle_events: &mut Vec<ScreenLifecycleEvent, M>,
791    ) -> Result<(), ScreenStackError> {
792        let transition = stack.apply_lifecycle(command, lifecycle_events)?;
793        if spec.effect == ScreenTransitionEffect::None || spec.duration_ms == 0 {
794            self.animation = None;
795            self.active = None;
796            return Ok(());
797        }
798        let anim = Animation::new(0.0, 1.0, spec.duration_ms, spec.easing);
799        self.animation = Some(anim);
800        self.active = Some(ActiveScreenTransition {
801            from: transition.from,
802            to: transition.to,
803            effect: spec.effect,
804            origin: spec.origin,
805            progress: 0.0,
806        });
807        Ok(())
808    }
809
810    pub fn tick(&mut self, dt_ms: u32) {
811        let Some(animation) = self.animation.as_mut() else {
812            return;
813        };
814        animation.tick(dt_ms);
815        if let Some(active) = self.active.as_mut() {
816            active.progress = animation.value();
817        }
818        if animation.is_done() {
819            self.animation = None;
820            self.active = None;
821        }
822    }
823
824    pub fn active(&self) -> Option<ActiveScreenTransition> {
825        self.active
826    }
827}
828
829impl Default for ScreenTransitionRunner {
830    fn default() -> Self {
831        Self::new()
832    }
833}
834
835pub fn render_transition_pair<'a, D, const NODES: usize, const EVENTS: usize, const DIRTY: usize>(
836    target: &mut D,
837    outgoing: &GuiContext<'a, NODES, EVENTS, DIRTY>,
838    incoming: &GuiContext<'a, NODES, EVENTS, DIRTY>,
839    active: ActiveScreenTransition,
840    viewport_w: u32,
841    viewport_h: u32,
842) -> Result<(), D::Error>
843where
844    D: embedded_graphics_core::draw_target::DrawTarget<
845            Color = embedded_graphics_core::pixelcolor::Rgb565,
846        >,
847{
848    let sample = active.sample(viewport_w, viewport_h);
849    if let Some(clip) = sample.outgoing_clip {
850        outgoing.render_with_offset_opacity_and_clip(
851            target,
852            sample.outgoing_offset_x,
853            sample.outgoing_offset_y,
854            sample.outgoing_opacity,
855            clip,
856        )?;
857    } else {
858        outgoing.render_with_offset_and_opacity(
859            target,
860            sample.outgoing_offset_x,
861            sample.outgoing_offset_y,
862            sample.outgoing_opacity,
863        )?;
864    }
865    if let Some(clip) = sample.incoming_clip {
866        incoming.render_with_offset_opacity_and_clip(
867            target,
868            sample.incoming_offset_x,
869            sample.incoming_offset_y,
870            sample.incoming_opacity,
871            clip,
872        )?;
873    } else {
874        incoming.render_with_offset_and_opacity(
875            target,
876            sample.incoming_offset_x,
877            sample.incoming_offset_y,
878            sample.incoming_opacity,
879        )?;
880    }
881    Ok(())
882}