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.
//! Ventana lógica: ejecuta el pipeline de 3 fases y guarda el estado entre frames.

use crate::core::{Bounds, Edges, EntityMap, Handle, Modifiers, Pixels, Point, Size};
use crate::gpu::text::GlyphTarget;
use crate::gpu::{Scene, SpriteCache, TextSystem};
use crate::view::any_element::{AnyElement, reset_frame_arena};
use crate::view::context::{ElementContext, FrameResources, FrameState, MouseState};
use crate::view::element::IntoElement;
use crate::view::element_state::ElementStateMap;
use crate::view::keymap::Keymap;
use crate::view::overlay::{paint_deferred, prepaint_deferred};
use crate::view::render::{Component, ViewContext};
use crate::view::style::{Direction, Length, Sides, Style};
use crate::view::text_layout::{ShapeCache, compute_layout};
use crate::view::theme::Theme;
use std::time::Instant;
use taffy::AvailableSpace;

type RootBuilder = Box<dyn FnMut(&mut EntityMap, &Theme, Size) -> Option<AnyElement>>;

pub struct Window {
    pub entities: EntityMap,
    pub theme: Theme,
    pub keymap: Keymap,
    /// Tamaño de 1 `rem` en píxeles lógicos (ajuste `ui_font_size`).
    pub rem_size: f32,
    pub(crate) viewport: Size,
    pub(crate) scale: f32,
    /// Márgenes que tapa el sistema en móvil (barra de estado, notch, barra de gestos).
    safe_area: Edges,
    text: TextSystem,
    sprites: SpriteCache,
    shape_cache: ShapeCache,
    pub(crate) frame: FrameState,
    pub(crate) element_state: ElementStateMap,
    pub(crate) mouse: MouseState,
    /// Manejador de arrastre capturado al pulsar: sobrevive a que los hitboxes se
    /// reconstruyan (la lista virtual del chat cambia de filas mientras se arrastra).
    pub(crate) pressed_drag: Option<crate::view::interactivity::DragHandler>,
    pub(crate) modifiers: Modifiers,
    pub(crate) pending_keys: Vec<String>,
    pub(crate) last_click: Option<(Instant, Point, u32)>,
    pub(crate) tooltip: Option<crate::view::tooltip::TooltipHover>,
    pub(crate) drag: Option<crate::view::drag::ActiveDrag>,
    pub(crate) accessibility: bool,
    /// Tema pedido por una vista (que no ve la ventana); se aplica al empezar el siguiente frame.
    pub theme_request: std::rc::Rc<std::cell::RefCell<Option<Theme>>>,
    root: Option<RootBuilder>,
}

impl Window {
    /// Ventana lógica lista para dibujar: tema, tamaño, escala y los atajos de los widgets ya registrados.
    pub fn new(theme: Theme, viewport: Size, scale: f32) -> Self {
        let mut frame = FrameState::default();
        frame.dispatch.clear();
        // Los widgets con estado (campos de texto, selectores, menús) traen sus atajos puestos: una app
        // no tiene que acordarse de `bind_widget_keys` para que escribir, borrar o elegir funcione.
        let mut keymap = Keymap::default();
        crate::widgets::bind_widget_keys(&mut keymap);
        let entities = EntityMap::new();
        let mut sprites = SpriteCache::default();
        sprites.set_async_worker(entities.background.clone(), entities.foreground());
        Self {
            entities,
            theme,
            keymap,
            rem_size: crate::view::units::DEFAULT_REM,
            viewport,
            scale,
            safe_area: Edges::all(Pixels::ZERO),
            text: TextSystem::new(),
            sprites,
            shape_cache: ShapeCache::default(),
            frame,
            element_state: ElementStateMap::default(),
            mouse: MouseState::OUTSIDE,
            pressed_drag: None,
            modifiers: Modifiers::default(),
            pending_keys: Vec::new(),
            last_click: None,
            tooltip: None,
            drag: None,
            accessibility: false,
            theme_request: Default::default(),
            root: None,
        }
    }

    /// Monta la vista raíz. Ocupa el ancho útil de la ventana y hereda el tema; para ocupar también el alto,
    /// devuelve `.page()` (o `.size_full()`). El fondo de la ventana ya es el del tema.
    pub fn set_root<V: Component>(&mut self, view: V) -> Handle<V> {
        let handle = self.entities.insert(view);
        self.root = Some(Box::new(move |entities, theme, viewport| {
            entities
                .update(handle, |view, entity| view.view(&mut ViewContext::new(theme, viewport, entity)).into_element())
        }));
        handle
    }

    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
        self.modifiers = modifiers;
    }

    pub fn resize(&mut self, viewport: Size, scale: f32) {
        self.viewport = viewport;
        self.scale = scale;
    }

    /// Desplazamiento vertical actual del contenedor con scroll de identificador `id`.
    pub fn scroll_offset(&mut self, id: &str) -> f32 {
        self.element_state.get_or_default::<std::cell::Cell<crate::view::interactivity::ScrollState>>(id).get().offset_y
    }

    pub fn scene(&self) -> &Scene {
        &self.frame.scene
    }

    /// El contenido se coloca dentro de estos márgenes; el fondo de la ventana sigue cubriendo todo.
    pub fn set_safe_area(&mut self, insets: Edges) {
        self.safe_area = insets;
    }

    pub fn safe_area(&self) -> Edges {
        self.safe_area
    }

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

    pub fn mouse_position(&self) -> Point {
        self.mouse.position
    }

    /// Rectángulo del cursor de texto enfocado (para la ventana del IME).
    pub fn ime_area(&self) -> Option<Bounds> {
        self.frame.ime_area
    }

    /// `true` si alguna animación pidió otro frame.
    pub fn wants_animation_frame(&mut self) -> bool {
        self.frame.wants_animation_frame | self.entities.timers.take_animation_frame()
    }

    /// Momento del próximo temporizador; sin temporizadores la app puede dormir.
    pub fn next_deadline(&self) -> Option<Instant> {
        self.entities.timers.next_deadline()
    }

    /// Ejecuta temporizadores vencidos y resultados de hilos; `true` si hay que redibujar.
    pub fn run_pending(&mut self) -> bool {
        self.entities.run_pending(Instant::now());
        let redraw = self.finish(crate::view::interactivity::Effects::default());
        redraw || self.entities.is_dirty()
    }

    /// Fases: 1. RequestLayout (Taffy), 2. Prepaint (hitboxes y overlays), 3. Paint.
    pub fn draw(&mut self, glyphs: Option<GlyphTarget<'_>>) {
        if let Some(theme) = self.theme_request.borrow_mut().take() {
            self.theme = theme;
        }
        self.frame.begin();
        self.frame.access_enabled = self.accessibility;
        self.shape_cache.begin_frame();
        self.element_state.begin_frame();
        crate::view::units::set_rem_size(self.rem_size);
        self.entities.drag_type = self.active_drag_type();
        let mut tooltip = self.tooltip_element();
        let Some(build_root) = self.root.as_mut() else { return };
        let Some(mut element) = build_root(&mut self.entities, &self.theme, self.viewport) else { return };
        let focused = self.entities.focus.focused();
        let active_drag = self.active_drag_type();
        let mut drag_preview = self.drag_preview();

        let mut cx = ElementContext::new(FrameResources {
            theme: &self.theme,
            scale: self.scale,
            viewport: self.viewport,
            frame: &mut self.frame,
            text: &mut self.text,
            sprites: &mut self.sprites,
            shape_cache: &mut self.shape_cache,
            glyphs,
            element_state: &mut self.element_state,
            mouse: self.mouse,
            focused,
            active_drag,
        });
        let child = element.request_layout(&mut cx);
        let root_style = Style {
            width: Some(Length::Px(self.viewport.width)),
            height: Some(Length::Px(self.viewport.height)),
            direction: Some(Direction::Column),
            padding: Sides {
                top: Some(self.safe_area.top),
                right: Some(self.safe_area.right),
                bottom: Some(self.safe_area.bottom),
                left: Some(self.safe_area.left),
            },
            ..Default::default()
        };
        let root = cx.request_node(&root_style, &[child]);
        let available = taffy::Size {
            width: AvailableSpace::Definite(self.viewport.width.0),
            height: AvailableSpace::Definite(self.viewport.height.0),
        };
        if let Err(error) = compute_layout(cx.frame, cx.text, cx.shape_cache, root, available) {
            eprintln!("falló el cálculo de layout: {error}");
            return;
        }
        element.prepaint(&mut cx);
        if let Some(tooltip) = tooltip.as_mut() {
            crate::view::tooltip::prepaint_tooltip(tooltip, &mut cx);
        }
        if let Some(preview) = drag_preview.as_mut() {
            crate::view::tooltip::prepaint_tooltip(preview, &mut cx);
        }
        let mut overlays = prepaint_deferred(&mut cx, self.viewport);
        element.paint(&mut cx);
        paint_deferred(&mut cx, &mut overlays);
        drop(cx);
        drop(overlays);
        drop(tooltip);
        drop(drag_preview);
        drop(element);
        reset_frame_arena();

        let path = self.frame.dispatch.path_to_focus(focused);
        self.entities.focus.set_path(self.frame.dispatch.focus_path(&path));
        self.entities.take_dirty();
    }

    /// Libera cachés de texto, iconos y layout; se llama al minimizar u ocultar la ventana.
    pub fn shrink_caches(&mut self) {
        self.text.shrink_caches();
        self.sprites.clear();
        self.shape_cache.clear();
        self.frame.taffy.clear();
    }
}