faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{
    mono_font::{
        MonoTextStyleBuilder,
        ascii::{FONT_7X14, FONT_9X18_BOLD},
    },
    pixelcolor::Rgb565,
    prelude::*,
    primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, RoundedRectangle},
    text::{Alignment, Baseline, Text, TextStyleBuilder},
};
use heapless::Vec;

use crate::{
    AlertAction, AlertButtonRole, AlertHostError, AlertKind, AlertSpec, Button, ButtonKind,
    ButtonSpec, ButtonTouchResponse, ButtonTouchState, FsTheme, I18n, Localized, TouchEvent,
    UiCanvas,
    alert_layout::{alert_layout, alert_layout_in_panel, message_top},
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct PresentedAlert<'a, Modal, const MAX_ACTIONS: usize> {
    key: Modal,
    kind: AlertKind,
    title: Localized<'a>,
    message: Localized<'a>,
    actions: Vec<AlertAction<'a>, MAX_ACTIONS>,
    touch_state: ButtonTouchState<u8>,
}

impl<'a, Modal, const MAX_ACTIONS: usize> PresentedAlert<'a, Modal, MAX_ACTIONS>
where
    Modal: Copy + Eq,
{
    pub(super) fn new<const N: usize>(
        key: Modal,
        spec: AlertSpec<'a, N>,
    ) -> Result<Self, AlertHostError> {
        let mut actions = Vec::new();
        for action in spec.actions {
            actions
                .push(action)
                .map_err(|_| AlertHostError::TooManyActions)?;
        }
        Ok(Self {
            key,
            kind: spec.kind,
            title: spec.title,
            message: spec.message,
            actions,
            touch_state: ButtonTouchState::new(),
        })
    }

    pub(super) fn key(&self) -> Modal {
        self.key
    }

    pub(super) fn title(&self) -> Localized<'a> {
        self.title
    }

    pub(super) fn clear_touch_state(&mut self) {
        self.touch_state.clear();
    }

    pub(super) fn panel(&self, bounds: Rectangle, theme: &FsTheme, i18n: &I18n<'a>) -> Rectangle {
        alert_layout::<1>(bounds, theme, i18n.text(self.message)).panel
    }

    pub(super) fn handle_touch(
        &mut self,
        touch: TouchEvent,
        panel: Rectangle,
    ) -> ButtonTouchResponse<u8> {
        let buttons = self.buttons(panel);
        self.touch_state.handle_touch(touch, buttons.as_slice())
    }

    pub(super) fn draw<D>(
        &self,
        display: &mut D,
        panel: Rectangle,
        theme: &FsTheme,
        i18n: &I18n<'a>,
    ) where
        D: UiCanvas,
    {
        let layout = alert_layout_in_panel::<1>(panel, i18n.text(self.message));
        RoundedRectangle::with_equal_corners(
            Rectangle::new(panel.top_left + Point::new(6, 8), panel.size),
            Size::new(22, 22),
        )
        .into_styled(PrimitiveStyle::with_fill(theme.dim))
        .draw(display)
        .ok();
        RoundedRectangle::with_equal_corners(panel, Size::new(22, 22))
            .into_styled(
                PrimitiveStyleBuilder::new()
                    .fill_color(theme.surface)
                    .stroke_color(theme.surface_alt)
                    .stroke_width(2)
                    .build(),
            )
            .draw(display)
            .ok();

        let title_style = MonoTextStyleBuilder::new()
            .font(&FONT_9X18_BOLD)
            .text_color(kind_color(self.kind, theme))
            .build();
        let message_style = MonoTextStyleBuilder::new()
            .font(&FONT_7X14)
            .text_color(theme.text_secondary)
            .build();
        let text_style = TextStyleBuilder::new()
            .alignment(Alignment::Center)
            .baseline(Baseline::Middle)
            .build();

        Text::with_text_style(
            i18n.text(self.title),
            panel.center() + Point::new(0, -40),
            title_style,
            text_style,
        )
        .draw(display)
        .ok();
        for (index, line) in layout.message_lines.iter().enumerate() {
            Text::with_text_style(
                line,
                Point::new(panel.center().x, message_top(panel) + (index as i32 * 18)),
                message_style,
                text_style,
            )
            .draw(display)
            .ok();
        }
        for button in self.buttons(panel) {
            button.draw_state(
                display,
                theme,
                i18n,
                self.touch_state.is_highlighted(&button),
            );
        }
    }

    fn buttons(&self, panel: Rectangle) -> Vec<Button<'a, u8>, MAX_ACTIONS> {
        let count = self.actions.len().max(1);
        let mut buttons = Vec::new();
        for (index, action) in self.actions.iter().enumerate() {
            let _ = buttons.push(Button::new(
                button_frame(panel, count, index),
                ButtonSpec {
                    key: action.id,
                    icon: None,
                    label: action.label,
                    kind: button_kind(action.role),
                },
            ));
        }
        buttons
    }
}

fn button_frame(panel: Rectangle, count: usize, index: usize) -> Rectangle {
    let width = panel.size.width.saturating_sub(36);
    let button_width = width / count as u32;
    let x = panel.top_left.x + 18 + (index as i32 * button_width as i32);
    let width = if index + 1 == count {
        panel
            .size
            .width
            .saturating_sub(36 + button_width * index as u32)
    } else {
        button_width.saturating_sub(8)
    };
    Rectangle::new(
        Point::new(x, panel.top_left.y + panel.size.height as i32 - 62),
        Size::new(width, 40),
    )
}

fn kind_color(kind: AlertKind, theme: &FsTheme) -> Rgb565 {
    match kind {
        AlertKind::Error => theme.danger,
        AlertKind::Warning => theme.warning,
        AlertKind::Confirm => theme.accent,
    }
}

fn button_kind(role: AlertButtonRole) -> ButtonKind {
    match role {
        AlertButtonRole::Primary => ButtonKind::Primary,
        AlertButtonRole::Secondary => ButtonKind::Secondary,
        AlertButtonRole::Destructive => ButtonKind::Destructive,
    }
}