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