faststep 0.1.0

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

use crate::EdgeInsets;

/// Alignment used when placing an image inside its frame.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageAlignment {
    /// Align to the top-left corner.
    TopLeading,
    /// Align to the top edge, centered horizontally.
    Top,
    /// Align to the top-right corner.
    TopTrailing,
    /// Align to the leading edge, centered vertically.
    Leading,
    /// Center the image.
    Center,
    /// Align to the trailing edge, centered vertically.
    Trailing,
    /// Align to the bottom-left corner.
    BottomLeading,
    /// Align to the bottom edge, centered horizontally.
    Bottom,
    /// Align to the bottom-right corner.
    BottomTrailing,
}

/// Visual configuration for an [`ImageView`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageViewStyle<C> {
    /// Image alignment inside the content rect.
    pub alignment: ImageAlignment,
    /// Inner padding applied before image placement.
    pub insets: EdgeInsets,
    /// Optional background fill for the shell.
    pub background: Option<C>,
    /// Optional border color for the shell.
    pub border: Option<C>,
    /// Border width in pixels.
    pub border_width: u32,
    /// Corner radius for the shell.
    pub corner_radius: u32,
}

impl<C> ImageViewStyle<C> {
    /// Creates the default image view style.
    pub const fn new() -> Self {
        Self {
            alignment: ImageAlignment::Center,
            insets: EdgeInsets::all(0),
            background: None,
            border: None,
            border_width: 0,
            corner_radius: 0,
        }
    }

    /// Sets image alignment.
    pub fn with_alignment(mut self, alignment: ImageAlignment) -> Self {
        self.alignment = alignment;
        self
    }

    /// Sets content insets.
    pub fn with_insets(mut self, insets: EdgeInsets) -> Self {
        self.insets = insets;
        self
    }

    /// Sets the shell background color.
    pub fn with_background(mut self, background: C) -> Self {
        self.background = Some(background);
        self
    }

    /// Sets border color and width.
    pub fn with_border(mut self, border: C, border_width: u32) -> Self {
        self.border = Some(border);
        self.border_width = border_width;
        self
    }

    /// Sets the shell corner radius.
    pub fn with_corner_radius(mut self, corner_radius: u32) -> Self {
        self.corner_radius = corner_radius;
        self
    }
}

impl<C> Default for ImageViewStyle<C> {
    fn default() -> Self {
        Self::new()
    }
}

/// Reusable bitmap view similar to UIKit's `UIImageView`.
pub struct ImageView<'a, T, C>
where
    T: ImageDrawable<Color = C> + OriginDimensions,
    C: PixelColor,
{
    /// Outer frame for the view.
    pub frame: Rectangle,
    /// Referenced image content.
    pub image: &'a T,
    /// Shell and alignment style.
    pub style: ImageViewStyle<C>,
}

impl<'a, T, C> ImageView<'a, T, C>
where
    T: ImageDrawable<Color = C> + OriginDimensions,
    C: PixelColor,
{
    /// Creates a new image view with default styling.
    pub const fn new(frame: Rectangle, image: &'a T) -> Self {
        Self {
            frame,
            image,
            style: ImageViewStyle::new(),
        }
    }

    /// Replaces the view style.
    pub fn with_style(mut self, style: ImageViewStyle<C>) -> Self {
        self.style = style;
        self
    }

    /// Draws the image view.
    pub fn draw<D>(&self, display: &mut D)
    where
        D: DrawTarget<Color = C>,
    {
        draw_shell(
            display,
            self.frame,
            self.style.background,
            self.style.border,
            self.style.border_width,
            self.style.corner_radius,
        );
        let content = self.style.insets.inset_rect(self.frame);
        if content.size.width == 0 || content.size.height == 0 {
            return;
        }

        let image_size = self.image.size();
        let origin = aligned_origin(content, image_size, self.style.alignment);
        let mut clipped = display.clipped(&content);
        Image::new(self.image, origin).draw(&mut clipped).ok();
    }
}

fn draw_shell<D, C>(
    display: &mut D,
    frame: Rectangle,
    background: Option<C>,
    border: Option<C>,
    border_width: u32,
    corner_radius: u32,
) where
    D: DrawTarget<Color = C>,
    C: PixelColor,
{
    if background.is_none() && border.is_none() {
        return;
    }

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

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

fn aligned_origin(frame: Rectangle, image_size: Size, alignment: ImageAlignment) -> Point {
    let dx = frame.size.width as i32 - image_size.width as i32;
    let dy = frame.size.height as i32 - image_size.height as i32;
    let offset = match alignment {
        ImageAlignment::TopLeading => Point::new(0, 0),
        ImageAlignment::Top => Point::new(dx / 2, 0),
        ImageAlignment::TopTrailing => Point::new(dx, 0),
        ImageAlignment::Leading => Point::new(0, dy / 2),
        ImageAlignment::Center => Point::new(dx / 2, dy / 2),
        ImageAlignment::Trailing => Point::new(dx, dy / 2),
        ImageAlignment::BottomLeading => Point::new(0, dy),
        ImageAlignment::Bottom => Point::new(dx / 2, dy),
        ImageAlignment::BottomTrailing => Point::new(dx, dy),
    };
    frame.top_left + offset
}