Skip to main content

faststep/
modal.rs

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/// Generic modal transition state machine used by higher-level hosts.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct ModalHost<M> {
19    state: ModalState<M>,
20}
21
22impl<M: Copy> ModalHost<M> {
23    /// Creates an empty modal host.
24    pub const fn new() -> Self {
25        Self {
26            state: ModalState::Hidden,
27        }
28    }
29
30    /// Starts presenting `modal`.
31    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    /// Starts dismissing the current modal, if any.
39    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    /// Returns whether a show or hide animation is in progress.
55    pub const fn is_animating(&self) -> bool {
56        matches!(
57            self.state,
58            ModalState::Showing { .. } | ModalState::Hiding { .. }
59        )
60    }
61
62    /// Returns whether any modal is currently owned by the host.
63    pub const fn has_modal(&self) -> bool {
64        !matches!(self.state, ModalState::Hidden)
65    }
66
67    /// Advances the modal animation state.
68    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    /// Returns the current modal layer using the default themed panel.
95    pub fn current(&self, bounds: Rectangle, theme: &FsTheme) -> Option<ModalLayer<M>> {
96        self.current_with_panel(bounds, modal_panel(bounds, theme))
97    }
98
99    /// Returns the current modal layer using an explicit panel rectangle.
100    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}