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.
//! Propiedades de estilo de uso menos frecuente (rasgos de texto, esquinas y lados, anillo, cuadrícula…).
//!
//! Viven en una caja dentro de `Style` que solo existe si se usa alguna: un `div` normal no paga memoria ni
//! tiempo de copia por ellas.

use crate::core::Bounds;
use crate::core::Pixels;
use crate::gpu::Affine;
use crate::gpu::{TextAlign, TextTransform};
use crate::view::shadow::Shadows;
use crate::view::style::{Justify, Length, Sides, refine_options};
use crate::view::theme::ColorSpec;

/// Rasgos de texto que los descendientes heredan: se aplican encima de los del padre.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TextRefine {
    pub weight: Option<u16>,
    pub align: Option<TextAlign>,
    /// Espaciado entre letras en em.
    pub tracking: Option<f32>,
    /// Líneas máximas antes de recortar con «…» (`truncate` = 1).
    pub lines: Option<u16>,
    pub transform: Option<TextTransform>,
    /// Interlineado como múltiplo del tamaño de letra.
    pub leading: Option<f32>,
    pub strike: Option<bool>,
    pub mono: Option<bool>,
}

impl TextRefine {
    fn refine(&mut self, other: &TextRefine) {
        refine_options!(self, other, weight, align, tracking, lines, transform, leading, strike, mono);
    }
}

/// Radio por esquina; lo que no se fija usa el radio uniforme de `Style::radius`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CornerRadii {
    pub top_left: Option<Pixels>,
    pub top_right: Option<Pixels>,
    pub bottom_right: Option<Pixels>,
    pub bottom_left: Option<Pixels>,
}

impl CornerRadii {
    fn refine(&mut self, other: &CornerRadii) {
        refine_options!(self, other, top_left, top_right, bottom_right, bottom_left);
    }
}

/// Cuadrícula (`grid-cols-3`, `col-span-2`…); con `cols` o `rows` el contenedor pasa a ser una cuadrícula.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GridStyle {
    pub cols: Option<u16>,
    pub rows: Option<u16>,
    pub col_span: Option<u16>,
    pub row_span: Option<u16>,
    /// Línea de inicio (1 es la primera; negativa cuenta desde el final).
    pub col_start: Option<i16>,
    pub row_start: Option<i16>,
    /// `grid-flow-col`: los hijos se colocan por columnas.
    pub flow_col: Option<bool>,
}

impl GridStyle {
    fn refine(&mut self, other: &GridStyle) {
        refine_options!(self, other, cols, rows, col_span, row_span, col_start, row_start, flow_col);
    }

    pub fn is_grid(&self) -> bool {
        self.cols.is_some() || self.rows.is_some()
    }
}

/// `transform`: traslación, giro, escala e inclinación del elemento, sin cambiar su sitio en el layout.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TransformSpec {
    /// En px, o en porcentaje del propio tamaño (`-translate-x-1/2` centra).
    pub translate_x: Option<Length>,
    pub translate_y: Option<Length>,
    /// Grados en sentido horario.
    pub rotate: Option<f32>,
    pub scale_x: Option<f32>,
    pub scale_y: Option<f32>,
    pub skew_x: Option<f32>,
    pub skew_y: Option<f32>,
}

impl TransformSpec {
    fn refine(&mut self, other: &TransformSpec) {
        refine_options!(self, other, translate_x, translate_y, rotate, scale_x, scale_y, skew_x, skew_y);
    }

    pub fn is_none(&self) -> bool {
        *self == Self::default()
    }

    /// Matriz sobre un elemento de `bounds`: escala, inclinación y giro alrededor del centro (`transform-origin`),
    /// y después la traslación. Es el orden de CSS (`translate rotate skew scale`).
    pub fn affine(&self, bounds: Bounds) -> Affine {
        let (width, height) = (bounds.size.width.0, bounds.size.height.0);
        let center = (bounds.origin.x.0 + width * 0.5, bounds.origin.y.0 + height * 0.5);
        let offset = |length: Option<Length>, extent: f32| match length {
            Some(Length::Px(value)) => value.0,
            Some(Length::Percent(fraction)) => fraction * extent,
            Some(Length::Auto) | None => 0.0,
        };
        Affine::scale(self.scale_x.unwrap_or(1.0), self.scale_y.unwrap_or(1.0))
            .then(Affine::skew(self.skew_x.unwrap_or(0.0), self.skew_y.unwrap_or(0.0)))
            .then(Affine::rotate(self.rotate.unwrap_or(0.0)))
            .about(center)
            .then(Affine::translate(offset(self.translate_x, width), offset(self.translate_y, height)))
    }
}

/// `blur` y `backdrop-blur`: desviación en px lógicos (como el `blur(8px)` de CSS).
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FilterSpec {
    /// Desenfoca el elemento: se aplica sobre lo ya pintado en su caja, después de pintar sus hijos.
    pub blur: Option<f32>,
    /// Desenfoca lo que hay detrás del elemento, antes de pintar su fondo (efecto cristal).
    pub backdrop_blur: Option<f32>,
}

impl FilterSpec {
    fn refine(&mut self, other: &FilterSpec) {
        refine_options!(self, other, blur, backdrop_blur);
    }
}

/// Bits de `StyleExtra::margin_auto`: los lados cuyo margen es `auto` (`mx-auto` centra, `ml-auto` empuja a la derecha).
pub const AUTO_TOP: u8 = 1;
pub const AUTO_RIGHT: u8 = 2;
pub const AUTO_BOTTOM: u8 = 4;
pub const AUTO_LEFT: u8 = 8;
pub const AUTO_ALL: u8 = AUTO_TOP | AUTO_RIGHT | AUTO_BOTTOM | AUTO_LEFT;

#[derive(Clone, Debug, Default, PartialEq)]
pub struct StyleExtra {
    pub text: TextRefine,
    pub corners: CornerRadii,
    /// Grosor de borde por lado; lo que no se fija usa `Style::border_width`.
    pub border: Sides,
    /// Sombras por capas (`shadow_md`, `glow`…).
    pub shadow: Option<Shadows>,
    /// Degradado lineal desde `background`: ángulo CSS en grados y color final.
    pub gradient: Option<(f32, ColorSpec)>,
    /// `ring-*`: contorno fuera de la caja que no ocupa espacio.
    pub ring: Option<Pixels>,
    pub ring_color: Option<ColorSpec>,
    /// `flex-row-reverse` y `flex-col-reverse`.
    pub reverse: Option<bool>,
    pub align_content: Option<Justify>,
    pub grid: GridStyle,
    pub margin_auto: u8,
    /// `z-10`: orden de apilado entre hermanos.
    pub z: Option<i16>,
    pub transform: TransformSpec,
    pub filter: FilterSpec,
}

impl StyleExtra {
    pub fn refine(&mut self, other: &StyleExtra) {
        self.text.refine(&other.text);
        self.corners.refine(&other.corners);
        self.border.refine(&other.border);
        self.grid.refine(&other.grid);
        self.transform.refine(&other.transform);
        self.filter.refine(&other.filter);
        refine_options!(self, other, shadow, gradient, ring, ring_color, reverse, align_content, z);
        self.margin_auto |= other.margin_auto;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::view::style::Style;

    #[test]
    fn un_estilo_sin_extras_solo_paga_un_puntero() {
        assert_eq!(std::mem::size_of::<Option<Box<StyleExtra>>>(), std::mem::size_of::<usize>());
        let style = Style::default();
        assert!(style.extra.is_none(), "no se reserva nada hasta usar una propiedad poco frecuente");
    }

    #[test]
    fn refinar_une_los_extras_sin_perder_los_del_estilo_base() {
        let mut base = Style::default();
        base.extra_mut().text.weight = Some(600);
        base.extra_mut().margin_auto = AUTO_LEFT;
        let mut other = Style::default();
        other.extra_mut().text.align = Some(TextAlign::Center);
        other.extra_mut().margin_auto = AUTO_RIGHT;
        base.refine(&other);
        let extra = base.extra.as_deref().unwrap();
        assert_eq!((extra.text.weight, extra.text.align), (Some(600), Some(TextAlign::Center)));
        assert_eq!(extra.margin_auto, AUTO_LEFT | AUTO_RIGHT);
        let mut empty = Style::default();
        empty.refine(&other);
        assert_eq!(empty.extra, other.extra, "sin extras propios se copian los del otro");
    }
}