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#[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 PushMoook,
25 PopMoook,
27 Zoom,
28 CircularReveal,
29 WipeLeft,
30 WipeRight,
31 WipeUp,
32 WipeDown,
33 ShutterLeft,
35 ShutterRight,
36 ShutterUp,
37 ShutterDown,
38 RoundFlipLeft,
40 RoundFlipRight,
41 PortHoleLeft,
43 PortHoleRight,
44 PortHoleUp,
45 PortHoleDown,
46 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}