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.
//! Fachada de una app de closure: `run_app("Título", |ui| ...)`.
//!
//! `Ui` junta lo que una pantalla necesita (tema, tamaño, entidades y estado) y ofrece los constructores y
//! componentes con nombres cortos. La closure se vuelve a ejecutar cuando cambia un `State` o un evento pide frame.

use crate::core::{EntityMap, Handle, Size};
use crate::view::any_element::AnyElement;
use crate::view::div::{Div, div};
use crate::view::element::{IntoElement, ParentElement};
use crate::view::media::Svg;
use crate::view::recipes::Recipes;
use crate::view::render::{Component, ViewContext};
use crate::view::state::{State, StateStore};
use crate::view::styled::Styled;
use crate::view::text_element::Text;
use crate::view::theme::Theme;
use crate::view::widget_store::WidgetStore;
use crate::widgets::{
    Button, IconName, InputChanged, TextInput, empty_state, field, icon, page_header, section, setting_row, switch,
};

struct BoundInput {
    state: State<String>,
}

pub struct Ui<'a> {
    pub(crate) theme: &'a Theme,
    pub(crate) viewport: Size,
    pub(crate) entities: &'a mut EntityMap,
    pub(crate) state: &'a mut StateStore,
    pub(crate) widgets: &'a mut WidgetStore,
}

impl<'a> Ui<'a> {
    pub(crate) fn new(
        theme: &'a Theme,
        viewport: Size,
        entities: &'a mut EntityMap,
        state: &'a mut StateStore,
        widgets: &'a mut WidgetStore,
    ) -> Self {
        Self { theme, viewport, entities, state, widgets }
    }

    /// Tema vigente (claro/oscuro, tokens, tipografía).
    pub fn theme(&self) -> &Theme {
        self.theme
    }

    /// Tamaño lógico de la ventana.
    pub fn viewport(&self) -> Size {
        self.viewport
    }

    /// Entidades de la app, para trabajo avanzado o para convivir con vistas existentes.
    pub fn entities(&mut self) -> &mut EntityMap {
        self.entities
    }

    /// Estado de la app por clave estable: `ui.state("clics", 0u32)`.
    ///
    /// La primera llamada crea el valor; las siguientes devuelven el mismo. El tipo debe coincidir con el de la
    /// primera llamada (si no, el error dice la clave y el tipo).
    pub fn state<T: 'static>(&mut self, key: &'static str, default: T) -> State<T> {
        self.state.get_or_insert(key, default)
    }

    /// Componente con estado propio, sin `impl`: recibe la parte mutable y un asa para los manejadores.
    ///
    /// ```ignore
    /// fn panel(ui: &mut Ui, state: &mut Panel, handle: State<Panel>) -> Div {
    ///     ui.column().child(state.title.clone())
    ///         .child(ui.button("Cerrar").on_click(handle.update(|p| p.open = false)))
    /// }
    ///
    /// ui.component("panel", Panel::new, panel);
    /// ```
    pub fn component<T: 'static, R>(
        &mut self,
        key: &'static str,
        init: impl FnOnce() -> T,
        build: impl FnOnce(&mut Ui, &mut T, State<T>) -> R,
    ) -> R {
        let state = self.state.get_or_insert_with(key, init);
        state.with_mut(|value| build(self, value, state.clone()))
    }

    /// Como [`Self::component`], pero la inicialización recibe `ui` (para crear entidades o leer el tema).
    ///
    /// Es la vía para migrar una app cuyo estado raíz necesita `EntityMap`:
    /// `ui.component_with("app", |ui| App::new(ui.entities()), app_view)`.
    pub fn component_with<T: 'static, R>(
        &mut self,
        key: &'static str,
        init: impl FnOnce(&mut Ui) -> T,
        build: impl FnOnce(&mut Ui, &mut T, State<T>) -> R,
    ) -> R {
        let state = match self.state.get::<T>(key) {
            Some(state) => state,
            None => {
                let value = init(self);
                self.state.insert(key, value)
            }
        };
        state.with_mut(|value| build(self, value, state.clone()))
    }

    /// Dibuja una vista existente como hija; sirve para migrar una app por partes.
    pub fn child<V: Component>(&mut self, handle: Handle<V>) -> AnyElement {
        let (theme, viewport) = (self.theme, self.viewport);
        self.entities
            .update(handle, |view, entity| view.view(&mut ViewContext::new(theme, viewport, entity)).into_element())
            .unwrap_or_else(|| div().into_element())
    }

    // ---------- Constructores ----------

    pub fn text(&self, content: impl Into<String>) -> Text {
        crate::view::text_element::text(content)
    }

    pub fn title(&self, content: impl Into<String>) -> Div {
        let content: String = content.into();
        div().heading().child(content)
    }

    pub fn caption(&self, content: impl Into<String>) -> Div {
        let content: String = content.into();
        div().caption().child(content)
    }

    pub fn button(&self, label: impl Into<String>) -> Button {
        Button::new(label)
    }

    pub fn icon(&self, name: IconName) -> Svg {
        icon(name)
    }

    pub fn page(&self) -> Div {
        div().page()
    }

    pub fn card(&self) -> Div {
        div().card()
    }

    pub fn row(&self) -> Div {
        div().flex_row().items_center()
    }

    pub fn column(&self) -> Div {
        div().flex_col()
    }

    pub fn spacer(&self) -> Div {
        div().flex_1()
    }

    // ---------- Componentes ----------

    pub fn section(&self, title: impl Into<String>, hint: impl Into<String>) -> Div {
        section(title, hint)
    }

    pub fn setting_row(&self, title: impl Into<String>, hint: impl Into<String>, control: impl IntoElement) -> Div {
        setting_row(title, hint, control)
    }

    pub fn page_header(&self, title: impl Into<String>, subtitle: impl Into<String>) -> Div {
        page_header(title, subtitle)
    }

    pub fn field(&self, label: impl Into<String>, control: impl IntoElement) -> Div {
        field(label, control)
    }

    pub fn empty_state(&self, title: impl Into<String>, hint: impl Into<String>) -> Div {
        empty_state(title, hint)
    }

    /// Campo de texto de una línea enlazado a `State<String>`.
    ///
    /// Crea el `TextInput` y su suscripción la primera vez; después conserva foco, cursor y selección. El estado
    /// se actualiza al escribir y, si la app lo cambia desde fuera, el campo se sincroniza.
    pub fn text_field(&mut self, label: impl Into<String>, state: &State<String>) -> Div {
        let label: String = label.into();
        let key = state.key();
        let input = match self.widgets.text_input(key) {
            Some(input) => input,
            None => {
                let focus = self.entities.focus.new_handle();
                let input = self.entities.insert(TextInput::new(focus).placeholder(label.clone()));
                let binding = self.entities.insert(BoundInput { state: state.clone() });
                self.entities.subscribe(input, binding, |binding, event: &InputChanged, _| {
                    binding.state.write(|text| *text = event.0.clone());
                });
                self.widgets.insert_text_input(key, input);
                input
            }
        };

        let text = state.get();
        self.entities.update(input, |input, _| {
            if input.text() != text {
                input.set_text(text);
            }
        });
        let element = self.child(input);
        field(label, element)
    }

    pub fn switch(&self, state: &State<bool>) -> AnyElement {
        switch(state.get(), state.key(), state.toggle())
    }
}