faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{prelude::*, primitives::Rectangle};
use heapless::{String, Vec};

use crate::FsTheme;

const MAX_MESSAGE_LINES: usize = 8;
const MAX_LINE_CHARS: usize = 96;
const LINE_HEIGHT: u32 = 18;
const TITLE_HEIGHT: u32 = 40;
const MESSAGE_TOP: i32 = 72;
const BUTTON_HEIGHT: u32 = 40;
const H_PADDING: u32 = 24;
const V_PADDING: u32 = 22;

pub(crate) type WrappedLines = Vec<String<MAX_LINE_CHARS>, MAX_MESSAGE_LINES>;

pub(crate) struct AlertLayout<const N: usize> {
    pub panel: Rectangle,
    pub message_lines: WrappedLines,
}

pub(crate) fn alert_layout<const N: usize>(
    bounds: Rectangle,
    theme: &FsTheme,
    message: &str,
) -> AlertLayout<N> {
    let max_width = bounds
        .size
        .width
        .saturating_sub(theme.modal_margin.saturating_mul(2));
    let width = ((bounds.size.width.saturating_mul(13)) / 20)
        .max(360)
        .min(max_width);
    let message_width = width.saturating_sub(H_PADDING * 2);
    let max_chars = (message_width / 7).max(12) as usize;
    let message_lines = wrap_text(message, max_chars);
    let text_height = (message_lines.len().max(1) as u32) * LINE_HEIGHT;
    let height = V_PADDING * 2 + TITLE_HEIGHT + text_height + 24 + BUTTON_HEIGHT;
    let x = bounds.top_left.x + ((bounds.size.width.saturating_sub(width)) / 2) as i32;
    let y = bounds.top_left.y + ((bounds.size.height.saturating_sub(height)) / 2) as i32;
    alert_layout_in_panel(
        Rectangle::new(Point::new(x, y), Size::new(width, height)),
        message,
    )
}

pub(crate) fn alert_layout_in_panel<const N: usize>(
    panel: Rectangle,
    message: &str,
) -> AlertLayout<N> {
    let message_width = panel.size.width.saturating_sub(H_PADDING * 2);
    let max_chars = (message_width / 7).max(12) as usize;
    AlertLayout {
        panel,
        message_lines: wrap_text(message, max_chars),
    }
}

pub(crate) fn button_frames<const N: usize>(panel: Rectangle) -> [Rectangle; N] {
    let width = panel.size.width.saturating_sub(36);
    let button_width = width / N as u32;
    core::array::from_fn(|index| {
        let x = panel.top_left.x + 18 + (index as i32 * button_width as i32);
        let width = if index + 1 == N {
            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
                    - V_PADDING as i32
                    - BUTTON_HEIGHT as i32,
            ),
            Size::new(width, BUTTON_HEIGHT),
        )
    })
}

pub(crate) fn message_top(panel: Rectangle) -> i32 {
    panel.top_left.y + MESSAGE_TOP
}

fn wrap_text(text: &str, max_chars: usize) -> WrappedLines {
    let mut lines = WrappedLines::new();
    let mut current = String::<MAX_LINE_CHARS>::new();
    for segment in text.split('\n') {
        push_segment(&mut lines, &mut current, segment, max_chars);
        if !current.is_empty() {
            let _ = lines.push(current.clone());
            current.clear();
        }
    }
    if lines.is_empty() {
        let mut line = String::<MAX_LINE_CHARS>::new();
        let _ = line.push_str(text);
        let _ = lines.push(line);
    }
    lines
}

fn push_segment(
    lines: &mut WrappedLines,
    current: &mut String<MAX_LINE_CHARS>,
    segment: &str,
    max_chars: usize,
) {
    for word in segment.split_whitespace() {
        if current.is_empty() {
            push_word(lines, current, word, max_chars);
            continue;
        }
        if current.len() + 1 + word.len() <= max_chars {
            let _ = current.push(' ');
            let _ = current.push_str(word);
            continue;
        }
        let _ = lines.push(current.clone());
        current.clear();
        push_word(lines, current, word, max_chars);
    }
}

fn push_word(
    lines: &mut WrappedLines,
    current: &mut String<MAX_LINE_CHARS>,
    word: &str,
    max_chars: usize,
) {
    if word.len() <= max_chars {
        let _ = current.push_str(word);
        return;
    }

    for ch in word.chars() {
        if current.len() >= max_chars {
            let _ = lines.push(current.clone());
            current.clear();
        }
        let _ = current.push(ch);
    }
}