1use embedded_graphics::{prelude::*, primitives::Rectangle};
2
3use super::{Animation, Curve, FsTheme, ModalLayer, lerp_i32, lerp_u8};
4
5const MODAL_ANIMATION_MS: u32 = 320;
6const DIM_ALPHA_MAX: u8 = 96;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9enum ModalState<M> {
10 Hidden,
11 Showing { modal: M, animation: Animation },
12 Visible { modal: M },
13 Hiding { modal: M, animation: Animation },
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct ModalHost<M> {
19 state: ModalState<M>,
20}
21
22impl<M: Copy> ModalHost<M> {
23 pub const fn new() -> Self {
25 Self {
26 state: ModalState::Hidden,
27 }
28 }
29
30 pub fn show(&mut self, modal: M) {
32 self.state = ModalState::Showing {
33 modal,
34 animation: Animation::new(MODAL_ANIMATION_MS, Curve::EaseInOut),
35 };
36 }
37
38 pub fn dismiss(&mut self) {
40 let modal = match self.state {
41 ModalState::Showing { modal, .. } | ModalState::Visible { modal } => Some(modal),
42 ModalState::Hiding { modal, .. } => Some(modal),
43 ModalState::Hidden => None,
44 };
45
46 if let Some(modal) = modal {
47 self.state = ModalState::Hiding {
48 modal,
49 animation: Animation::new(MODAL_ANIMATION_MS, Curve::EaseInOut),
50 };
51 }
52 }
53
54 pub const fn is_animating(&self) -> bool {
56 matches!(
57 self.state,
58 ModalState::Showing { .. } | ModalState::Hiding { .. }
59 )
60 }
61
62 pub const fn has_modal(&self) -> bool {
64 !matches!(self.state, ModalState::Hidden)
65 }
66
67 pub fn advance(&mut self, dt_ms: u32) -> bool {
69 match &mut self.state {
70 ModalState::Hidden | ModalState::Visible { .. } => false,
71 ModalState::Showing { modal, animation } => {
72 let was_running = animation.is_running();
73 let is_running = animation.advance(dt_ms);
74 if is_running {
75 true
76 } else {
77 self.state = ModalState::Visible { modal: *modal };
78 was_running
79 }
80 }
81 ModalState::Hiding { animation, .. } => {
82 let was_running = animation.is_running();
83 let is_running = animation.advance(dt_ms);
84 if is_running {
85 true
86 } else {
87 self.state = ModalState::Hidden;
88 was_running
89 }
90 }
91 }
92 }
93
94 pub fn current(&self, bounds: Rectangle, theme: &FsTheme) -> Option<ModalLayer<M>> {
96 self.current_with_panel(bounds, modal_panel(bounds, theme))
97 }
98
99 pub fn current_with_panel(&self, bounds: Rectangle, panel: Rectangle) -> Option<ModalLayer<M>> {
101 let offscreen = offscreen_offset(bounds, panel);
102
103 match self.state {
104 ModalState::Hidden => None,
105 ModalState::Visible { modal } => Some(ModalLayer::new(modal, panel, 0, DIM_ALPHA_MAX)),
106 ModalState::Showing { modal, animation } => {
107 let progress = animation.progress_permille();
108 Some(ModalLayer::new(
109 modal,
110 panel,
111 lerp_i32(offscreen, 0, progress),
112 lerp_u8(0, DIM_ALPHA_MAX, progress),
113 ))
114 }
115 ModalState::Hiding { modal, animation } => {
116 let progress = animation.progress_permille();
117 Some(ModalLayer::new(
118 modal,
119 panel,
120 lerp_i32(0, offscreen, progress),
121 lerp_u8(DIM_ALPHA_MAX, 0, progress),
122 ))
123 }
124 }
125 }
126}
127
128fn modal_panel(bounds: Rectangle, theme: &FsTheme) -> Rectangle {
129 let max_width = bounds
130 .size
131 .width
132 .saturating_sub(theme.modal_margin.saturating_mul(2));
133 let width = ((bounds.size.width.saturating_mul(3)) / 5)
134 .max(320)
135 .min(max_width);
136 let height = (bounds.size.height / 3).max(176).min(208);
137 let x = bounds.top_left.x + ((bounds.size.width.saturating_sub(width)) / 2) as i32;
138 let y = bounds.top_left.y + ((bounds.size.height.saturating_sub(height)) / 2) as i32;
139 Rectangle::new(Point::new(x, y), Size::new(width, height))
140}
141
142fn offscreen_offset(bounds: Rectangle, panel: Rectangle) -> i32 {
143 bounds.top_left.y + bounds.size.height as i32 - panel.top_left.y + 24
144}