faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{
    Drawable,
    draw_target::{DrawTarget, DrawTargetExt},
    pixelcolor::Rgb565,
    prelude::{Point, Primitive, Size},
    primitives::{PrimitiveStyleBuilder, Rectangle, RoundedRectangle},
};

use super::layout::{draw_line, layout_lines};
use super::{TextAlignment, TextSpan, TextVerticalAlignment, TextViewStyle};

/// Simple text view that renders one string with one style.
pub struct TextView<'a> {
    /// Outer frame for the text view.
    pub frame: Rectangle,
    /// Plain text content.
    pub text: &'a str,
    /// Style for the whole text string.
    pub style: super::TextRunStyle,
    /// Container-level layout and shell styling.
    pub view_style: TextViewStyle,
}

impl<'a> TextView<'a> {
    /// Creates a plain text view.
    pub const fn new(frame: Rectangle, text: &'a str, style: super::TextRunStyle) -> Self {
        Self {
            frame,
            text,
            style,
            view_style: TextViewStyle::new(),
        }
    }

    /// Replaces the container style.
    pub fn with_view_style(mut self, view_style: TextViewStyle) -> Self {
        self.view_style = view_style;
        self
    }

    /// Draws the text view.
    pub fn draw<D>(&self, display: &mut D)
    where
        D: DrawTarget<Color = Rgb565>,
    {
        let spans = [TextSpan::new(self.text, self.style)];
        draw_text_view(display, self.frame, &spans, &self.view_style);
    }
}

/// Rich text view built from multiple styled spans.
pub struct RichTextView<'a> {
    /// Outer frame for the text view.
    pub frame: Rectangle,
    /// Styled spans rendered in order.
    pub spans: &'a [TextSpan<'a>],
    /// Container-level layout and shell styling.
    pub view_style: TextViewStyle,
}

impl<'a> RichTextView<'a> {
    /// Creates a rich text view.
    pub const fn new(frame: Rectangle, spans: &'a [TextSpan<'a>]) -> Self {
        Self {
            frame,
            spans,
            view_style: TextViewStyle::new(),
        }
    }

    /// Replaces the container style.
    pub fn with_view_style(mut self, view_style: TextViewStyle) -> Self {
        self.view_style = view_style;
        self
    }

    /// Draws the rich text view.
    pub fn draw<D>(&self, display: &mut D)
    where
        D: DrawTarget<Color = Rgb565>,
    {
        draw_text_view(display, self.frame, self.spans, &self.view_style);
    }
}

/// Shared renderer used by [`TextView`] and [`RichTextView`].
pub fn draw_text_view<D>(
    display: &mut D,
    frame: Rectangle,
    spans: &[TextSpan<'_>],
    view_style: &TextViewStyle,
) where
    D: DrawTarget<Color = Rgb565>,
{
    draw_shell(display, frame, view_style);
    let content = view_style.insets.inset_rect(frame);
    if content.size.width == 0 || content.size.height == 0 || spans.is_empty() {
        return;
    }

    let lines = layout_lines(spans, view_style, content.size.width as i32);
    if lines.is_empty() {
        return;
    }

    let spacing = view_style.line_spacing as i32;
    let total_height = lines.iter().map(|line| line.height).sum::<i32>()
        + spacing * (lines.len().saturating_sub(1) as i32);
    let available_height = content.size.height as i32;
    let offset_y = match view_style.vertical_alignment {
        TextVerticalAlignment::Top => 0,
        TextVerticalAlignment::Center => ((available_height - total_height).max(0)) / 2,
        TextVerticalAlignment::Bottom => (available_height - total_height).max(0),
    };

    let mut clipped = display.clipped(&content);
    let mut cursor_y = content.top_left.y + offset_y;
    for line in lines {
        let offset_x = match view_style.alignment {
            TextAlignment::Leading => 0,
            TextAlignment::Center => ((content.size.width as i32 - line.width).max(0)) / 2,
            TextAlignment::Trailing => (content.size.width as i32 - line.width).max(0),
        };
        draw_line(
            &mut clipped,
            spans,
            line,
            Point::new(content.top_left.x + offset_x, cursor_y),
        );
        cursor_y += line.height + spacing;
    }
}

fn draw_shell<D>(display: &mut D, frame: Rectangle, style: &TextViewStyle)
where
    D: DrawTarget<Color = Rgb565>,
{
    if style.background.is_none() && style.border.is_none() {
        return;
    }

    let mut primitive = PrimitiveStyleBuilder::new();
    if let Some(background) = style.background {
        primitive = primitive.fill_color(background);
    }
    if let Some(border) = style.border {
        primitive = primitive
            .stroke_color(border)
            .stroke_width(style.border_width.max(1));
    }

    RoundedRectangle::with_equal_corners(
        frame,
        Size::new(style.corner_radius, style.corner_radius),
    )
    .into_styled(primitive.build())
    .draw(display)
    .ok();
}