aura-gui 0.2.1

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.
//! Sombras de caja como `box-shadow` de CSS: desplazamiento, desenfoque, extensión y color, con hasta
//! [`MAX_SHADOWS`] capas. Cada capa es una sola primitiva gaussiana analítica en el shader: no hay pasadas de
//! desenfoque ni texturas intermedias, así que una sombra cuesta lo mismo que un rectángulo.

use crate::view::theme::ColorSpec;
use crate::core::{Point, px};

/// Una capa de sombra. Sin color propio usa el color de sombra del tema.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BoxShadow {
    pub offset: Point,
    /// Radio de desenfoque en px, con el mismo significado que en CSS.
    pub blur: f32,
    /// Crece (o encoge, si es negativa) la caja antes de desenfocar.
    pub spread: f32,
    pub color: Option<ColorSpec>,
    /// Multiplica la opacidad del color.
    pub strength: f32,
}

impl BoxShadow {
    /// `box-shadow: x y blur`.
    pub const fn new(x: f32, y: f32, blur: f32) -> Self {
        Self { offset: Point { x: px(x), y: px(y) }, blur, spread: 0.0, color: None, strength: 1.0 }
    }

    pub const fn spread(mut self, spread: f32) -> Self {
        self.spread = spread;
        self
    }

    pub const fn strength(mut self, strength: f32) -> Self {
        self.strength = strength;
        self
    }

    pub fn color(mut self, color: impl Into<ColorSpec>) -> Self {
        self.color = Some(color.into());
        self
    }
}

pub const MAX_SHADOWS: usize = 3;

/// Pila de capas, de arriba abajo como en CSS (la primera se pinta encima).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Shadows {
    layers: [BoxShadow; MAX_SHADOWS],
    len: u8,
}

impl Shadows {
    pub const NONE: Self = Self { layers: [BoxShadow::new(0.0, 0.0, 0.0); MAX_SHADOWS], len: 0 };

    pub const fn one(shadow: BoxShadow) -> Self {
        let mut shadows = Self::NONE;
        shadows.layers[0] = shadow;
        shadows.len = 1;
        shadows
    }

    /// Añade una capa debajo de las anteriores; más allá de [`MAX_SHADOWS`] se ignora.
    pub const fn with(mut self, shadow: BoxShadow) -> Self {
        if (self.len as usize) < MAX_SHADOWS {
            self.layers[self.len as usize] = shadow;
            self.len += 1;
        }
        self
    }

    pub fn layers(&self) -> &[BoxShadow] {
        &self.layers[..self.len as usize]
    }

    /// Las mismas capas con otro color (p. ej. una sombra de color de marca).
    pub fn tinted(mut self, color: ColorSpec) -> Self {
        for layer in &mut self.layers[..self.len as usize] {
            layer.color = Some(color);
        }
        self
    }
}

impl From<BoxShadow> for Shadows {
    fn from(shadow: BoxShadow) -> Self {
        Self::one(shadow)
    }
}

impl From<ShadowPreset> for Shadows {
    fn from(preset: ShadowPreset) -> Self {
        preset.shadows()
    }
}

/// Sombras predefinidas con la misma escala que Tailwind.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShadowPreset {
    Xs,
    Sm,
    Md,
    Lg,
    Xl,
    Xxl,
}

impl ShadowPreset {
    /// `(desplazamiento_y, desenfoque, opacidad relativa)` de la capa principal.
    pub const fn parameters(self) -> (f32, f32, f32) {
        match self {
            ShadowPreset::Xs => (1.0, 2.0, 0.5),
            ShadowPreset::Sm => (1.0, 3.0, 0.6),
            ShadowPreset::Md => (4.0, 8.0, 0.7),
            ShadowPreset::Lg => (10.0, 20.0, 0.8),
            ShadowPreset::Xl => (20.0, 32.0, 0.9),
            ShadowPreset::Xxl => (25.0, 50.0, 1.0),
        }
    }

    pub const fn shadows(self) -> Shadows {
        let (y, blur, strength) = self.parameters();
        let main = BoxShadow::new(0.0, y, blur).strength(strength);
        match self {
            // Como `shadow-2xl`: una sombra larga y recogida, que flota.
            ShadowPreset::Xxl => Shadows::one(main.spread(-12.0)),
            _ => Shadows::one(main),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn layers_stop_at_the_maximum() {
        let layer = BoxShadow::new(0.0, 1.0, 2.0);
        let shadows = Shadows::one(layer).with(layer).with(layer).with(layer);
        assert_eq!(shadows.layers().len(), MAX_SHADOWS);
        assert!(Shadows::NONE.layers().is_empty());
    }

    #[test]
    fn presets_keep_their_previous_look() {
        let md = Shadows::from(ShadowPreset::Md);
        assert_eq!(md.layers(), &[BoxShadow::new(0.0, 4.0, 8.0).strength(0.7)]);
    }
}