aura-gui 0.3.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.
//! Rasgos tipográficos de Tailwind que el shaping sabe aplicar: peso, alineación, espaciado entre letras, recorte
//! con «…» y capitalización. Van agrupados para que añadir uno no obligue a tocar cada constructor de estilo.

use cosmic_text::{Align, Attrs, Ellipsize, EllipsizeHeightLimit, Weight};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextAlign {
    Left,
    Center,
    Right,
    Justify,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextTransform {
    Upper,
    Lower,
    Capitalize,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextTraits {
    /// 100 (thin) a 900 (black); 400 es el normal.
    pub weight: u16,
    pub align: Option<TextAlign>,
    /// Espaciado entre letras en em (`tracking-wide` = 0.025).
    pub tracking: f32,
    /// Máximo de líneas antes de recortar con «…»; 0 no recorta (`truncate` = 1).
    pub lines: u16,
    pub transform: Option<TextTransform>,
}

impl Default for TextTraits {
    fn default() -> Self {
        Self { weight: 400, align: None, tracking: 0.0, lines: 0, transform: None }
    }
}

impl Hash for TextTraits {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (self.weight, self.align, self.tracking.to_bits(), self.lines, self.transform).hash(state);
    }
}

impl TextTraits {
    /// Peso y espaciado sobre los atributos base; lo que está en su valor por defecto no se toca.
    pub(crate) fn apply(&self, mut attrs: Attrs<'static>) -> Attrs<'static> {
        if self.weight != 400 {
            attrs = attrs.weight(Weight(self.weight));
        }
        if self.tracking != 0.0 {
            attrs = attrs.letter_spacing(self.tracking);
        }
        attrs
    }

    pub(crate) fn alignment(&self) -> Option<Align> {
        self.align.map(|align| match align {
            TextAlign::Left => Align::Left,
            TextAlign::Center => Align::Center,
            TextAlign::Right => Align::Right,
            TextAlign::Justify => Align::Justified,
        })
    }

    pub(crate) fn ellipsize(&self) -> Ellipsize {
        match self.lines {
            0 => Ellipsize::None,
            lines => Ellipsize::End(EllipsizeHeightLimit::Lines(usize::from(lines))),
        }
    }

    /// El texto ya cambiado de capitalización. Los tramos se miden en bytes y `to_uppercase` puede alargarlos
    /// («ß» → «SS»), así que con tramos enriquecidos el texto no se toca.
    pub(crate) fn transformed<'a>(&self, text: &'a str, rich: bool) -> Cow<'a, str> {
        match self.transform {
            Some(_) if rich => Cow::Borrowed(text),
            Some(TextTransform::Upper) => Cow::Owned(text.to_uppercase()),
            Some(TextTransform::Lower) => Cow::Owned(text.to_lowercase()),
            Some(TextTransform::Capitalize) => Cow::Owned(capitalize(text)),
            None => Cow::Borrowed(text),
        }
    }
}

fn capitalize(text: &str) -> String {
    let mut start = true;
    text.chars()
        .flat_map(|c| {
            let upper = start && c.is_alphabetic();
            start = c.is_whitespace();
            if upper { c.to_uppercase().collect::<Vec<_>>() } else { vec![c] }
        })
        .collect()
}

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

    #[test]
    fn por_defecto_no_cambia_nada() {
        let traits = TextTraits::default();
        assert_eq!(traits.ellipsize(), Ellipsize::None);
        assert!(traits.alignment().is_none());
        assert_eq!(traits.transformed("Hola", false), "Hola");
    }

    #[test]
    fn capitalizacion() {
        let mut traits = TextTraits { transform: Some(TextTransform::Capitalize), ..Default::default() };
        assert_eq!(traits.transformed("hola mundo  azul", false), "Hola Mundo  Azul");
        traits.transform = Some(TextTransform::Upper);
        assert_eq!(traits.transformed("ñandú", false), "ÑANDÚ");
        assert_eq!(traits.transformed("ñandú", true), "ñandú", "con tramos no se toca");
    }

    #[test]
    fn truncar_es_una_linea() {
        let traits = TextTraits { lines: 1, ..Default::default() };
        assert_eq!(traits.ellipsize(), Ellipsize::End(EllipsizeHeightLimit::Lines(1)));
    }
}