mirui 0.45.0

A lightweight, no_std ECS-driven UI framework for embedded, desktop, and WebAssembly
Documentation
use crate::render::font::Font;
use crate::render::glyph_run::PosedGlyphs;
use crate::render::path::Path;
use crate::render::raster::FillRule;
use crate::render::texture::Texture;
use crate::types::{Color, Fixed, Opa, Point, Rect, Transform};

pub use mirx::scene::{LineCap, LineJoin, Paint};

/// Straight-alpha blending. Opaque targets fold each mode into the destination
/// with the source alpha; transparent targets also account for destination
/// alpha. A zero-alpha source leaves the destination unchanged.
///
/// | mode | SwRenderer | wgpu | sdl_gpu | web_canvas |
/// |---|---|---|---|---|
/// | SourceOver / Add | full | full | full (native) | full |
/// | Screen / Multiply | full | full | full | full |
/// | Darken / Lighten / Difference | full | exact bounded fallback | exact bounded fallback | full |
///
/// Unsupported native image semantics are resolved by the backend before draw
/// submission. Backends with target readback use a clipped exact fallback when
/// caller-provided workspace is available and otherwise return a typed error.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CompositeMode {
    /// `out = src*src.a + dst*(1 - src.a)`. Default; matches v0.36.0.
    #[default]
    SourceOver,
    /// `out = saturate(src*src.a + dst)`. LED glow / fire / additive sprites.
    Add,
    /// `out = 1 - (1 - src*src.a)*(1 - dst)`. Soft glow / DropGlow halo.
    Screen,
    /// `out = (src*src.a)*dst/255 + dst*(1 - src.a)`. Tint / shading.
    Multiply,
    /// `out = min(src*src.a, dst) + dst*(1 - src.a)`. Photoshop Darken.
    Darken,
    /// `out = max(src*src.a, dst) + dst*(1 - src.a)`. Photoshop Lighten.
    Lighten,
    /// `out = |src*src.a - dst| + dst*(1 - src.a)`. Inversion / creative.
    Difference,
}

impl CompositeMode {
    /// Per-channel formula at `src.a == 255`. The caller folds `src.a`
    /// back via the standard non-premul `out = m * src.a + dst *
    /// (255 - src.a)` weight, so `src.a == 0` always preserves `dst`
    /// and `src.a == 255` yields exactly the value returned here.
    ///
    /// Internal arithmetic is u32 to keep the 255 × 255 path from
    /// overflowing; division by 255 uses the `(x + 127) / 255`
    /// round-to-nearest approximation, exact within ±1 over u8.
    #[inline]
    pub fn blend_channel(self, src: u8, dst: u8) -> u8 {
        let s = src as u32;
        let d = dst as u32;
        match self {
            Self::SourceOver => src,
            Self::Add => (s + d).min(255) as u8,
            Self::Screen => {
                let inv = (255 - s) * (255 - d);
                (255 - ((inv + 127) / 255)) as u8
            }
            Self::Multiply => ((s * d + 127) / 255) as u8,
            Self::Darken => src.min(dst),
            Self::Lighten => src.max(dst),
            Self::Difference => src.abs_diff(dst),
        }
    }
}

#[cfg(test)]
mod composite_mode_tests {
    use super::CompositeMode::*;

    #[test]
    fn source_over_returns_src() {
        assert_eq!(SourceOver.blend_channel(0, 0), 0);
        assert_eq!(SourceOver.blend_channel(128, 200), 128);
        assert_eq!(SourceOver.blend_channel(255, 0), 255);
    }

    #[test]
    fn add_saturates_at_255() {
        assert_eq!(Add.blend_channel(128, 128), 255);
        assert_eq!(Add.blend_channel(255, 255), 255);
        assert_eq!(Add.blend_channel(0, 0), 0);
        assert_eq!(Add.blend_channel(64, 64), 128);
    }

    #[test]
    fn screen_is_inverse_multiply_of_inverses() {
        assert!((190..=192).contains(&Screen.blend_channel(128, 128)));
        assert_eq!(Screen.blend_channel(255, 0), 255);
        assert_eq!(Screen.blend_channel(0, 0), 0);
        assert_eq!(Screen.blend_channel(0, 200), 200);
    }

    #[test]
    fn multiply_halves_at_50_percent() {
        assert!((63..=65).contains(&Multiply.blend_channel(128, 128)));
        assert_eq!(Multiply.blend_channel(0, 200), 0);
        assert_eq!(Multiply.blend_channel(255, 200), 200);
    }

    #[test]
    fn darken_keeps_smaller() {
        assert_eq!(Darken.blend_channel(64, 192), 64);
        assert_eq!(Darken.blend_channel(200, 100), 100);
        assert_eq!(Darken.blend_channel(128, 128), 128);
    }

    #[test]
    fn lighten_keeps_larger() {
        assert_eq!(Lighten.blend_channel(64, 192), 192);
        assert_eq!(Lighten.blend_channel(200, 100), 200);
    }

    #[test]
    fn difference_is_absolute_diff() {
        assert_eq!(Difference.blend_channel(192, 64), 128);
        assert_eq!(Difference.blend_channel(64, 192), 128);
        assert_eq!(Difference.blend_channel(100, 100), 0);
    }
}

/// Draw operation submitted to a renderer through a checked draw request.
///
/// All coordinate fields (`area`, `pos`, path points, `radius`, `width`) are
/// in **logical pixels**. Each drawable variant carries its local [`Transform`].
/// An optional `quad` is explicit leaf geometry, not inherited projective
/// state. Widget and scene homographies are supplied through
/// [`crate::render::renderer::Renderer::submit`].
pub enum DrawCommand<'a> {
    Fill {
        area: Rect,
        transform: Transform,
        quad: Option<[Point; 4]>,
        color: Color,
        radius: Fixed,
        opa: Opa,
    },
    Border {
        area: Rect,
        transform: Transform,
        quad: Option<[Point; 4]>,
        color: Color,
        width: Fixed,
        radius: Fixed,
        opa: Opa,
    },
    GlyphRun {
        pos: Point,
        transform: Transform,
        glyphs: &'a [textflow::shaping::PositionedGlyph],
        font: &'a Font,
        color: Color,
        opa: Opa,
    },
    PosedGlyphRun {
        pos: Point,
        transform: Transform,
        glyphs: PosedGlyphs<'a>,
        font: &'a Font,
        color: Color,
        opa: Opa,
    },
    Line {
        p1: Point,
        p2: Point,
        transform: Transform,
        color: Color,
        width: Fixed,
        opa: Opa,
    },
    /// Stroked arc on a circle (center, radius). Angles in degrees, CCW.
    Arc {
        center: Point,
        transform: Transform,
        radius: Fixed,
        start_angle: Fixed,
        end_angle: Fixed,
        color: Color,
        width: Fixed,
        opa: Opa,
    },
    /// Blit `texture` at `pos`, scaling (nearest) to `size` logical pixels.
    /// `radius > 0` clips to a rounded rectangle via SDF coverage. `composite`
    /// selects the blend formula — see [`CompositeMode`] for per-mode backend
    /// support.
    Blit {
        pos: Point,
        size: Point,
        transform: Transform,
        quad: Option<[Point; 4]>,
        texture: &'a Texture<'a>,
        opa: Opa,
        radius: Fixed,
        composite: CompositeMode,
    },
    /// Fill the closed region described by `path`. Path vertices are in
    /// logical pixels. Unsupported native paint combinations may use a bounded
    /// exact fallback and otherwise return a typed route error.
    FillPath {
        path: &'a Path,
        transform: Transform,
        paint: &'a Paint,
        opa: Opa,
        fill_rule: FillRule,
    },
    /// Stroke `path` with `width` logical pixels. Cap/join follow SVG
    /// semantics; `miter_limit` defaults to 4.0 when omitted by the
    /// caller. Unsupported paint combinations return a typed route error.
    StrokePath {
        path: &'a Path,
        transform: Transform,
        paint: &'a Paint,
        width: Fixed,
        opa: Opa,
        line_cap: LineCap,
        line_join: LineJoin,
        miter_limit: Fixed,
        dash: &'a [Fixed],
    },
    PushClip {
        path: &'a Path,
        transform: Transform,
        fill_rule: FillRule,
    },
    PopClip,
    ApplyBlur {
        alpha: Fixed,
        region: Rect,
    },
}

impl DrawCommand<'_> {
    #[inline]
    pub fn transform(&self) -> Transform {
        match *self {
            Self::Fill { transform, .. }
            | Self::Border { transform, .. }
            | Self::GlyphRun { transform, .. }
            | Self::PosedGlyphRun { transform, .. }
            | Self::Line { transform, .. }
            | Self::Arc { transform, .. }
            | Self::Blit { transform, .. }
            | Self::FillPath { transform, .. }
            | Self::StrokePath { transform, .. } => transform,
            Self::PushClip { .. } | Self::PopClip | Self::ApplyBlur { .. } => Transform::IDENTITY,
        }
    }
}