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

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

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 = corner_radii(style, bounds.size);

        // Al revés: en CSS la primera capa queda encima.
        for layer in style.shadows().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));
        }

        self.paint_ring(bounds, style, corners);
        let background = style.background.map(|spec| self.theme.resolve(spec));
        let border = border_edges(style);
        let has_border = [border.top, border.right, border.bottom, border.left].iter().any(|side| *side > Pixels::ZERO);
        if background.is_none() && !has_border {
            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 has_border {
            let border_color =
                style.border_color.map_or(self.theme.color(Token::Border), |spec| self.theme.resolve(spec));
            quad = quad.border(border, fade(border_color, opacity));
        }
        self.frame.scene.insert_quad(quad);
    }

    /// Desenfoca lo ya pintado dentro de `bounds` con las esquinas de `style`. Dentro de un elemento transformado se
    /// usa el rectángulo que envuelve a la figura transformada (no hay desenfoque girado).
    pub fn paint_blur(&mut self, bounds: Bounds, style: &Style, sigma: f32) {
        if sigma <= 0.0 {
            return;
        }
        let corners = corner_radii(style, bounds.size);
        let transform = self.frame.scene.transform();
        let [x, y, width, height] =
            transform.bounds_of([bounds.origin.x.0, bounds.origin.y.0, bounds.size.width.0, bounds.size.height.0]);
        let (scale, mask) = (transform.scale_factor(), self.mask());
        let corners = Corners {
            top_left: corners.top_left * scale,
            top_right: corners.top_right * scale,
            bottom_right: corners.bottom_right * scale,
            bottom_left: corners.bottom_left * scale,
        };
        let area = Bounds::new(Point::new(Pixels(x), Pixels(y)), Size::new(Pixels(width), Pixels(height)));
        self.frame.scene.insert_blur(Blur::new(area, mask, corners, sigma * scale));
    }

    /// `ring-*`: contorno por fuera de la caja que no ocupa espacio en el layout.
    fn paint_ring(&mut self, bounds: Bounds, style: &Style, corners: Corners) {
        let Some(extra) = style.extra.as_deref() else { return };
        let Some(ring) = extra.ring.filter(|width| *width > Pixels::ZERO) else { return };
        let color =
            extra.ring_color.map_or(self.theme.color(Token::Accent).with_alpha(0.5), |spec| self.theme.resolve(spec));
        let grow = |radius: Pixels| radius + ring;
        let outer = Corners {
            top_left: grow(corners.top_left),
            top_right: grow(corners.top_right),
            bottom_right: grow(corners.bottom_right),
            bottom_left: grow(corners.bottom_left),
        };
        let size = Size::new(bounds.size.width + ring * 2.0, bounds.size.height + ring * 2.0);
        let quad = Quad::new(Bounds::new(bounds.origin - Point::new(ring, ring), size), self.mask(), Rgba::TRANSPARENT)
            .corners(outer)
            .border(Edges::all(ring), fade(color, style.opacity.unwrap_or(1.0)));
        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),
            ));
        }
    }
}

/// Radio de cada esquina: el fijado para esa esquina o el uniforme, sin pasar de la mitad del lado menor.
fn corner_radii(style: &Style, size: Size) -> Corners {
    let limit = (size.width / 2.0).min(size.height / 2.0);
    let base = style.radius.unwrap_or_default();
    let own = style.extra.as_deref().map(|extra| extra.corners).unwrap_or_default();
    let pick = |corner: Option<Pixels>| corner.unwrap_or(base).min(limit);
    Corners {
        top_left: pick(own.top_left),
        top_right: pick(own.top_right),
        bottom_right: pick(own.bottom_right),
        bottom_left: pick(own.bottom_left),
    }
}

/// Grosor de borde por lado: el fijado para ese lado o el uniforme.
fn border_edges(style: &Style) -> Edges {
    let all = style.border_width.unwrap_or_default();
    let own = style.extra.as_deref().map(|extra| extra.border).unwrap_or_default();
    Edges {
        top: own.top.unwrap_or(all),
        right: own.right.unwrap_or(all),
        bottom: own.bottom.unwrap_or(all),
        left: own.left.unwrap_or(all),
    }
}

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