faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
mod palette;

use embedded_graphics::{
    mono_font::{
        MonoTextStyleBuilder,
        ascii::{FONT_7X14, FONT_9X18_BOLD},
    },
    pixelcolor::Rgb565,
    prelude::*,
    primitives::{PrimitiveStyleBuilder, Rectangle, RoundedRectangle},
    text::{Alignment, Baseline, Text, TextStyleBuilder},
};

use crate::{ButtonTouchResponse, FsTheme, I18n, Localized, TouchEvent, TouchPhase};

use palette::{background, border, foreground};

/// Semantic role for a button.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ButtonKind {
    /// Primary call-to-action styling.
    Primary,
    /// Secondary neutral styling.
    Secondary,
    /// Destructive styling.
    Destructive,
    /// Minimal low-emphasis styling.
    Ghost,
}

/// Immutable button configuration.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ButtonSpec<'a, K> {
    /// Action key emitted on activation.
    pub key: K,
    /// Optional icon glyph rendered above the label.
    pub icon: Option<&'a str>,
    /// Localized button label.
    pub label: Localized<'a>,
    /// Semantic button role.
    pub kind: ButtonKind,
}

/// Reusable button view that owns its highlight state.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Button<'a, K> {
    /// Outer frame for the button.
    pub frame: Rectangle,
    /// Immutable button configuration.
    pub spec: ButtonSpec<'a, K>,
    touch_active: bool,
    highlighted: bool,
}

impl<'a, K: Copy> Button<'a, K> {
    /// Creates a button from a frame and spec.
    pub const fn new(frame: Rectangle, spec: ButtonSpec<'a, K>) -> Self {
        Self {
            frame,
            spec,
            touch_active: false,
            highlighted: false,
        }
    }

    /// Updates the button frame.
    pub fn set_frame(&mut self, frame: Rectangle) {
        self.frame = frame;
    }

    /// Clears transient touch state.
    pub fn clear_touch_state(&mut self) {
        self.touch_active = false;
        self.highlighted = false;
    }

    /// Returns whether the button is highlighted.
    pub const fn is_highlighted(&self) -> bool {
        self.highlighted
    }

    /// Draws the button using its current internal highlight state.
    pub fn draw<D>(&self, display: &mut D, theme: &FsTheme, i18n: &I18n<'a>)
    where
        D: DrawTarget<Color = Rgb565>,
    {
        self.draw_state(display, theme, i18n, self.is_highlighted());
    }

    /// Updates touch tracking and returns button interaction output.
    pub fn handle_touch(&mut self, touch: TouchEvent) -> ButtonTouchResponse<K> {
        match touch.phase {
            TouchPhase::Start => self.start_touch(touch),
            TouchPhase::Move => self.move_touch(touch),
            TouchPhase::End => self.finish_touch(touch),
            TouchPhase::Cancel => self.cancel_touch(),
        }
    }

    /// Draws the button using an explicit highlight state.
    pub fn draw_state<D>(
        &self,
        display: &mut D,
        theme: &FsTheme,
        i18n: &I18n<'a>,
        highlighted: bool,
    ) where
        D: DrawTarget<Color = Rgb565>,
    {
        let style = PrimitiveStyleBuilder::new()
            .fill_color(background(self.spec.kind, theme, highlighted))
            .stroke_color(border(self.spec.kind, theme, highlighted))
            .stroke_width(if highlighted { 3 } else { 2 })
            .build();
        RoundedRectangle::with_equal_corners(self.frame, Size::new(18, 18))
            .into_styled(style)
            .draw(display)
            .ok();

        let label_style = MonoTextStyleBuilder::new()
            .font(&FONT_7X14)
            .text_color(foreground(self.spec.kind, theme, highlighted))
            .build();
        let icon_style = MonoTextStyleBuilder::new()
            .font(&FONT_9X18_BOLD)
            .text_color(foreground(self.spec.kind, theme, highlighted))
            .build();
        let text_style = TextStyleBuilder::new()
            .alignment(Alignment::Center)
            .baseline(Baseline::Middle)
            .build();

        let mut label_center = self.frame.center();
        if let Some(icon) = self.spec.icon {
            Text::with_text_style(
                icon,
                self.frame.center() + Point::new(0, -10),
                icon_style,
                text_style,
            )
            .draw(display)
            .ok();
            label_center += Point::new(0, 14);
        }

        Text::with_text_style(
            i18n.text(self.spec.label),
            label_center,
            label_style,
            text_style,
        )
        .draw(display)
        .ok();
    }

    fn start_touch(&mut self, touch: TouchEvent) -> ButtonTouchResponse<K> {
        let was_highlighted = self.highlighted;
        if touch.within(self.frame) {
            self.touch_active = true;
            self.highlighted = true;
            return ButtonTouchResponse {
                action: None,
                captured: true,
                redraw: !was_highlighted,
            };
        }

        self.clear_touch_state();
        ButtonTouchResponse {
            action: None,
            captured: false,
            redraw: was_highlighted,
        }
    }

    fn move_touch(&mut self, touch: TouchEvent) -> ButtonTouchResponse<K> {
        if !self.touch_active {
            return ButtonTouchResponse {
                action: None,
                captured: false,
                redraw: false,
            };
        }

        let highlighted = touch.within(self.frame);
        let redraw = highlighted != self.highlighted;
        self.highlighted = highlighted;
        ButtonTouchResponse {
            action: None,
            captured: true,
            redraw,
        }
    }

    fn finish_touch(&mut self, touch: TouchEvent) -> ButtonTouchResponse<K> {
        if !self.touch_active {
            return ButtonTouchResponse {
                action: None,
                captured: false,
                redraw: false,
            };
        }

        let action = (self.highlighted && touch.within(self.frame)).then_some(self.spec.key);
        let redraw = self.highlighted;
        self.clear_touch_state();
        ButtonTouchResponse {
            action,
            captured: true,
            redraw,
        }
    }

    fn cancel_touch(&mut self) -> ButtonTouchResponse<K> {
        if !self.touch_active {
            return ButtonTouchResponse {
                action: None,
                captured: false,
                redraw: false,
            };
        }

        let redraw = self.highlighted;
        self.clear_touch_state();
        ButtonTouchResponse {
            action: None,
            captured: true,
            redraw,
        }
    }
}