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.
//! Texto dentro del layout: hojas medibles de Taffy y caché de maquetado.

use crate::gpu::{FontFamily, ShapedText, TextRun, TextStyle, TextSystem, TextTraits};
use crate::view::context::FrameState;
use rustc_hash::{FxHashMap, FxHasher};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use taffy::{AvailableSpace, NodeId};

/// Contenido de una hoja de texto; Taffy le pide medidas durante el layout.
#[derive(Clone, Debug)]
pub struct TextLeaf {
    pub content: Rc<str>,
    pub runs: Rc<[TextRun]>,
    pub font_size: f32,
    pub line_height: f32,
    pub monospace: bool,
    /// Familia concreta (por ejemplo, la de la terminal); `None` usa la sans o la monoespaciada.
    pub family: Option<FontFamily>,
    pub wrap: bool,
    /// Peso, alineación, espaciado, recorte y capitalización.
    pub traits: TextTraits,
}

#[derive(Clone, PartialEq, Eq, Hash)]
struct ShapeKey {
    content: Rc<str>,
    style_hash: u64,
    width: Option<u32>,
}

impl TextLeaf {
    fn style_hash(&self) -> u64 {
        let mut hasher = FxHasher::default();
        self.runs.hash(&mut hasher);
        self.font_size.to_bits().hash(&mut hasher);
        self.line_height.to_bits().hash(&mut hasher);
        self.monospace.hash(&mut hasher);
        self.family.hash(&mut hasher);
        self.traits.hash(&mut hasher);
        hasher.finish()
    }
}

/// Caché de dos generaciones: lo que no se usa durante un frame completo se descarta,
/// así la memoria queda acotada al texto visible sin vaciar todo de golpe.
/// Usa FxHash: la clave se consulta varias veces por cada texto visible y frame,
/// y no necesita resistencia a colisiones provocadas.
#[derive(Default)]
pub struct ShapeCache {
    current: FxHashMap<ShapeKey, Rc<ShapedText>>,
    previous: FxHashMap<ShapeKey, Rc<ShapedText>>,
}

impl ShapeCache {
    pub fn begin_frame(&mut self) {
        self.previous = std::mem::take(&mut self.current);
    }

    pub fn clear(&mut self) {
        self.current = FxHashMap::default();
        self.previous = FxHashMap::default();
    }

    /// Maqueta `leaf` con un ancho máximo opcional, reutilizando frames anteriores.
    pub fn shape(&mut self, text: &mut TextSystem, leaf: &TextLeaf, max_width: Option<f32>) -> Rc<ShapedText> {
        let width = max_width.filter(|_| leaf.wrap).map(|width| width.max(0.0).ceil());
        let key =
            ShapeKey { content: leaf.content.clone(), style_hash: leaf.style_hash(), width: width.map(f32::to_bits) };
        if let Some(shaped) = self.current.get(&key) {
            return shaped.clone();
        }
        if let Some(shaped) = self.previous.remove(&key) {
            self.current.insert(key, shaped.clone());
            return shaped;
        }
        let style = TextStyle {
            font_size: leaf.font_size,
            line_height: leaf.line_height,
            monospace: leaf.monospace,
            family: leaf.family,
            max_width: width,
            traits: leaf.traits,
        };
        let shaped = Rc::new(text.shape_rich(&leaf.content, &leaf.runs, &style));
        self.current.insert(key, shaped.clone());
        shaped
    }
}

fn measure(
    text: &mut TextSystem,
    cache: &mut ShapeCache,
    leaf: &TextLeaf,
    known: taffy::Size<Option<f32>>,
    available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
    let max_width = known.width.or(match available.width {
        AvailableSpace::Definite(width) => Some(width),
        // Ancho mínimo: cada palabra en su línea.
        AvailableSpace::MinContent => Some(0.0),
        AvailableSpace::MaxContent => None,
    });
    let shaped = cache.shape(text, leaf, max_width);
    taffy::Size {
        width: known.width.unwrap_or(shaped.size.width.0.ceil()),
        height: known.height.unwrap_or(shaped.size.height.0),
    }
}

/// Calcula el layout de `root` midiendo las hojas de texto bajo demanda.
pub fn compute_layout(
    frame: &mut FrameState,
    text: &mut TextSystem,
    cache: &mut ShapeCache,
    root: NodeId,
    available: taffy::Size<AvailableSpace>,
) -> Result<(), taffy::TaffyError> {
    frame.taffy.compute_layout_with_measure(root, available, |inputs, _node, context, style| {
        taffy::compute_leaf_layout(
            inputs,
            style,
            |_, _| 0.0,
            |known, available| match context.as_deref() {
                Some(leaf) => measure(text, cache, leaf, known, available),
                None => taffy::Size::ZERO,
            },
        )
    })
}