mirui 0.41.0

A lightweight, no_std ECS-driven UI framework for embedded, desktop, and WebAssembly
Documentation
use alloc::vec::Vec;

use crate::render::scene::SceneOp;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CodecError {
    UnexpectedEof,
    BadMagic,
    UnknownVersion(u8),
    UnknownTag(u8),
    CrcMismatch { expected: u32, actual: u32 },
    BadFillRule(u8),
    BadResourceKind(u8),
    BadUtf8,
    UnbalancedGroup,
    BadSkipOffset,
    UnsupportedScale(u8),
    UnknownFlags(u8),
    BadComposite(u8),
}

impl From<mirx::CodecError> for CodecError {
    fn from(e: mirx::CodecError) -> Self {
        match e {
            mirx::CodecError::UnexpectedEof => Self::UnexpectedEof,
            mirx::CodecError::BadMagic => Self::BadMagic,
            mirx::CodecError::UnknownVersion(v) => Self::UnknownVersion(v),
            mirx::CodecError::UnknownTag(t) => Self::UnknownTag(t),
            mirx::CodecError::CrcMismatch { expected, actual } => {
                Self::CrcMismatch { expected, actual }
            }
            mirx::CodecError::BadFillRule(b) => Self::BadFillRule(b),
            mirx::CodecError::BadResourceKind(b) => Self::BadResourceKind(b),
            mirx::CodecError::BadUtf8 => Self::BadUtf8,
            mirx::CodecError::UnbalancedGroup => Self::UnbalancedGroup,
            mirx::CodecError::BadSkipOffset => Self::BadSkipOffset,
            mirx::CodecError::UnsupportedScale(s) => Self::UnsupportedScale(s),
            mirx::CodecError::UnknownFlags(f) => Self::UnknownFlags(f),
            mirx::CodecError::BadComposite(b) => Self::BadComposite(b),
        }
    }
}

pub fn encode_scene(ops: &[SceneOp]) -> Result<Vec<u8>, CodecError> {
    let mirx_ops: alloc::vec::Vec<mirx::SceneOp> = ops.iter().cloned().map(Into::into).collect();
    mirx::Scene::from_ops(mirx_ops).encode().map_err(Into::into)
}

pub fn decode_scene(payload: &[u8]) -> Result<Vec<SceneOp>, CodecError> {
    let scene = mirx::Scene::decode(payload).map_err(CodecError::from)?;
    Ok(scene.ops.into_iter().map(Into::into).collect())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::render::command::CompositeMode;
    use crate::render::path::Path;
    use crate::render::raster::FillRule;
    use crate::render::scene::ResourceRef;
    use crate::types::{Color, Fixed, Point, Rect, Transform};
    use alloc::vec;

    fn red() -> Color {
        Color::rgb(255, 0, 0)
    }

    fn roundtrip(ops: vec::Vec<SceneOp>) {
        let bytes = encode_scene(&ops).unwrap();
        let back = decode_scene(&bytes).unwrap();
        assert_eq!(back, ops);
    }

    #[test]
    fn fill_rect_identity_roundtrips() {
        roundtrip(vec![SceneOp::FillRect {
            area: Rect::new(
                Fixed::from_int(1),
                Fixed::from_int(2),
                Fixed::from_int(3),
                Fixed::from_int(4),
            ),
            transform: Transform::IDENTITY,
            quad: None,
            color: red(),
            radius: Fixed::ZERO,
            opa: 200,
        }]);
    }

    #[test]
    fn golden_bytes_are_stable() {
        let ops = vec![
            SceneOp::FillRect {
                area: Rect::new(
                    Fixed::from_int(1),
                    Fixed::from_int(2),
                    Fixed::from_int(3),
                    Fixed::from_int(4),
                ),
                transform: Transform::IDENTITY,
                quad: None,
                color: Color::rgba(0x11, 0x22, 0x33, 0x44),
                radius: Fixed::ZERO,
                opa: 0xAB,
            },
            SceneOp::Line {
                p1: Point::ZERO,
                p2: Point::new(Fixed::from_int(5), Fixed::ZERO),
                transform: Transform::IDENTITY,
                color: Color::rgba(1, 2, 3, 4),
                width: Fixed::from_int(1),
                opa: 0xFF,
            },
        ];
        let golden: &[u8] = &[
            0x03, 0x01, 0x08, 0x00, 0x84, 0xcc, 0x5e, 0xd3, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00,
            0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x11, 0x22,
            0x33, 0x44, 0xab, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x02, 0x03,
            0x04, 0xff, 0x00,
        ];
        assert_eq!(encode_scene(&ops).unwrap(), golden);
        assert_eq!(decode_scene(golden).unwrap(), ops);
    }

    #[test]
    fn fill_path_roundtrips() {
        roundtrip(vec![SceneOp::FillPath {
            path: Path::from_owned(vec![
                crate::render::path::PathCmd::MoveTo(Point::ZERO),
                crate::render::path::PathCmd::LineTo(Point::new(Fixed::from_int(4), Fixed::ZERO)),
                crate::render::path::PathCmd::QuadTo {
                    ctrl: Point::new(Fixed::from_int(4), Fixed::from_int(4)),
                    end: Point::new(Fixed::ZERO, Fixed::from_int(4)),
                },
                crate::render::path::PathCmd::Close,
            ]),
            transform: Transform::IDENTITY,
            color: red(),
            opa: 255,
            fill_rule: FillRule::NonZero,
        }]);
    }

    #[test]
    fn blit_per_composite_mode_roundtrips() {
        for m in [
            CompositeMode::Add,
            CompositeMode::Screen,
            CompositeMode::Multiply,
            CompositeMode::Darken,
            CompositeMode::Lighten,
            CompositeMode::Difference,
        ] {
            roundtrip(vec![SceneOp::Blit {
                texture: ResourceRef::Index(3),
                pos: Point::ZERO,
                size: Point::new(Fixed::from_int(8), Fixed::from_int(8)),
                transform: Transform::IDENTITY,
                quad: None,
                opa: 255,
                radius: Fixed::ZERO,
                composite: m,
            }]);
        }
    }

    #[test]
    fn corrupt_crc_is_rejected() {
        let ops = vec![SceneOp::Line {
            p1: Point::ZERO,
            p2: Point::ZERO,
            transform: Transform::IDENTITY,
            color: red(),
            width: Fixed::from_int(1),
            opa: 255,
        }];
        let mut bytes = encode_scene(&ops).unwrap();
        let last = bytes.len() - 1;
        bytes[last] ^= 0xFF;
        assert!(matches!(
            decode_scene(&bytes),
            Err(CodecError::CrcMismatch { .. })
        ));
    }

    #[test]
    fn unbalanced_group_end_is_rejected_at_encode() {
        assert!(matches!(
            encode_scene(&[SceneOp::GroupEnd]),
            Err(CodecError::UnbalancedGroup)
        ));
    }
}