aura-gui 0.1.0

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.
//! Utilidades de pintado: convierten estilos y texto en primitivas de la escena.

use crate::view::context::ElementContext;
use crate::view::style::Style;
use crate::view::theme::Token;
use crate::core::{Bounds, Corners, Edges, Pixels, Point, Rgba, Size};
use crate::gpu::text::TextPlacement;
use crate::gpu::{ImageSource, MonochromeSprite, Quad, Shadow, ShapedText, Underline};

impl ElementContext<'_> {
    /// Sombra, fondo y borde de una caja, todo con SDF en el shader.
    pub fn paint_box(&mut self, bounds: Bounds, style: &Style) {
        let mask = self.mask();
        let opacity = style.opacity.unwrap_or(1.0);
        let radius = style.radius.unwrap_or_default().min(bounds.size.width / 2.0).min(bounds.size.height / 2.0);
        let corners = Corners::all(radius);

        // Al revés: en CSS la primera capa queda encima.
        for layer in style.shadow.iter().flat_map(|shadows| shadows.layers().iter().rev()) {
            let color = layer.color.map_or(self.theme.shadow, |spec| self.theme.resolve(spec));
            let color = color.with_alpha(color.a * layer.strength * opacity);
            let spread = Pixels(layer.spread);
            let grown = Size::new(
                Pixels((bounds.size.width.0 + spread.0 * 2.0).max(0.0)),
                Pixels((bounds.size.height.0 + spread.0 * 2.0).max(0.0)),
            );
            let origin = bounds.origin + layer.offset - Point::new(spread, spread);
            let corners = Corners::all(Pixels((radius.0 + spread.0).max(0.0)));
            self.frame.scene.insert_shadow(Shadow::new(Bounds::new(origin, grown), mask, color, corners, layer.blur));
        }

        let background = style.background.map(|spec| self.theme.resolve(spec));
        let border_width = style.border_width.unwrap_or_default();
        if background.is_none() && border_width == Pixels::ZERO {
            return;
        }

        let fill = background.unwrap_or(Rgba::TRANSPARENT);
        let mut quad = Quad::new(bounds, mask, fade(fill, opacity)).corners(corners);
        if let (Some((angle, end)), Some(_)) = (style.gradient, background) {
            quad = quad.gradient(angle, fade(self.theme.resolve(end), opacity));
        }
        if border_width > Pixels::ZERO {
            let border_color =
                style.border_color.map_or(self.theme.color(Token::Border), |spec| self.theme.resolve(spec));
            quad = quad.border(Edges::all(border_width), fade(border_color, opacity));
        }
        self.frame.scene.insert_quad(quad);
    }

    /// Rectángulo sólido recortado por la máscara vigente (cursor, selección, guías).
    pub fn fill_rect(&mut self, bounds: Bounds, color: Rgba) {
        let quad = Quad::new(bounds, self.mask(), color);
        self.frame.scene.insert_quad(quad);
    }

    /// Rectángulo con degradado lineal (ángulo CSS en grados), recortado por la máscara vigente.
    pub fn paint_gradient_rect(&mut self, bounds: Bounds, angle: f32, from: Rgba, to: Rgba) {
        let quad = Quad::new(bounds, self.mask(), from).gradient(angle, to);
        self.frame.scene.insert_quad(quad);
    }

    /// Subrayado recto u ondulado (diagnósticos, enlaces).
    pub fn paint_underline(&mut self, bounds: Bounds, color: Rgba, wavy: bool) {
        let underline = Underline::new(bounds, self.mask(), color, 1.0, wavy);
        self.frame.scene.insert_underline(underline);
    }

    /// Contorno de 1 px (corchetes emparejados, celdas resaltadas).
    pub fn paint_outline(&mut self, bounds: Bounds, color: Rgba, radius: Pixels) {
        let quad = Quad::new(bounds, self.mask(), Rgba::TRANSPARENT)
            .corners(Corners::all(radius))
            .border(Edges::all(Pixels(1.0)), color);
        self.frame.scene.insert_quad(quad);
    }

    pub fn paint_rounded_rect(&mut self, bounds: Bounds, color: Rgba, radius: Pixels) {
        let quad = Quad::new(bounds, self.mask(), color).corners(Corners::all(radius));
        self.frame.scene.insert_quad(quad);
    }

    pub fn paint_text(&mut self, shaped: &ShapedText, origin: Point, color: Rgba) {
        let placement = TextPlacement { origin, color, clip: self.mask(), scale: self.scale };
        let Some(target) = self.glyphs.as_mut() else {
            return;
        };
        self.text.paint(shaped, &placement, target, &mut self.frame.scene);
    }

    /// Tamaño que ocuparía `shaped`, útil para centrar o alinear.
    pub fn measure(&self, shaped: &ShapedText) -> Size {
        shaped.size
    }

    /// Icono SVG rasterizado al tamaño físico exacto, teñido con `color`.
    pub fn paint_icon(&mut self, name: &'static str, svg: &'static [u8], bounds: Bounds, color: Rgba) {
        let size_px = (bounds.size.width.0.max(bounds.size.height.0) * self.scale).round() as u32;
        let mask = self.mask();
        let Some(target) = self.glyphs.as_mut() else { return };
        if let Some(tile) = self.sprites.icon(name, svg, size_px, target) {
            self.frame.scene.insert_monochrome(MonochromeSprite::new(bounds, mask, tile.uv, color));
        }
    }

    /// Libera del atlas una imagen que ya no se pintará (el recorte anterior de un diagrama ampliado).
    pub fn forget_image(&mut self, id: u64) {
        let Some(target) = self.glyphs.as_mut() else { return };
        self.sprites.forget_image(id, target);
    }

    /// Fuera de la máscara (por ejemplo, desplazada fuera de la vista) no se decodifica ni ocupa el atlas.
    pub fn paint_image(&mut self, source: &ImageSource, bounds: Bounds) {
        let mask = self.mask();
        let visible = mask.intersect(&bounds);
        if visible.size.width <= Pixels::ZERO || visible.size.height <= Pixels::ZERO {
            return;
        }
        let Some(target) = self.glyphs.as_mut() else { return };
        if let Some(tile) = self.sprites.image(source, target) {
            self.frame.scene.insert_polychrome(MonochromeSprite::new(
                bounds,
                mask,
                tile.uv,
                Rgba::new(1.0, 1.0, 1.0, 1.0),
            ));
        }
    }
}

fn fade(color: Rgba, opacity: f32) -> Rgba {
    color.with_alpha(color.a * opacity)
}