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.
//! Constructores visuales y widgets de alto nivel expuestos en la fachada `Ui`.

use crate::core::{Handle, Pixels};
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::state::State;
use crate::view::styled::Styled;
use crate::view::text_element::Text;
use crate::view::ui::Ui;
use crate::view::uniform_list::uniform_list;
use crate::widgets::{
    Button, IconName, InputChanged, InputSubmitted, TextInput, empty_state, field, icon, page_header, section,
    setting_row, switch,
};

struct BoundInput {
    state: State<String>,
}

struct SubmitBinding {
    action: Box<dyn Fn(String)>,
}

impl<'a> Ui<'a> {
    // ---------- Constructores básicos ----------

    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 y listas ----------

    /// Lista virtualizada: construye solo las filas visibles y conserva el scroll entre frames.
    pub fn list<R: IntoElement>(
        &mut self,
        key: &'static str,
        count: usize,
        row_height: Pixels,
        mut row: impl FnMut(&mut Ui, usize) -> R,
    ) -> AnyElement {
        let list = self.widgets.uniform_list(key);
        let mut ui = Ui {
            theme: self.theme,
            viewport: self.viewport,
            entities: &mut *self.entities,
            state: &mut *self.state,
            widgets: &mut *self.widgets,
            root_id: self.root_id,
            theme_request: self.theme_request,
        };
        uniform_list(&list, count, row_height, |range| range.map(|index| row(&mut ui, index).into_element()).collect())
            .into_element()
    }

    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)
    }

    fn ensure_text_input(&mut self, placeholder: &str, state: &State<String>) -> Handle<TextInput> {
        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(placeholder.to_owned()));
                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
            }
        };
        state.read(|text| {
            self.entities.update(input, |input, _| {
                if input.text() != text {
                    input.set_text(text.clone());
                }
            });
        });
        input
    }

    /// 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.
    pub fn text_field(&mut self, label: impl Into<String>, state: &State<String>) -> Div {
        let label: String = label.into();
        let input = self.ensure_text_input(&label, state);
        let element = self.child(input);
        field(label, element)
    }

    /// Como [`Self::text_field`], pero al pulsar `Enter` con texto no vacío (sin espacios a los lados) vacía el
    /// campo, entrega el texto a `on_submit` y redibuja automáticamente.
    pub fn text_field_submit(
        &mut self,
        label: impl Into<String>,
        state: &State<String>,
        on_submit: impl Fn(String) + 'static,
    ) -> Div {
        let label: String = label.into();
        let key = state.key();
        let input = self.ensure_text_input(&label, state);
        if !self.widgets.has_text_submit(key) {
            let state_clone = state.clone();
            let root_id = self.root_id;
            let submit = self.entities.insert(SubmitBinding { action: Box::new(on_submit) });
            self.entities.subscribe(input, submit, move |submit, _: &InputSubmitted, cx| {
                if let Some(trimmed) = state_clone.take_trimmed() {
                    cx.update(input, |input, _| input.set_text(String::new()));
                    (submit.action)(trimmed);
                    cx.mark_dirty(root_id);
                }
            });
            self.widgets.mark_text_submit(key);
        }
        let element = self.child(input);
        field(label, element)
    }

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