embedded-gui 0.1.0

no_std GUI and HUD primitives for embedded-graphics displays
Documentation
use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};

use crate::render::{
    CHAR_HEIGHT, CHAR_WIDTH, TextAlign, TextMetrics, TextStyle, TextWrap, VerticalAlign,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Span<'a> {
    pub content: &'a str,
    pub style: TextStyle,
}

impl<'a> Span<'a> {
    pub const fn raw(content: &'a str) -> Self {
        Self {
            content,
            style: TextStyle::new(Rgb565::WHITE),
        }
    }

    pub const fn styled(content: &'a str, style: TextStyle) -> Self {
        Self { content, style }
    }

    pub fn width_chars(&self) -> usize {
        self.content.chars().filter(|&ch| ch != '\n').count()
    }

    pub fn metrics(&self) -> TextMetrics {
        TextMetrics {
            width: self.width_chars() as u32 * self.style.font.advance(),
            height: self.style.font.line_height(),
        }
    }
}

impl<'a> From<&'a str> for Span<'a> {
    fn from(value: &'a str) -> Self {
        Self::raw(value)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Line<'a> {
    pub spans: &'a [Span<'a>],
    pub align: TextAlign,
}

impl<'a> Line<'a> {
    pub const fn from_spans(spans: &'a [Span<'a>]) -> Self {
        Self {
            spans,
            align: TextAlign::Left,
        }
    }

    pub const fn aligned(mut self, align: TextAlign) -> Self {
        self.align = align;
        self
    }

    pub fn width_chars(&self) -> usize {
        self.widest_line_chars(u32::MAX, TextWrap::None)
    }

    pub fn char_count(&self) -> usize {
        self.spans
            .iter()
            .map(|span| span.content.chars().count())
            .sum()
    }

    pub fn metrics(&self) -> TextMetrics {
        let mut width = 0u32;
        let mut height = CHAR_HEIGHT;
        for span in self.spans {
            width = width.saturating_add(
                span.content.chars().filter(|&ch| ch != '\n').count() as u32
                    * span.style.font.advance(),
            );
            height = height.max(span.style.font.line_height());
        }
        TextMetrics { width, height }
    }

    pub fn visual_line_count(&self, max_width: u32, wrap: TextWrap) -> usize {
        let char_count = self.char_count();
        if char_count == 0 {
            return 1;
        }

        let mut lines = 0;
        let mut start = 0;
        while start < char_count {
            let (len, consumed_newline) = self.segment_len_at(start, max_width, wrap);
            lines += 1;
            start += len + usize::from(consumed_newline);
            if len == 0 && !consumed_newline {
                break;
            }
        }
        lines.max(1)
    }

    pub fn widest_line_chars(&self, max_width: u32, wrap: TextWrap) -> usize {
        let char_count = self.char_count();
        let mut widest = 0;
        let mut start = 0;
        while start < char_count {
            let (len, consumed_newline) = self.segment_len_at(start, max_width, wrap);
            widest = widest.max(len);
            start += len + usize::from(consumed_newline);
            if len == 0 && !consumed_newline {
                break;
            }
        }
        widest
    }

    pub fn widest_line_width(&self, max_width: u32, wrap: TextWrap) -> u32 {
        let char_count = self.char_count();
        let mut widest = 0u32;
        let mut start = 0;
        while start < char_count {
            let (len, consumed_newline) = self.segment_len_at(start, max_width, wrap);
            widest = widest.max(self.segment_width(start, len));
            start += len + usize::from(consumed_newline);
            if len == 0 && !consumed_newline {
                break;
            }
        }
        widest
    }

    pub fn max_line_height(&self) -> u32 {
        self.spans
            .iter()
            .map(|span| span.style.font.line_height())
            .max()
            .unwrap_or(CHAR_HEIGHT)
    }

    pub(crate) fn segment_len_at(
        &self,
        start: usize,
        max_width: u32,
        wrap: TextWrap,
    ) -> (usize, bool) {
        let mut len = 0;
        let mut width = 0u32;
        let min_advance = self
            .spans
            .iter()
            .map(|span| span.style.font.advance())
            .min()
            .unwrap_or(CHAR_WIDTH)
            .max(1);
        let limit_width = max_width.max(min_advance);
        let mut last_ws_break = None;

        for (ch, style) in self
            .spans
            .iter()
            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style)))
            .skip(start)
        {
            if ch == '\n' {
                return (len, true);
            }
            if matches!(wrap, TextWrap::Character | TextWrap::Word) {
                let advance = style.font.advance();
                if len > 0 && width.saturating_add(advance) > limit_width {
                    if matches!(wrap, TextWrap::Word) {
                        if let Some(idx) = last_ws_break {
                            return (idx, false);
                        }
                    }
                    return (len, false);
                }
                width = width.saturating_add(advance);
            }
            if matches!(wrap, TextWrap::Word) && ch.is_whitespace() {
                last_ws_break = Some(len + 1);
            }
            len += 1;
        }

        (len, false)
    }

    pub(crate) fn segment_width(&self, start: usize, len: usize) -> u32 {
        self.spans
            .iter()
            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style)))
            .enumerate()
            .filter_map(|(idx, (ch, style))| {
                if idx < start || idx >= start + len || ch == '\n' {
                    None
                } else {
                    Some(style.font.advance())
                }
            })
            .sum()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Text<'a> {
    pub lines: &'a [Line<'a>],
    pub align: TextAlign,
    pub vertical_align: VerticalAlign,
    pub wrap: TextWrap,
    pub line_spacing: u8,
}

impl<'a> Text<'a> {
    pub const fn from_lines(lines: &'a [Line<'a>]) -> Self {
        Self {
            lines,
            align: TextAlign::Left,
            vertical_align: VerticalAlign::Top,
            wrap: TextWrap::None,
            line_spacing: 1,
        }
    }

    pub const fn aligned(mut self, align: TextAlign) -> Self {
        self.align = align;
        self
    }

    pub const fn vertical_aligned(mut self, align: VerticalAlign) -> Self {
        self.vertical_align = align;
        self
    }

    pub const fn wrapped(mut self, wrap: TextWrap) -> Self {
        self.wrap = wrap;
        self
    }

    pub const fn line_spacing(mut self, spacing: u8) -> Self {
        self.line_spacing = spacing;
        self
    }

    pub fn metrics(&self, max_width: u32) -> TextMetrics {
        let mut lines = 0usize;
        let mut widest = 0u32;
        let mut max_line_height = CHAR_HEIGHT;

        for line in self.lines {
            lines += line.visual_line_count(max_width, self.wrap);
            widest = widest.max(line.widest_line_width(max_width, self.wrap));
            max_line_height = max_line_height.max(line.max_line_height());
        }

        let lines = lines.max(1);
        TextMetrics {
            width: widest.min(max_width),
            height: lines as u32 * max_line_height
                + lines.saturating_sub(1) as u32 * self.line_spacing as u32,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextDirection {
    Ltr,
    Rtl,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ShapingConfig {
    pub direction: TextDirection,
    pub language_tag: Option<&'static str>,
    pub enable_ligatures: bool,
}

impl Default for ShapingConfig {
    fn default() -> Self {
        Self {
            direction: TextDirection::Ltr,
            language_tag: None,
            enable_ligatures: true,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ShapedGlyph {
    pub ch: char,
    pub x_advance: i16,
}

pub trait TextShaper {
    fn shape<const N: usize>(
        &self,
        text: &str,
        config: ShapingConfig,
        out: &mut heapless::Vec<ShapedGlyph, N>,
    );
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BasicTextShaper;

impl TextShaper for BasicTextShaper {
    fn shape<const N: usize>(
        &self,
        text: &str,
        config: ShapingConfig,
        out: &mut heapless::Vec<ShapedGlyph, N>,
    ) {
        out.clear();
        let iter = text.chars();
        if matches!(config.direction, TextDirection::Rtl) {
            for ch in iter.rev() {
                let _ = out.push(ShapedGlyph { ch, x_advance: 1 });
            }
        } else {
            for ch in iter {
                let _ = out.push(ShapedGlyph { ch, x_advance: 1 });
            }
        }
    }
}