faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{
    Drawable,
    draw_target::DrawTarget,
    mono_font::MonoTextStyleBuilder,
    pixelcolor::Rgb565,
    prelude::Point,
    text::{Baseline, Text, TextStyleBuilder},
};
use heapless::Vec;

use super::{TextRunStyle, TextSpan, TextViewStyle, TextWrap};

pub(super) const MAX_LAYOUT_LINES: usize = 32;

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Cursor {
    span: usize,
    byte: usize,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FragmentKind {
    Word,
    Space,
    Newline,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Fragment {
    kind: FragmentKind,
    start: Cursor,
    end: Cursor,
    width: i32,
    height: i32,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct LineLayout {
    start: Cursor,
    end: Cursor,
    pub(super) width: i32,
    pub(super) height: i32,
}

pub(super) fn layout_lines(
    spans: &[TextSpan<'_>],
    view_style: &TextViewStyle,
    max_width: i32,
) -> Vec<LineLayout, MAX_LAYOUT_LINES> {
    let mut lines = Vec::new();
    let mut cursor = Cursor::default();
    let mut line_start = cursor;
    let mut line_end = cursor;
    let mut line_width = 0;
    let mut line_height = 0;
    let mut saw_content = false;
    let mut pending_space = None;

    while let Some(fragment) = next_fragment(spans, cursor) {
        cursor = fragment.end;
        match fragment.kind {
            FragmentKind::Newline => {
                push_line(
                    &mut lines,
                    line_start,
                    line_end,
                    line_width,
                    line_height.max(fragment.height),
                );
                line_start = cursor;
                line_end = cursor;
                line_width = 0;
                line_height = 0;
                saw_content = false;
                pending_space = None;
            }
            FragmentKind::Space => {
                if !saw_content {
                    line_start = fragment.end;
                } else if matches!(view_style.wrap, TextWrap::SingleLine) {
                    line_width += fragment.width;
                    line_height = line_height.max(fragment.height);
                    line_end = fragment.end;
                } else {
                    pending_space = Some(fragment);
                }
            }
            FragmentKind::Word => {
                let pending_width = pending_space.map(|space| space.width).unwrap_or(0);
                let next_width = line_width + pending_width + fragment.width;
                if matches!(view_style.wrap, TextWrap::Multiline)
                    && saw_content
                    && max_width > 0
                    && next_width > max_width
                {
                    push_line(&mut lines, line_start, line_end, line_width, line_height);
                    line_start = fragment.start;
                    line_end = fragment.end;
                    line_width = fragment.width;
                    line_height = fragment.height;
                    saw_content = true;
                    pending_space = None;
                    continue;
                }

                if let Some(space) = pending_space.take() {
                    line_width += space.width;
                    line_height = line_height.max(space.height);
                }
                if !saw_content {
                    line_start = fragment.start;
                }
                line_width += fragment.width;
                line_height = line_height.max(fragment.height);
                line_end = fragment.end;
                saw_content = true;

                if matches!(view_style.wrap, TextWrap::SingleLine)
                    && max_width > 0
                    && line_width >= max_width
                {
                    break;
                }
            }
        }
    }

    if saw_content || (lines.is_empty() && line_start != cursor) {
        push_line(
            &mut lines,
            line_start,
            line_end,
            line_width,
            line_height.max(1),
        );
    }
    lines
}

pub(super) fn draw_line<D>(display: &mut D, spans: &[TextSpan<'_>], line: LineLayout, origin: Point)
where
    D: DrawTarget<Color = Rgb565>,
{
    let text_style = TextStyleBuilder::new().baseline(Baseline::Top).build();
    let mut cursor = line.start;
    let mut pen = origin;

    while cursor != line.end {
        let span = &spans[cursor.span];
        let end = if cursor.span == line.end.span {
            line.end.byte
        } else {
            span.text.len()
        };
        if end > cursor.byte {
            let text = &span.text[cursor.byte..end];
            let style = MonoTextStyleBuilder::new()
                .font(span.style.font.mono())
                .text_color(span.style.color)
                .build();
            Text::with_text_style(text, pen, style, text_style)
                .draw(display)
                .ok();
            pen.x += text.chars().count() as i32 * span.style.font.advance();
        }
        if cursor.span == line.end.span {
            break;
        }
        cursor = Cursor {
            span: cursor.span + 1,
            byte: 0,
        };
    }
}

fn push_line(
    lines: &mut Vec<LineLayout, MAX_LAYOUT_LINES>,
    start: Cursor,
    end: Cursor,
    width: i32,
    height: i32,
) {
    let _ = lines.push(LineLayout {
        start,
        end,
        width: width.max(0),
        height: height.max(1),
    });
}

fn next_fragment(spans: &[TextSpan<'_>], start: Cursor) -> Option<Fragment> {
    let (first, style, mut end) = next_char(spans, start)?;
    if first == '\n' {
        return Some(Fragment {
            kind: FragmentKind::Newline,
            start,
            end,
            width: 0,
            height: style.font.line_height(),
        });
    }

    let whitespace = first.is_whitespace();
    let mut width = style.font.advance();
    let mut height = style.font.line_height();
    while let Some((ch, next_style, next)) = next_char(spans, end) {
        if ch == '\n' || ch.is_whitespace() != whitespace {
            break;
        }
        width += next_style.font.advance();
        height = height.max(next_style.font.line_height());
        end = next;
    }

    Some(Fragment {
        kind: if whitespace {
            FragmentKind::Space
        } else {
            FragmentKind::Word
        },
        start,
        end,
        width,
        height,
    })
}

fn next_char(spans: &[TextSpan<'_>], mut cursor: Cursor) -> Option<(char, TextRunStyle, Cursor)> {
    while cursor.span < spans.len() {
        let span = &spans[cursor.span];
        if cursor.byte >= span.text.len() {
            cursor.span += 1;
            cursor.byte = 0;
            continue;
        }
        let text = &span.text[cursor.byte..];
        let ch = text.chars().next()?;
        let next = Cursor {
            span: cursor.span,
            byte: cursor.byte + ch.len_utf8(),
        };
        return Some((ch, span.style, next));
    }
    None
}