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.
//! Contexto compartido por las tres fases: layout, prepaint y paint.

use crate::core::{Bounds, FocusId, Pixels, Point, Size};
use crate::gpu::text::GlyphTarget;
use crate::gpu::{Affine, ShapedText, SpriteCache, TextSystem};
use crate::view::element_state::ElementStateMap;
use crate::view::interactivity::{ScrollState, SharedScroll};
use crate::view::style::Style;
use crate::view::text_layout::{ShapeCache, TextLeaf};
use crate::view::theme::Theme;
use std::any::TypeId;
use std::rc::Rc;
use std::time::Instant;
use taffy::NodeId;

pub use crate::view::frame_state::{DEFAULT_LEADING, FrameState, InheritedText, MouseState};

/// Recursos de la ventana que los elementos usan mientras se dibuja un frame.
pub struct FrameResources<'a> {
    pub theme: &'a Theme,
    pub scale: f32,
    /// Tamaño de la ventana en px lógicos (para `md:`, `lg:`…).
    pub viewport: Size,
    pub frame: &'a mut FrameState,
    pub text: &'a mut TextSystem,
    pub sprites: &'a mut SpriteCache,
    pub shape_cache: &'a mut ShapeCache,
    pub glyphs: Option<GlyphTarget<'a>>,
    pub element_state: &'a mut ElementStateMap,
    pub mouse: MouseState,
    pub focused: Option<FocusId>,
    /// Tipo de la carga que se arrastra, si hay un arrastre en curso.
    pub active_drag: Option<TypeId>,
}

pub struct ElementContext<'a> {
    pub theme: &'a Theme,
    pub scale: f32,
    viewport: Size,
    /// Transformaciones activas durante el prepaint (la última es la vigente): llevan los hitboxes a pantalla.
    pub(crate) hit_transforms: Vec<Affine>,
    pub now: Instant,
    pub(crate) frame: &'a mut FrameState,
    pub(crate) text: &'a mut TextSystem,
    pub(crate) sprites: &'a mut SpriteCache,
    pub(crate) shape_cache: &'a mut ShapeCache,
    pub(crate) glyphs: Option<GlyphTarget<'a>>,
    pub(crate) element_state: &'a mut ElementStateMap,
    pub(crate) mouse: MouseState,
    pub(crate) focused: Option<FocusId>,
    pub(crate) active_drag: Option<TypeId>,
    origins: Vec<Point>,
    masks: Vec<Bounds>,
    text_styles: Vec<InheritedText>,
}

impl<'a> ElementContext<'a> {
    pub fn new(resources: FrameResources<'a>) -> Self {
        Self {
            theme: resources.theme,
            scale: resources.scale,
            viewport: resources.viewport,
            hit_transforms: Vec::new(),
            now: Instant::now(),
            frame: resources.frame,
            text: resources.text,
            sprites: resources.sprites,
            shape_cache: resources.shape_cache,
            glyphs: resources.glyphs,
            element_state: resources.element_state,
            mouse: resources.mouse,
            focused: resources.focused,
            active_drag: resources.active_drag,
            origins: vec![Point::default()],
            masks: Vec::new(),
            text_styles: vec![InheritedText::default()],
        }
    }

    // ---------- Layout ----------

    /// Registra un nodo en Taffy. Ante un error interno cae a un nodo vacío en vez de abortar.
    pub fn request_node(&mut self, style: &Style, children: &[NodeId]) -> NodeId {
        let taffy_style = crate::view::layout::to_taffy(style);
        let tree = &mut self.frame.taffy;
        match tree.new_with_children(taffy_style, children) {
            Ok(node) => node,
            Err(error) => {
                eprintln!("no se pudo crear el nodo de layout: {error}");
                tree.new_leaf(taffy::Style::default()).unwrap_or(NodeId::new(0))
            }
        }
    }

    /// Hoja de texto que Taffy mide durante el layout (permite el ajuste de línea).
    pub fn request_text_node(&mut self, style: &Style, leaf: TextLeaf) -> NodeId {
        let taffy_style = crate::view::layout::to_taffy(style);
        match self.frame.taffy.new_leaf_with_context(taffy_style, leaf) {
            Ok(node) => node,
            Err(error) => {
                eprintln!("no se pudo crear la hoja de texto: {error}");
                self.request_node(style, &[])
            }
        }
    }

    pub fn node_bounds(&self, node: NodeId) -> Bounds {
        let Ok(layout) = self.frame.taffy.layout(node) else { return Bounds::default() };
        let origin = self.origin() + Point::new(Pixels(layout.location.x), Pixels(layout.location.y));
        Bounds::new(origin, Size::new(Pixels(layout.size.width), Pixels(layout.size.height)))
    }

    pub fn node_content_size(&self, node: NodeId) -> Size {
        match self.frame.taffy.layout(node) {
            Ok(layout) => {
                let reach = layout.scrollable_overflow_rect;
                Size::new(Pixels(reach.right), Pixels(reach.bottom))
            }
            Err(_) => Size::default(),
        }
    }

    // ---------- Pilas de origen, recorte y texto ----------

    pub fn origin(&self) -> Point {
        self.origins.last().copied().unwrap_or_default()
    }

    pub fn with_origin<R>(&mut self, origin: Point, body: impl FnOnce(&mut Self) -> R) -> R {
        self.origins.push(origin);
        let result = body(self);
        self.origins.pop();
        result
    }

    pub fn mask(&self) -> Bounds {
        self.masks
            .last()
            .copied()
            .unwrap_or(Bounds::new(Point::new(Pixels(-1.0e6), Pixels(-1.0e6)), Size::new(Pixels(2.0e6), Pixels(2.0e6))))
    }

    pub fn with_mask<R>(&mut self, bounds: Bounds, body: impl FnOnce(&mut Self) -> R) -> R {
        let clipped = self.mask().intersect(&bounds);
        self.masks.push(clipped);
        let result = body(self);
        self.masks.pop();
        result
    }

    /// Los overlays se pintan fuera del recorte de su ancestro.
    pub(crate) fn without_mask<R>(&mut self, body: impl FnOnce(&mut Self) -> R) -> R {
        let saved = std::mem::take(&mut self.masks);
        let result = body(self);
        self.masks = saved;
        result
    }

    pub fn viewport(&self) -> Size {
        self.viewport
    }

    pub fn text_style(&self) -> InheritedText {
        self.text_styles.last().copied().unwrap_or_default()
    }

    pub fn with_text_style<R>(&mut self, style: &Style, body: impl FnOnce(&mut Self) -> R) -> R {
        let mut inherited = self.text_style();
        inherited.color = style.text_color.unwrap_or(inherited.color);
        inherited.size = style.font_size.unwrap_or(inherited.size);
        inherited.bold = style.font_bold.unwrap_or(inherited.bold);
        inherited.italic = style.italic.unwrap_or(inherited.italic);
        inherited.underline = style.underline.unwrap_or(inherited.underline);
        inherited.wrap = style.text_wrap.unwrap_or(inherited.wrap);
        if let Some(extra) = &style.extra {
            inherited.refine(&extra.text);
        }
        self.text_styles.push(inherited);
        let result = body(self);
        self.text_styles.pop();
        result
    }

    // ---------- Estado por elemento y ciclo de frame ----------

    /// Estado `S` que el elemento `id` conserva entre frames (ver `ElementStateMap`).
    pub fn element_state<S: Default + 'static>(&mut self, id: &str) -> Rc<S> {
        self.element_state.get_or_default(id)
    }

    pub fn scroll_state(&mut self, id: &str) -> SharedScroll {
        self.element_state::<std::cell::Cell<ScrollState>>(id)
    }

    pub fn set_ime_area(&mut self, bounds: Bounds) {
        self.frame.ime_area = Some(bounds);
    }

    /// Pide otro frame al terminar este (animaciones en curso).
    pub fn request_animation_frame(&mut self) {
        self.frame.wants_animation_frame = true;
    }

    // ---------- Texto ----------

    pub fn shape_leaf(&mut self, leaf: &TextLeaf, max_width: Option<f32>) -> Rc<ShapedText> {
        self.shape_cache.shape(self.text, leaf, max_width)
    }

    /// Texto de una línea sin ajuste, con la altura de línea estándar (1.4).
    pub fn shape(&mut self, content: &str, size: Pixels, monospace: bool) -> Rc<ShapedText> {
        self.shape_in(content, size, monospace, None)
    }

    /// Como `shape`, con una familia concreta (de `font_family`) si la hay.
    pub fn shape_in(
        &mut self,
        content: &str,
        size: Pixels,
        monospace: bool,
        family: Option<crate::gpu::FontFamily>,
    ) -> Rc<ShapedText> {
        let leaf = TextLeaf {
            content: Rc::from(content),
            runs: Rc::from([]),
            font_size: size.0,
            line_height: (size.0 * DEFAULT_LEADING).round(),
            monospace,
            family,
            wrap: false,
            traits: Default::default(),
        };
        self.shape_leaf(&leaf, None)
    }

    /// Familia instalada con ese nombre; la primera vez la busca y carga del sistema.
    pub fn font_family(&mut self, name: &str) -> Option<crate::gpu::FontFamily> {
        self.text.font_family(name)
    }
}