aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Glifos, iconos e imágenes por CPU, leídos del atlas en memoria. Si el tile mide lo mismo que su destino (el
//! caso de casi todo el texto) se copia texel a texel; si no, se interpola como el muestreador lineal de la GPU.

use super::canvas::{Canvas, Premultiplied};
use crate::gpu::scene::MonochromeSprite;

/// Píxeles del atlas: lado del atlas y bytes por píxel (1 en el de glifos, 4 en el de color).
pub struct AtlasView<'a> {
    pub pixels: &'a [u8],
    pub size: u32,
    pub channels: u32,
}

impl AtlasView<'_> {
    #[inline]
    fn texel(&self, x: u32, y: u32) -> [f32; 4] {
        let index = ((y.min(self.size - 1) * self.size + x.min(self.size - 1)) * self.channels) as usize;
        let value = |offset: usize| self.pixels[index + offset] as f32 / 255.0;
        if self.channels == 1 { [value(0); 4] } else { [value(0), value(1), value(2), value(3)] }
    }

    /// Muestra bilineal en coordenadas de texel (centros en `n + 0.5`).
    fn sample(&self, u: f32, v: f32) -> [f32; 4] {
        let (x, y) = ((u - 0.5).max(0.0), (v - 0.5).max(0.0));
        let (x0, y0) = (x.floor() as u32, y.floor() as u32);
        let (fx, fy) = (x.fract(), y.fract());
        let [a, b, c, d] =
            [self.texel(x0, y0), self.texel(x0 + 1, y0), self.texel(x0, y0 + 1), self.texel(x0 + 1, y0 + 1)];
        std::array::from_fn(|i| {
            let top = a[i] + (b[i] - a[i]) * fx;
            let bottom = c[i] + (d[i] - c[i]) * fx;
            top + (bottom - top) * fy
        })
    }
}

/// `polychrome`: el atlas guarda RGBA premultiplicado y el color del sprite solo aporta su opacidad.
pub fn sprite(canvas: &mut Canvas, atlas: &AtlasView, sprite: &MonochromeSprite, polychrome: bool) {
    let bounds = sprite.bounds;
    let (columns, rows) = canvas.area(bounds, 0.0, sprite.clip);
    if columns.is_empty() || rows.is_empty() || bounds[2] <= 0.0 || bounds[3] <= 0.0 {
        return;
    }
    let size = atlas.size as f32;
    let [u0, v0, u1, v1] = sprite.uv.map(|uv| uv * size);
    let tint = Premultiplied::from_straight(sprite.color);
    let exact =
        ((u1 - u0) - bounds[2] * canvas.scale).abs() < 0.5 && ((v1 - v0) - bounds[3] * canvas.scale).abs() < 0.5;
    for y in rows {
        let ty = (canvas.logical(y) - bounds[1]) / bounds[3];
        for x in columns.clone() {
            let tx = (canvas.logical(x) - bounds[0]) / bounds[2];
            let (u, v) = (u0 + tx * (u1 - u0), v0 + ty * (v1 - v0));
            let texel = if exact { atlas.texel(u as u32, v as u32) } else { atlas.sample(u, v) };
            let color = if polychrome {
                Premultiplied { r: texel[0], g: texel[1], b: texel[2], a: texel[3] }.scaled(tint.a)
            } else {
                tint.scaled(texel[0])
            };
            if color.a > 0.002 {
                canvas.blend_at(x, y, color);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::{Bounds, Point, Rgba, Size, px};

    #[test]
    fn un_glifo_se_tine_con_su_color_y_respeta_el_recorte() {
        // Atlas 4×4 con una máscara 2×2 opaca en la esquina.
        let mut mask = vec![0u8; 16];
        for index in [0, 1, 4, 5] {
            mask[index] = 255;
        }
        let atlas = AtlasView { pixels: &mask, size: 4, channels: 1 };
        let mut pixels = vec![0xffffff; 16];
        let mut canvas = Canvas { pixels: &mut pixels, width: 4, height: 4, scale: 1.0 };
        let bounds = Bounds::new(Point::new(px(1.0), px(1.0)), Size::new(px(2.0), px(2.0)));
        let clip = Bounds::new(Point::new(px(0.0), px(0.0)), Size::new(px(2.0), px(4.0)));
        let glyph = MonochromeSprite::new(bounds, clip, [0.0, 0.0, 0.5, 0.5], Rgba::new(0.0, 0.0, 0.0, 1.0));
        sprite(&mut canvas, &atlas, &glyph, false);
        assert_eq!(pixels[4 + 1], 0x000000, "dentro del recorte, negro");
        assert_eq!(pixels[4 + 2], 0xffffff, "fuera del recorte, intacto");
    }
}