embedded-gui 0.1.0

no_std GUI and HUD primitives for embedded-graphics displays
Documentation
use crate::geometry::Rect;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageFit {
    Stretch,
    Center,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageRef<'a> {
    pub width: u32,
    pub height: u32,
    pub pixels: &'a [u16],
}

impl<'a> ImageRef<'a> {
    pub const fn new(width: u32, height: u32, pixels: &'a [u16]) -> Self {
        Self {
            width,
            height,
            pixels,
        }
    }

    pub fn bounds_at(&self, rect: Rect, fit: ImageFit) -> Rect {
        match fit {
            ImageFit::Stretch => rect,
            ImageFit::Center => {
                let x = rect.x + rect.w.saturating_sub(self.width) as i32 / 2;
                let y = rect.y + rect.h.saturating_sub(self.height) as i32 / 2;
                Rect::new(x, y, self.width.min(rect.w), self.height.min(rect.h))
            }
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpriteSheet<'a> {
    pub image: ImageRef<'a>,
    pub sprite_w: u32,
    pub sprite_h: u32,
    pub columns: u32,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReelFrame {
    pub sprite_index: u16,
    pub duration_ms: u16,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReelPlayer<'a> {
    pub sheet: SpriteSheet<'a>,
    pub frames: &'a [ReelFrame],
    pub repeat: bool,
    current: usize,
    elapsed_in_frame_ms: u32,
    finished: bool,
}

impl<'a> ReelPlayer<'a> {
    pub const fn new(sheet: SpriteSheet<'a>, frames: &'a [ReelFrame], repeat: bool) -> Self {
        Self {
            sheet,
            frames,
            repeat,
            current: 0,
            elapsed_in_frame_ms: 0,
            finished: false,
        }
    }

    pub fn tick(&mut self, dt_ms: u32) {
        if self.frames.is_empty() || self.finished {
            return;
        }
        self.elapsed_in_frame_ms = self.elapsed_in_frame_ms.saturating_add(dt_ms);
        loop {
            let frame = self.frames[self.current];
            let frame_ms = u32::from(frame.duration_ms).max(1);
            if self.elapsed_in_frame_ms < frame_ms {
                break;
            }
            self.elapsed_in_frame_ms -= frame_ms;
            if self.current + 1 < self.frames.len() {
                self.current += 1;
                continue;
            }
            if self.repeat {
                self.current = 0;
            } else {
                self.finished = true;
            }
            break;
        }
    }

    pub const fn is_finished(&self) -> bool {
        self.finished
    }

    pub fn restart(&mut self) {
        self.current = 0;
        self.elapsed_in_frame_ms = 0;
        self.finished = false;
    }

    pub fn current_sprite_rect(&self) -> Option<Rect> {
        let frame = self.frames.get(self.current)?;
        Some(self.sheet.sprite_rect(frame.sprite_index as u32))
    }
}

impl<'a> SpriteSheet<'a> {
    pub const fn new(image: ImageRef<'a>, sprite_w: u32, sprite_h: u32) -> Self {
        let columns = match image.width.checked_div(sprite_w) {
            Some(c) => c,
            None => 1,
        };
        Self {
            image,
            sprite_w,
            sprite_h,
            columns,
        }
    }

    pub fn sprite_rect(&self, index: u32) -> Rect {
        let columns = self.columns.max(1);
        let col = index % columns;
        let row = index / columns;
        Rect::new(
            (col * self.sprite_w) as i32,
            (row * self.sprite_h) as i32,
            self.sprite_w,
            self.sprite_h,
        )
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageAtlasEntry {
    pub id: u16,
    pub rect: Rect,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageAtlas<'a> {
    pub image: ImageRef<'a>,
    pub entries: &'a [ImageAtlasEntry],
}

impl<'a> ImageAtlas<'a> {
    pub const fn new(image: ImageRef<'a>, entries: &'a [ImageAtlasEntry]) -> Self {
        Self { image, entries }
    }

    pub fn rect_for(&self, id: u16) -> Option<Rect> {
        self.entries
            .iter()
            .find(|entry| entry.id == id)
            .map(|e| e.rect)
    }
}

#[cfg(all(feature = "std", feature = "image-decode"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImageDecodeError {
    InvalidHeader,
    Unsupported,
    InvalidData,
    Capacity,
}

#[cfg(all(feature = "std", feature = "image-decode"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EncodedImageFormat {
    PpmAscii,
}

#[cfg(all(feature = "std", feature = "image-decode"))]
pub trait ImageDecoder {
    fn decode<const N: usize>(
        &self,
        format: EncodedImageFormat,
        data: &str,
        out_pixels: &mut heapless::Vec<u16, N>,
    ) -> Result<(u32, u32), ImageDecodeError>;
}

#[cfg(all(feature = "std", feature = "image-decode"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BasicImageDecoder;

#[cfg(all(feature = "std", feature = "image-decode"))]
impl ImageDecoder for BasicImageDecoder {
    fn decode<const N: usize>(
        &self,
        format: EncodedImageFormat,
        data: &str,
        out_pixels: &mut heapless::Vec<u16, N>,
    ) -> Result<(u32, u32), ImageDecodeError> {
        match format {
            EncodedImageFormat::PpmAscii => decode_ppm_ascii(data, out_pixels),
        }
    }
}

#[cfg(all(feature = "std", feature = "image-decode"))]
pub fn decode_image_with<const N: usize>(
    decoder: &impl ImageDecoder,
    format: EncodedImageFormat,
    data: &str,
    out_pixels: &mut heapless::Vec<u16, N>,
) -> Result<(u32, u32), ImageDecodeError> {
    decoder.decode(format, data, out_pixels)
}

#[cfg(all(feature = "std", feature = "image-decode"))]
pub fn decode_image_auto<const N: usize>(
    data: &str,
    out_pixels: &mut heapless::Vec<u16, N>,
) -> Result<(u32, u32), ImageDecodeError> {
    let format = if data.trim_start().starts_with("P3") {
        EncodedImageFormat::PpmAscii
    } else {
        return Err(ImageDecodeError::Unsupported);
    };
    decode_image_with(&BasicImageDecoder, format, data, out_pixels)
}

#[cfg(all(feature = "std", feature = "image-decode"))]
pub fn decode_ppm_ascii<const N: usize>(
    data: &str,
    out_pixels: &mut heapless::Vec<u16, N>,
) -> Result<(u32, u32), ImageDecodeError> {
    let mut parts = data.split_whitespace();
    if parts.next() != Some("P3") {
        return Err(ImageDecodeError::InvalidHeader);
    }
    let width: u32 = parts
        .next()
        .ok_or(ImageDecodeError::InvalidHeader)?
        .parse()
        .map_err(|_| ImageDecodeError::InvalidHeader)?;
    let height: u32 = parts
        .next()
        .ok_or(ImageDecodeError::InvalidHeader)?
        .parse()
        .map_err(|_| ImageDecodeError::InvalidHeader)?;
    let maxv: u32 = parts
        .next()
        .ok_or(ImageDecodeError::InvalidHeader)?
        .parse()
        .map_err(|_| ImageDecodeError::InvalidHeader)?;
    if maxv == 0 {
        return Err(ImageDecodeError::InvalidData);
    }
    out_pixels.clear();
    let count = width.saturating_mul(height);
    for _ in 0..count {
        let r: u32 = parts
            .next()
            .ok_or(ImageDecodeError::InvalidData)?
            .parse()
            .map_err(|_| ImageDecodeError::InvalidData)?;
        let g: u32 = parts
            .next()
            .ok_or(ImageDecodeError::InvalidData)?
            .parse()
            .map_err(|_| ImageDecodeError::InvalidData)?;
        let b: u32 = parts
            .next()
            .ok_or(ImageDecodeError::InvalidData)?
            .parse()
            .map_err(|_| ImageDecodeError::InvalidData)?;
        let r5 = ((r.saturating_mul(31)) / maxv) as u16;
        let g6 = ((g.saturating_mul(63)) / maxv) as u16;
        let b5 = ((b.saturating_mul(31)) / maxv) as u16;
        out_pixels
            .push((r5 << 11) | (g6 << 5) | b5)
            .map_err(|_| ImageDecodeError::Capacity)?;
    }
    Ok((width, height))
}