embedded-gui 0.1.0

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

use crate::{
    geometry::{EdgeInsets, Rect},
    render::{RenderCtx, TextAlign, TextStyle},
    style::{Border, Style},
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Block<'a> {
    pub title: Option<&'a str>,
    pub title_align: TextAlign,
    pub border: Border,
    pub style: Style,
    pub padding: EdgeInsets,
}

impl<'a> Block<'a> {
    pub const fn new() -> Self {
        Self {
            title: None,
            title_align: TextAlign::Left,
            border: Border::none(),
            style: Style::new(),
            padding: EdgeInsets::all(0),
        }
    }

    pub const fn styled(style: Style) -> Self {
        Self {
            title: None,
            title_align: TextAlign::Left,
            border: style.border,
            padding: style.padding,
            style,
        }
    }

    pub const fn title(mut self, title: &'a str) -> Self {
        self.title = Some(title);
        self
    }

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

    pub const fn border(mut self, border: Border) -> Self {
        self.border = border;
        self
    }

    pub const fn padding(mut self, padding: EdgeInsets) -> Self {
        self.padding = padding;
        self
    }

    pub fn inner(self, rect: Rect) -> Rect {
        let border = self.border.width as i16;
        rect.inset(EdgeInsets {
            left: self.padding.left.saturating_add(border),
            right: self.padding.right.saturating_add(border),
            top: self.padding.top.saturating_add(border),
            bottom: self.padding.bottom.saturating_add(border),
        })
    }

    pub fn title_area(self, rect: Rect) -> Option<Rect> {
        self.title.map(|_| {
            Rect::new(
                rect.x + self.border.width as i32 + self.padding.left.max(0) as i32,
                rect.y,
                rect.w
                    .saturating_sub(self.border.width as u32 * 2)
                    .saturating_sub(self.padding.left.max(0) as u32)
                    .saturating_sub(self.padding.right.max(0) as u32),
                self.style.font.line_height() + 1,
            )
        })
    }

    pub fn content_area(self, rect: Rect) -> Rect {
        let inner = self.inner(rect);
        if self.title.is_none() {
            return inner;
        }

        let title_h = self.style.font.line_height() + 3;
        Rect::new(
            inner.x,
            inner.y + title_h as i32,
            inner.w,
            inner.h.saturating_sub(title_h),
        )
    }

    pub fn render<D>(self, rect: Rect, ctx: &mut RenderCtx<'_, D>) -> Result<(), D::Error>
    where
        D: DrawTarget<Color = Rgb565>,
    {
        if let Some(shadow) = self.style.shadow {
            let spread = ctx.shadow_spread_for(shadow.spread);
            let mut i = 0u8;
            while i < spread {
                let grow = i as i32;
                let shadow_rect = Rect::new(
                    rect.x + shadow.offset_x as i32 - grow,
                    rect.y + shadow.offset_y as i32 - grow,
                    rect.w.saturating_add((i as u32) * 2),
                    rect.h.saturating_add((i as u32) * 2),
                );
                let fade = spread as u16;
                let opacity = ((shadow.opacity as u16) * (fade - i as u16) / fade) as u8;
                let radius = self.style.corner_radius.saturating_add(i);
                ctx.fill_rounded_rect_alpha(shadow_rect, radius, shadow.color, opacity)?;
                i += 1;
            }
        }

        if let Some(gradient) = self.style.gradient {
            ctx.fill_rounded_rect_gradient_alpha(
                rect,
                self.style.corner_radius,
                gradient,
                self.style.opacity,
            )?;
        } else if let Some(bg) = self.style.background {
            ctx.fill_rounded_rect_alpha(rect, self.style.corner_radius, bg, self.style.opacity)?;
        }
        ctx.stroke_rounded_rect_alpha(
            rect,
            self.style.corner_radius,
            self.border,
            self.style.opacity,
        )?;

        if let Some(title) = self.title {
            let title_rect = self.title_area(rect).unwrap_or(Rect::empty());
            ctx.draw_text_in(
                title_rect,
                title,
                TextStyle::new(self.style.accent)
                    .with_font(self.style.font)
                    .with_align(self.title_align)
                    .with_opacity(self.style.opacity),
            )?;
        }

        Ok(())
    }
}

impl Default for Block<'_> {
    fn default() -> Self {
        Self::new()
    }
}