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.
docs.rs failed to build aura-gui-0.3.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

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.

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

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.

Familia Utilidades
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
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:

Receta Equivale a
.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:

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

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:

[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.

// 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(|cancel| fetch(cancel), |this, result, cx| this.on_fetched(result, cx));
self.search.run(cx, |_| slow_search(q), |this, hits, _| this.hits = hits);   // `Latest`: solo vale la última
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();
store.update(|json| json["theme"] = "Nord".into())?;

// 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.