# Aura UI
Interfaces nativas aceleradas por GPU para **macOS, Windows, Linux, Android e iOS** con un solo código en Rust.
Estilo encadenado con los nombres de Tailwind, componentes tipo shadcn, sombras por capas y animaciones, pensado para
gastar poco: poca RAM, casi nada de CPU en reposo y binarios pequeños.
```bash
cargo add aura-gui
```
Un solo crate con un módulo por capa: `aura_ui::core` (entidades, tareas, geometría, color, animación y capa de
sistema), `::gpu` (renderizador), `::view` (elementos, estilo, recetas y layout), `::widgets` (componentes y
estructura de pantalla), `::app` (ventana y bucle) y `::ai` (agentes de IA; feature `ai`, activa por defecto:
`default-features = false` la quita).
```rust
use aura_ui::prelude::*;
fn main() {
run_app("Contador", |ui| {
let clics = ui.state("clics", 0u32);
ui.page().center().gap_4()
.child(ui.card().p_6().child(format!("Clics: {}", clics.get())))
.child(ui.button("Sumar").on_click(clics.update(|n| *n += 1)))
});
}
```
La app es una closure: no hay `impl`, ni `AppOptions`, ni `set_root`. `ui.state("clave", valor)` crea el estado
la primera vez y lo conserva; `update`, `set` y `toggle` devuelven manejadores que mutan y redibujan. `ui.page()`
ocupa la ventana y hereda el tema; `column![...]` y `row![...]` quitan los `.child(...)` repetidos.
## API de closure
- **Entrada**: `run_app("Título", |ui| ...)`; con tamaño o tema, `AppOptions::new(..).size(..).run_ui(|ui| ...)`.
- **Estado**: `let nombre = ui.state("nombre", String::new());` → `nombre.get()`, `nombre.update(..)`,
`nombre.set(..)`, `nombre.toggle()`.
- **Formularios**: `ui.text_field("Nombre", &nombre)` enlaza el campo con `State<String>` y conserva foco y cursor.
- **Componentes con estado**: `ui.component("panel", Panel::new, panel)`; `panel` es una función
`fn(ui: &mut Ui, state: &mut Panel, handle: State<Panel>) -> Div`. Sin `impl`.
- **Composición**: `column![...]`, `row![...]`, `ui.page()`, `ui.card()`, `ui.section()`, `ui.setting_row()`,
`ui.page_header()`, `ui.field()`, `ui.empty_state()`.
- **Convivencia**: `ui.child(handle)` dibuja una vista existente mientras se migra una app por partes.
## Tailwind
Los nombres son los de Tailwind en `snake_case` y todos son métodos de `Styled`: con `use aura_ui::prelude::*;` basta.
Cada uno solo fija un campo; no hay trabajo en tiempo de ejecución ni memoria extra hasta que se usan.
| Espaciado y tamaño (escala completa, 1 = 4 px) | `p_4 px_2 pt_0_5 m_8 mx_auto gap_x_2 w_64 h_1_2 size_10 min_w_0 max_w_full inset_0 top_px` |
| Flex | `flex_row flex_col_reverse flex_wrap grow shrink_0 flex_1 flex_auto items_baseline justify_evenly self_end content_between` |
| Cuadrícula | `grid_cols_3 grid_rows_2 col_span_2 col_start_2 row_span_3 grid_flow_col` + `gap_*` |
| Color | `blue(500) slate(900) rose(50)` del módulo `palette`, con `.with_alpha(0.5)`; degradados `bg_gradient_to_r(a, b)` |
| Texto | `text_xs … text_9xl font_thin … font_black text_center leading_tight tracking_wide truncate line_clamp_3 uppercase line_through font_mono` |
| Bordes | `border border_t_2 border_x_0 rounded_lg rounded_t_xl rounded_bl_full ring_2 ring_color(..)` |
| Efectos | `shadow_md glow opacity_50` |
| Transformaciones | `rotate_45 neg_rotate_12 scale_105 scale_x_50 translate_x_4 neg_translate_y_1_2 skew_x_6` (giran y escalan desde el centro; no cambian el layout) |
| Filtros | `blur_sm blur_md backdrop_blur_xl backdrop_blur_px(6.0)` |
| Apilado | `z_10 z_50 neg_z_10 z(7)` entre hermanos, también para el ratón |
| Overflow | `overflow_hidden overflow_x_scroll overflow_auto` |
| Estados | `hover active focus group_hover drag_over` |
| Variantes | `sm md lg xl xxl` (según el ancho de la ventana, primero móvil), `dark light` |
```rust
div()
.flex_col().gap_4().p_6().rounded_xl().border().border_color(palette::slate(200))
.md(|s| s.flex_row().items_center())
.dark(|s| s.bg(palette::slate(900)).border_color(palette::slate(700)))
.child(text("Título").text_2xl().font_semibold().tracking_tight().truncate())
.child(div().w_1_3().h_10().rounded_lg().bg_gradient_to_r(palette::blue(500), palette::indigo(600)))
```
**Matices**
- `backdrop_blur_*` difumina lo que hay detrás del elemento (cristal esmerilado). `blur_*` difumina el elemento y sus
hijos, pero se aplica sobre toda su caja: también suaviza el fondo que tenga debajo y no se extiende más allá del borde.
- Un desenfoque dentro de un elemento girado usa el rectángulo que envuelve al giro (no hay desenfoque girado).
- Sin GPU (el rasterizador por CPU) las transformaciones sin giro funcionan; un giro o una inclinación se pinta sin
girar. Las hitboxes de un elemento transformado son el rectángulo que envuelve a la figura.
- Un desenfoque pinta el frame en una textura propia y suelta su memoria al rato de no usarse.
**Todavía no existe**: `divide-*` y `space-*`, `cursor-*` más allá de los de `CursorStyle`, `select-none`,
`pointer-events-none`, `object-fit`, `mix-blend`, `sticky`, otros filtros (`brightness`, `grayscale`…), valores
arbitrarios (`w-[137px]`; usa `.w(px(137.0))`) y las variantes `disabled:` y `focus-within:`.
### Recetas
Las combinaciones que toda app repite tienen nombre y salen del tema. Son un trait, así que valen también para
`Button`, `Badge` y `Style`:
| `.page()` | pantalla completa, en columna, con fondo y texto del tema |
| `.card()` | superficie elevada, borde, `rounded_xl`, sombra suave y `p_4` |
| `.panel()` | superficie plana con borde tenue (barras laterales, docks) |
| `.hstack()` · `.vstack()` · `.center()` | `flex_row + items_center` · `flex_col` · centrado en ambos ejes |
| `.muted()` · `.caption()` | texto secundario · texto pequeño secundario |
| `.heading()` · `.subheading()` | título de pantalla · título de sección |
| `.scroll_y()` · `.flexible()` | scroll vertical que encoge · `flex_1` con `min_w_0` |
Para estructura hay funciones que devuelven un `Div` ya compuesto:
```rust
page_header("Ajustes", "Se guardan al instante")
section("Tema", "Cambia toda la aplicación")
setting_row("Animaciones", "Iconos vivos y transiciones", switch(on, "anim", handler))
field("Modelo", text_input)
empty_state("Sin resultados", "Prueba con otra búsqueda")
```
## Sombras y animaciones
```rust
div().shadow_lg() // presets xs, sm, md, lg, xl, 2xl
div().shadow(Shadows::one(BoxShadow::new(0., 4., 6.).spread(-1.)).with(BoxShadow::new(0., 12., 24.))) // capas
div().glow(Token::Accent, 16.0) // resplandor de color
let x = cx.transition("panel", if abierto { 180.0 } else { 0.0 }, Animation::new(ms(600)).easing(Easing::SPRING));
let t = cx.animate("pulso", Animation::new(ms(900)).ping_pong());
let c = cx.animate_between("fondo", Animation::new(ms(500)).easing(Easing::STANDARD), rojo, azul);
```
Curvas: `Linear`, `EaseIn`, `EaseOut`, `EaseInOut`, `CubicBezier(..)` (como CSS) y `Spring { bounce }`. Con retraso,
repetición y ida y vuelta. Una animación solo pide frames mientras corre.
## Eficiencia
Medido en macOS arm64 con el ejemplo `contador`, build release:
| Memoria (huella) | 25 MB |
| CPU en reposo | 0,00 s en 20 s |
| Binario | 7,0 MB (macOS) · 6,1 MB (iOS) · **4,2 MB** (iOS con `opt-level = "s"` y LTO completo) |
Cómo lo logra: el bucle duerme hasta que hay entrada, un temporizador o una animación; no pinta si no se ve; usa la GPU
de bajo consumo con un solo frame en cola; suelta cachés al pasar a segundo plano o ante un aviso de poca memoria; las
sombras son una sola primitiva analítica en el shader, sin pasadas de desenfoque.
Perfil recomendado para tus apps móviles:
```toml
[profile.release]
opt-level = "s"
lto = "fat"
codegen-units = 1
panic = "abort"
strip = "symbols"
```
## Menos código repetido
Todo viene de `use aura_ui::prelude::*;`, sin importar nada más.
Lo que la librería hace sola: la vista raíz ocupa el ancho y el área segura y el tema pinta el fondo y el color de
texto por defecto (`.page()` la estira también al alto); los atajos de los widgets (`TextInput`, `Picker`, menús) ya
están registrados; y cualquier cambio hecho dentro de un manejador redibuja (el `update` de la entidad marca la
ventana), así que un `listener` no necesita `cx.notify()`. Las recetas (`.page()`, `.card()`, `.hstack()`…) y las
piezas de estructura (`section`, `setting_row`…) quitan las cadenas de estilo que se copiaban de una pantalla a otra.
```rust
// Clics: sin `|this: &mut V, _: &ClickEvent, cx|` ni `cx.notify()` (se avisa solo)
.on_click(cx.click(|this, cx| this.pick(name.clone(), cx)))
.on_click(cx.toggle(|t| &mut t.menu_open)) // invierte un bool
.on_click(cx.set(|t| &mut t.tab, Tab::Audio)) // asigna un valor
.on_click(cx.fire(Closed)) // emite un evento a quien se suscriba
// Segundo plano: sin hilo propio, canal ni `sender.update`; vuelve a la vista y redibuja
cx.job_blocking("login", |cancel| wait_for_browser(cancel), |this, ok, cx| this.on_login(ok, cx));
// Configuración: JSON en ~/.config/<app> (APPDATA en Windows), escritura atómica
let store = Store::new("miapp", "prefs.json").unwrap();
let theme = store.load().str_or("theme", "Aura Dark").to_owned();
// Controles y animación que toda app acaba copiando
div().pressable() // mano + hover + active
tool_button(icon, "Deshacer", enabled, cx.click(|t, _| t.undo()))
chip("Alta", selected) // etiqueta pulsable
switch(on, "notifs", cx.toggle(|t| &mut t.notifs))
let t = cx.phase("carga", motion); // segundos; `wave(t, 2.0, 0.0)` da una onda de 0 a 1
let a = cx.fade_in("panel", 200, motion);
// Arranque: la vista directa, sin cierre ni `set_root`
AppOptions::new("Mi app").size(960.0, 640.0).run(MiVista)?;
```
Por qué cuesta poco: `cx.job` reutiliza el pool de la app (en vez de un hilo del sistema por tarea) y un trabajo
cancelado no llega a tocar la UI; guardar es un temporal más un renombrado; nada de esto despierta el bucle
cuando no hay trabajo.
## Migración a la API de closure
La forma nueva es `run_app` + `ui.state`, sin `impl`:
- **Entrada**: `run_app("Título", |ui| ...)` o `AppOptions::new(..).run_ui(|ui| ...)`.
- **Estado**: `ui.state("clave", valor)`; `update`, `set` y `toggle` mutan y redibujan.
- **Formularios**: `ui.text_field("Nombre", &estado)` enlaza el campo y conserva foco/cursor.
- **Componentes**: `ui.component("panel", Panel::new, panel)` para separar una app en funciones con estado propio.
- **Macros**: `column![...]` y `row![...]` sustituyen las cadenas de `.child(...)`.
Para apps que ya tienen estado en entidades (`Handle`/`ViewContext`) existe el puente
`fn panel_view(this: &mut Panel, cx: &mut ViewContext<Panel>) -> AnyElement { … }` +
`aura_ui::component!(Panel, panel_view)`: tampoco se escribe `impl`, y las apps pueden migrarse después a
`ui.component`. Los traits públicos `View` y `Render` ya no existen; `component!` es la vía de compatibilidad
para las vistas con entidades. Los atajos de `TextInput`, `Picker` y menús ya se registran al crear la ventana.
## Móvil
Ver `docs/movil.md` del repositorio. Resumen: iOS compila para `aarch64-apple-ios` y el simulador; Android
para `aarch64-linux-android` con NativeActivity. Toque, deslizar con inercia, pulsación larga, teclado del sistema y
área segura están implementados. En iOS, añade el manifiesto UIScene al `Info.plist` (plantilla en `docs/ios/Info.plist`;
lo exige el SDK de iOS 27). Probado en el simulador de iPhone; **aún no en un iPhone ni un Android físicos.**
## Licencia
Aura UI tiene **doble licencia**, a tu elección (crates.io muestra la primera; la segunda va incluida en el paquete):
- **AGPL-3.0-only** (`LICENSE-AGPL`, <https://www.gnu.org/licenses/agpl-3.0.html>): gratis. Si la usas, tu app completa
debe publicarse bajo AGPL-3.0 con su código fuente.
- **Licencia comercial** (`LICENSE-COMMERCIAL.md`): para apps de código cerrado. Regalías del **10 % de los ingresos
brutos** de cada producto que use Aura UI. Para obtenerla escribe a <camiloghs1@gmail.com>.
Si no tienes la licencia comercial, aplica la AGPL-3.0. Los iconos de `src/widgets/svg` son de Lucide (ISC), con su
aviso en `src/widgets/svg/LICENSE-lucide`.
Copyright (C) 2026 Camilo Grajales Holguín.