mirui 0.42.0

A lightweight, no_std ECS-driven UI framework for embedded, desktop, and WebAssembly
Documentation
use super::SwRenderer;
use crate::render::font::{Font, GlyphKind};
use crate::types::{Color, Fixed, Point, Rect};

impl SwRenderer<'_> {
    pub(super) fn draw_label_inner(
        &mut self,
        pos: &Point,
        text: &str,
        font: &Font,
        clip: &Rect,
        color: &Color,
        opa: u8,
    ) {
        let phys_pos = self.viewport.point_to_physical(*pos);
        let phys_clip = self.viewport.rect_to_physical(*clip);
        let scale = self.viewport.scale().to_int().max(1);
        let phys_bounds = phys_clip.pixel_bounds();
        let (mut cx, cy) = phys_pos.floor();
        let metrics = font.metrics();
        let char_h = metrics.line_height as i32;
        let requested_size = (font.size as i32 * scale).clamp(1, u16::MAX as i32) as u16;
        for ch in text.chars() {
            let Some(g) = font.glyph(ch, requested_size) else {
                continue;
            };
            let advance;
            match &g.kind {
                GlyphKind::Mono(bitmap) => {
                    advance = g.advance as i32 * scale;
                    self.blit_mono_glyph(bitmap, cx, cy, scale, char_h, phys_bounds, color, opa);
                }
                GlyphKind::Sdf {
                    atlas,
                    source_size,
                    bit_depth,
                    spread,
                    ..
                } => {
                    advance =
                        g.advance as i32 * requested_size as i32 / (*source_size as i32).max(1);
                    self.blit_sdf_glyph(
                        atlas,
                        *source_size,
                        *bit_depth,
                        *spread,
                        cx,
                        cy,
                        requested_size,
                        phys_bounds,
                        color,
                        opa,
                    );
                }
                GlyphKind::Grayscale {
                    coverage,
                    bpp,
                    w,
                    h,
                    ..
                } => {
                    advance = g.advance as i32 * scale;
                    self.blit_gray_glyph(coverage, *bpp, *w, *h, cx, cy, phys_bounds, color, opa);
                }
            }
            cx += advance;
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn blit_mono_glyph(
        &mut self,
        bitmap: &[u8],
        cx: i32,
        cy: i32,
        scale: i32,
        char_h: i32,
        phys_bounds: (i32, i32, i32, i32),
        color: &Color,
        opa: u8,
    ) {
        let (clip_x, clip_y, clip_x2, clip_y2) = phys_bounds;
        let target_w = self.target.width as usize;
        let clip_mask = self.clip_stack.last().map(|m| m.alpha.as_slice());
        for row in 0..char_h.min(bitmap.len() as i32) {
            let byte = bitmap[row as usize];
            for col in 0..8 {
                if byte & (0x80 >> col) == 0 {
                    continue;
                }
                for sy in 0..scale {
                    for sx in 0..scale {
                        let px = cx + col * scale + sx;
                        let py = cy + row * scale + sy;
                        if px >= clip_x && px < clip_x2 && py >= clip_y && py < clip_y2 {
                            let alpha = match clip_mask {
                                Some(m) => {
                                    let ca = m[py as usize * target_w + px as usize];
                                    if ca == 0 {
                                        continue;
                                    }
                                    ((opa as u16 * ca as u16 + 127) / 255) as u8
                                }
                                None => opa,
                            };
                            self.target.blend_pixel(
                                Fixed::from_int(px),
                                Fixed::from_int(py),
                                color,
                                alpha,
                            );
                        }
                    }
                }
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn blit_gray_glyph(
        &mut self,
        coverage: &[u8],
        bpp: u8,
        w: u8,
        h: u8,
        x0: i32,
        y0: i32,
        phys_bounds: (i32, i32, i32, i32),
        color: &Color,
        base_opa: u8,
    ) {
        let (clip_x, clip_y, clip_x2, clip_y2) = phys_bounds;
        let w = w as usize;
        let h = h as usize;
        let max_q = (1u16 << bpp) - 1;
        let target_w = self.target.width as usize;
        let clip_mask = self.clip_stack.last().map(|m| m.alpha.as_slice());
        let mut bit_cursor = 0usize;
        for row in 0..h {
            for col in 0..w {
                let q = read_packed(coverage, bit_cursor, bpp);
                bit_cursor += bpp as usize;
                if q == 0 {
                    continue;
                }
                let cov = (q as u32 * 255 / max_q as u32) as u8;
                let mut alpha = (cov as u32 * base_opa as u32 / 255) as u8;
                if alpha == 0 {
                    continue;
                }
                let px = x0 + col as i32;
                let py = y0 + row as i32;
                if px >= clip_x && px < clip_x2 && py >= clip_y && py < clip_y2 {
                    if let Some(mask) = clip_mask {
                        let ca = mask[py as usize * target_w + px as usize];
                        if ca == 0 {
                            continue;
                        }
                        if ca < 255 {
                            alpha = ((alpha as u16 * ca as u16 + 127) / 255) as u8;
                            if alpha == 0 {
                                continue;
                            }
                        }
                    }
                    self.target.blend_pixel_int(px, py, color, alpha);
                }
            }
        }
    }
}

fn read_packed(data: &[u8], bit_pos: usize, bpp: u8) -> u16 {
    let byte_idx = bit_pos / 8;
    let bit_off = bit_pos % 8;
    let hi = *data.get(byte_idx).unwrap_or(&0) as u16;
    let lo = *data.get(byte_idx + 1).unwrap_or(&0) as u16;
    let window = (hi << 8) | lo;
    let shift = 16 - bit_off - bpp as usize;
    let mask = (1u16 << bpp) - 1;
    (window >> shift) & mask
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::render::backends::sw::SwRenderer;
    use crate::render::texture::{ColorFormat, Texture};
    use alloc::vec;

    fn pixel_alpha(buf: &[u8], stride: usize, x: usize, y: usize) -> u8 {
        buf[(y * stride + x) * 4 + 3]
    }

    #[test]
    fn read_packed_4bit_msb_first() {
        let data = [0xAB_u8, 0xCD];
        assert_eq!(read_packed(&data, 0, 4), 0xA);
        assert_eq!(read_packed(&data, 4, 4), 0xB);
        assert_eq!(read_packed(&data, 8, 4), 0xC);
        assert_eq!(read_packed(&data, 12, 4), 0xD);
    }

    #[test]
    fn read_packed_handles_unaligned_and_cross_byte() {
        let data = [0b11_01_00_10_u8, 0b01_11_00_10];
        assert_eq!(read_packed(&data, 0, 2), 0b11);
        assert_eq!(read_packed(&data, 6, 2), 0b10);
        assert_eq!(read_packed(&data, 8, 2), 0b01);
        assert_eq!(read_packed(&data, 4, 8), 0b0010_0111);
    }

    #[test]
    fn read_packed_past_end_reads_zero() {
        let data = [0xFF_u8];
        assert_eq!(read_packed(&data, 8, 4), 0);
    }

    #[test]
    fn gray_full_coverage_is_opaque_zero_is_blank() {
        let coverage = [0xF0_u8];
        let mut buf = vec![0u8; 4 * 4 * 4];
        let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
        let mut backend = SwRenderer::new(tex);
        let color = Color::rgba(255, 255, 255, 255);

        backend.blit_gray_glyph(&coverage, 4, 2, 1, 0, 0, (0, 0, 4, 4), &color, 255);

        assert_eq!(pixel_alpha(&buf, 4, 0, 0), 255, "0xF -> opaque");
        assert_eq!(pixel_alpha(&buf, 4, 1, 0), 0, "0x0 -> blank");
    }

    #[test]
    fn gray_mid_coverage_scales_to_alpha() {
        let coverage = [0x80_u8];
        let mut buf = vec![0u8; 4 * 4 * 4];
        let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
        let mut backend = SwRenderer::new(tex);
        let color = Color::rgba(255, 255, 255, 255);

        backend.blit_gray_glyph(&coverage, 4, 1, 1, 0, 0, (0, 0, 4, 4), &color, 255);

        let r = buf[0];
        assert!((130..=140).contains(&r), "0x8/0xF * 255 ≈ 136, got {r}");
    }

    #[test]
    fn gray_respects_clip_bounds() {
        let coverage = [0xFF_u8, 0xFF];
        let mut buf = vec![0u8; 4 * 4 * 4];
        let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
        let mut backend = SwRenderer::new(tex);
        let color = Color::rgba(255, 255, 255, 255);

        backend.blit_gray_glyph(&coverage, 4, 4, 1, 0, 0, (0, 0, 2, 4), &color, 255);

        assert_eq!(pixel_alpha(&buf, 4, 0, 0), 255);
        assert_eq!(pixel_alpha(&buf, 4, 1, 0), 255);
        assert_eq!(pixel_alpha(&buf, 4, 2, 0), 0, "clipped at x=2");
    }

    #[test]
    fn gray_rows_pack_without_byte_padding() {
        let coverage = [0xF0_u8, 0xF0, 0xF0];
        let mut buf = vec![0u8; 4 * 4 * 4];
        let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
        let mut backend = SwRenderer::new(tex);
        let color = Color::rgba(255, 255, 255, 255);

        backend.blit_gray_glyph(&coverage, 4, 3, 2, 0, 0, (0, 0, 4, 4), &color, 255);

        assert_eq!(pixel_alpha(&buf, 4, 0, 0), 255);
        assert_eq!(pixel_alpha(&buf, 4, 1, 0), 0);
        assert_eq!(pixel_alpha(&buf, 4, 2, 0), 255);
        assert_eq!(pixel_alpha(&buf, 4, 0, 1), 0);
        assert_eq!(pixel_alpha(&buf, 4, 1, 1), 255);
        assert_eq!(pixel_alpha(&buf, 4, 2, 1), 0);
    }
}