aura-gui 0.3.5

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.
//! Rectángulos (con esquinas, borde y degradado), sombras y subrayados por CPU, con las fórmulas del shader.
//!
//! Rapidez: el interior de cada figura (lejos de esquinas y bordes) se rellena sin calcular distancias; solo los
//! píxeles del contorno pagan la fórmula completa. Un panel de pantalla completa cuesta casi lo que un `memset`.

use super::canvas::{Canvas, Premultiplied, pack, rounded_rect_distance};
use crate::gpu::scene::{Quad, Shadow, Underline};
use std::ops::Range;

/// Columnas de una fila que están seguro dentro del rectángulo encogido `inset` (lógico) por cada lado.
fn interior(canvas: &Canvas, bounds: [f32; 4], inset: f32, y: u32, columns: &Range<u32>) -> Range<u32> {
    let center_y = canvas.logical(y);
    if center_y < bounds[1] + inset || center_y > bounds[1] + bounds[3] - inset {
        return 0..0;
    }
    let first = ((bounds[0] + inset) * canvas.scale - 0.5).ceil().max(columns.start as f32) as u32;
    let last = (((bounds[0] + bounds[2] - inset) * canvas.scale - 0.5).floor() + 1.0).min(columns.end as f32);
    let last = last.max(first as f32) as u32;
    first..last
}

fn max4(values: [f32; 4]) -> f32 {
    values.into_iter().fold(0.0, f32::max)
}

pub fn quad(canvas: &mut Canvas, quad: &Quad) {
    let bounds = quad.bounds;
    let (columns, rows) = canvas.area(bounds, 1.0, quad.clip);
    let half = (bounds[2] * 0.5, bounds[3] * 0.5);
    let center = (bounds[0] + half.0, bounds[1] + half.1);
    let widths = quad.border_widths; // top, right, bottom, left
    let has_border = widths.iter().sum::<f32>() > 0.001;
    let inner_half = (half.0 - (widths[3] + widths[1]) * 0.5, half.1 - (widths[0] + widths[2]) * 0.5);
    let inner_center = (center.0 + (widths[3] - widths[1]) * 0.5, center.1 + (widths[0] - widths[2]) * 0.5);
    let widest = max4(widths);
    let inner_radii = quad.corner_radii.map(|radius| (radius - widest).max(0.0));
    let fill = Premultiplied::from_straight(quad.background);
    let border = Premultiplied::from_straight(quad.border_color);
    let gradient = (quad.gradient[2] > 0.5).then(|| {
        let direction = (quad.gradient[0], quad.gradient[1]);
        let length = (bounds[2] * direction.0).abs() + (bounds[3] * direction.1).abs();
        (direction, length.max(0.001), Premultiplied::from_straight(quad.gradient_end))
    });
    let paint = |point: (f32, f32)| match gradient {
        Some((direction, length, end)) => {
            let along = (point.0 - center.0) * direction.0 + (point.1 - center.1) * direction.1;
            fill.mix(end, (0.5 + along / length).clamp(0.0, 1.0))
        }
        None => fill,
    };
    let inset = max4(quad.corner_radii) + widest + 1.0;
    // Un color opaco y sin degradado se copia tal cual al interior, sin mezclar píxel a píxel.
    let opaque = (gradient.is_none() && fill.a >= 0.999).then(|| pack(fill.r, fill.g, fill.b));
    for y in rows {
        let center_y = canvas.logical(y);
        let solid = interior(canvas, bounds, inset, y, &columns);
        // Con relleno opaco el interior ya está pintado: el bucle solo recorre los dos bordes de la fila.
        let edges = match opaque.filter(|_| !solid.is_empty()) {
            Some(packed) => {
                canvas.fill_span(y, solid.clone(), packed);
                (columns.start..solid.start).chain(solid.end..columns.end)
            }
            None => (columns.clone()).chain(0..0),
        };
        for x in edges {
            let point = (canvas.logical(x), center_y);
            if solid.contains(&x) {
                canvas.blend_at(x, y, paint(point));
                continue;
            }
            let local = (point.0 - center.0, point.1 - center.1);
            let coverage = (0.5 - rounded_rect_distance(local, half, quad.corner_radii)).clamp(0.0, 1.0);
            if coverage <= 0.0 {
                continue;
            }
            let mut color = paint(point);
            if has_border {
                let inner_local = (point.0 - inner_center.0, point.1 - inner_center.1);
                let inner = rounded_rect_distance(inner_local, inner_half, inner_radii);
                color = color.mix(border, (0.5 + inner).clamp(0.0, 1.0));
            }
            canvas.blend_at(x, y, color.scaled(coverage));
        }
    }
}

/// Aproximación de erf (Abramowitz-Stegun), la misma del shader.
fn erf(value: f32) -> f32 {
    let x = value.abs();
    let t = 1.0 / (1.0 + 0.327_591_1 * x);
    let poly = t * (0.254_829_6 + t * (-0.284_496_74 + t * (1.421_413_8 + t * (-1.453_152 + t * 1.061_405_4))));
    value.signum() * (1.0 - poly * (-x * x).exp())
}

pub fn shadow(canvas: &mut Canvas, shadow: &Shadow) {
    let bounds = shadow.bounds;
    let (columns, rows) = canvas.area(bounds, shadow.blur * 3.0, shadow.clip);
    let half = (bounds[2] * 0.5, bounds[3] * 0.5);
    let center = (bounds[0] + half.0, bounds[1] + half.1);
    let sigma = (shadow.blur * 0.5).max(0.001);
    let color = Premultiplied::from_straight(shadow.color);
    // Más adentro que esto la integral ya vale 1: sombra plena sin calcular erf.
    let inset = max4(shadow.corner_radii) + sigma * 4.0;
    for y in rows {
        let center_y = canvas.logical(y);
        let solid = interior(canvas, bounds, inset, y, &columns);
        for x in columns.clone() {
            if solid.contains(&x) {
                canvas.blend_at(x, y, color);
                continue;
            }
            let local = (canvas.logical(x) - center.0, center_y - center.1);
            let distance = rounded_rect_distance(local, half, shadow.corner_radii);
            let falloff = 0.5 * (1.0 - erf(distance / (sigma * std::f32::consts::SQRT_2)));
            if falloff > 0.002 {
                canvas.blend_at(x, y, color.scaled(falloff));
            }
        }
    }
}

pub fn underline(canvas: &mut Canvas, underline: &Underline) {
    let bounds = underline.bounds;
    let (columns, rows) = canvas.area(bounds, 0.0, underline.clip);
    let color = Premultiplied::from_straight(underline.color);
    let center_y = bounds[3] * 0.5;
    for y in rows {
        let local_y = canvas.logical(y) - bounds[1];
        for x in columns.clone() {
            let local_x = canvas.logical(x) - bounds[0];
            let wave = (local_x * 1.2).sin() * bounds[3] * 0.25 * underline.wavy;
            let distance = (local_y - center_y - wave).abs() - underline.thickness * 0.5;
            let alpha = (0.5 - distance).clamp(0.0, 1.0);
            if alpha > 0.0 {
                canvas.blend_at(x, y, color.scaled(alpha));
            }
        }
    }
}

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

    fn bounds(x: f32, y: f32, w: f32, h: f32) -> Bounds {
        Bounds::new(Point::new(px(x), px(y)), Size::new(px(w), px(h)))
    }

    #[test]
    fn rellena_el_interior_redondea_esquinas_y_pinta_el_borde() {
        let mut pixels = vec![0xffffff; 20 * 20];
        let mut canvas = Canvas { pixels: &mut pixels, width: 20, height: 20, scale: 1.0 };
        let red = Rgba::new(1.0, 0.0, 0.0, 1.0);
        let all = bounds(0.0, 0.0, 20.0, 20.0);
        let rounded = Quad::new(bounds(2.0, 2.0, 16.0, 16.0), all, red)
            .corners(Corners::all(px(6.0)))
            .border(Edges::all(px(2.0)), Rgba::new(0.0, 0.0, 1.0, 1.0));
        quad(&mut canvas, &rounded);
        assert_eq!(pixels[10 * 20 + 10], 0xff0000, "centro relleno");
        assert_eq!(pixels[10 * 20 + 2], 0x0000ff, "borde izquierdo");
        assert_eq!(pixels[2 * 20 + 2], 0xffffff, "la esquina redondeada queda fuera");
    }

    #[test]
    fn la_sombra_se_desvanece_hacia_afuera_y_el_subrayado_es_fino() {
        let mut pixels = vec![pack(1.0, 1.0, 1.0); 40 * 40];
        let mut canvas = Canvas { pixels: &mut pixels, width: 40, height: 40, scale: 1.0 };
        let all = bounds(0.0, 0.0, 40.0, 40.0);
        let black = Rgba::new(0.0, 0.0, 0.0, 1.0);
        shadow(&mut canvas, &Shadow::new(bounds(10.0, 10.0, 20.0, 20.0), all, black, Corners::all(px(0.0)), 4.0));
        let darkness = |index: usize| 255 - (pixels[index] & 0xff);
        assert!(darkness(20 * 40 + 20) > 250, "dentro, plena");
        assert!(darkness(20 * 40 + 31) < darkness(20 * 40 + 29), "se aclara al salir");
        let mut lines = vec![pack(1.0, 1.0, 1.0); 10 * 4];
        let mut canvas = Canvas { pixels: &mut lines, width: 10, height: 4, scale: 1.0 };
        underline(
            &mut canvas,
            &Underline::new(bounds(0.0, 0.0, 10.0, 4.0), bounds(0.0, 0.0, 10.0, 4.0), black, 1.0, false),
        );
        // Una línea de 1 px centrada en y = 2 cubre la mitad de las filas 1 y 2, como en la GPU.
        assert!(lines[10 * 2 + 5] & 0xff < 200, "la línea central se pinta");
        assert_eq!(lines[5] & 0xff, 255, "la fila de arriba no");
    }
}