Skip to main content

faststep/alert/
view.rs

1use embedded_graphics::{
2    mono_font::{
3        MonoTextStyleBuilder,
4        ascii::{FONT_7X14, FONT_9X18_BOLD},
5    },
6    pixelcolor::Rgb565,
7    prelude::*,
8    primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, RoundedRectangle},
9    text::{Alignment, Baseline, Text, TextStyleBuilder},
10};
11
12use crate::alert_layout::{alert_layout, alert_layout_in_panel, button_frames, message_top};
13use crate::{
14    Button, ButtonKind, ButtonSpec, ButtonTouchResponse, ButtonTouchState, FsTheme, I18n,
15    TouchEvent,
16};
17
18use super::{AlertButtonRole, AlertKind, AlertSpec};
19
20/// Drawable alert widget for one [`AlertSpec`].
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct AlertView<'a, const N: usize> {
23    /// Immutable alert specification.
24    pub spec: AlertSpec<'a, N>,
25    touch_state: ButtonTouchState<u8>,
26}
27
28impl<'a, const N: usize> AlertView<'a, N> {
29    /// Creates an alert view for a specification.
30    pub const fn new(spec: AlertSpec<'a, N>) -> Self {
31        Self {
32            spec,
33            touch_state: ButtonTouchState::new(),
34        }
35    }
36
37    /// Clears transient button state.
38    pub fn clear_touch_state(&mut self) {
39        self.touch_state.clear();
40    }
41
42    /// Returns the preferred panel rectangle for the alert.
43    pub fn panel(&self, bounds: Rectangle, theme: &FsTheme, i18n: &I18n<'a>) -> Rectangle {
44        alert_layout::<N>(bounds, theme, i18n.text(self.spec.message)).panel
45    }
46
47    /// Routes one touch event to the alert buttons.
48    pub fn handle_touch(&mut self, touch: TouchEvent, panel: Rectangle) -> ButtonTouchResponse<u8> {
49        let buttons = self.buttons(panel);
50        self.touch_state.handle_touch(touch, &buttons)
51    }
52
53    /// Draws the alert.
54    pub fn draw<D>(&self, display: &mut D, panel: Rectangle, theme: &FsTheme, i18n: &I18n<'a>)
55    where
56        D: embedded_graphics::draw_target::DrawTarget<Color = Rgb565>,
57    {
58        self.draw_state(display, panel, theme, i18n);
59    }
60
61    /// Draws the alert using the current touch state.
62    pub fn draw_state<D>(&self, display: &mut D, panel: Rectangle, theme: &FsTheme, i18n: &I18n<'a>)
63    where
64        D: embedded_graphics::draw_target::DrawTarget<Color = Rgb565>,
65    {
66        let layout = alert_layout_in_panel::<N>(panel, i18n.text(self.spec.message));
67        RoundedRectangle::with_equal_corners(
68            Rectangle::new(panel.top_left + Point::new(6, 8), panel.size),
69            Size::new(22, 22),
70        )
71        .into_styled(PrimitiveStyle::with_fill(theme.dim))
72        .draw(display)
73        .ok();
74
75        let panel_style = PrimitiveStyleBuilder::new()
76            .fill_color(theme.surface)
77            .stroke_color(theme.surface_alt)
78            .stroke_width(2)
79            .build();
80        RoundedRectangle::with_equal_corners(panel, Size::new(22, 22))
81            .into_styled(panel_style)
82            .draw(display)
83            .ok();
84
85        let title_style = MonoTextStyleBuilder::new()
86            .font(&FONT_9X18_BOLD)
87            .text_color(kind_color(self.spec.kind, theme))
88            .build();
89        let message_style = MonoTextStyleBuilder::new()
90            .font(&FONT_7X14)
91            .text_color(theme.text_secondary)
92            .build();
93        let text_style = TextStyleBuilder::new()
94            .alignment(Alignment::Center)
95            .baseline(Baseline::Middle)
96            .build();
97
98        Text::with_text_style(
99            i18n.text(self.spec.title),
100            panel.center() + Point::new(0, -40),
101            title_style,
102            text_style,
103        )
104        .draw(display)
105        .ok();
106        for (index, line) in layout.message_lines.iter().enumerate() {
107            Text::with_text_style(
108                line,
109                Point::new(panel.center().x, message_top(panel) + (index as i32 * 18)),
110                message_style,
111                text_style,
112            )
113            .draw(display)
114            .ok();
115        }
116
117        for button in self.buttons(panel) {
118            button.draw_state(
119                display,
120                theme,
121                i18n,
122                self.touch_state.is_highlighted(&button),
123            );
124        }
125    }
126
127    fn buttons(&self, panel: Rectangle) -> [Button<'a, u8>; N] {
128        core::array::from_fn(|index| {
129            let action = self.spec.actions[index];
130            Button::new(
131                button_frames::<N>(panel)[index],
132                ButtonSpec {
133                    key: action.id,
134                    icon: None,
135                    label: action.label,
136                    kind: button_kind(action.role),
137                },
138            )
139        })
140    }
141}
142
143fn kind_color(kind: AlertKind, theme: &FsTheme) -> Rgb565 {
144    match kind {
145        AlertKind::Error => theme.danger,
146        AlertKind::Warning => theme.warning,
147        AlertKind::Confirm => theme.accent,
148    }
149}
150
151fn button_kind(role: AlertButtonRole) -> ButtonKind {
152    match role {
153        AlertButtonRole::Primary => ButtonKind::Primary,
154        AlertButtonRole::Secondary => ButtonKind::Secondary,
155        AlertButtonRole::Destructive => ButtonKind::Destructive,
156    }
157}