Apps con interfaz gráfica en Rust: Tauri, egui, Slint e Iced
Tarde o temprano vas a querer una ventana de verdad, con botones, y no solo texto en una terminal. ¿Se puede hacer eso en Rust? Sí. Las apps con interfaz gráfica (GUI, del inglés graphical user interface) hechas en Rust son rápidas, pequeñas y seguras, y hoy existen varios toolkits buenos. Pero el mundo de las GUI en Rust es más joven que, por ejemplo, C# con WPF o JavaScript con Electron, así que conviene conocer el panorama antes de elegir uno.
Esta página te da una visión honesta del tema, una tabla para ayudarte a elegir, la idea detrás del modo inmediato (immediate mode) y el modo retenido (retained mode), y un primer programa pequeño para los toolkits más populares.
¿Dónde puedes ejecutar el código? Una GUI necesita una ventana real, así que los programas de egui, Tauri, Slint e Iced son para tu propio ordenador, con cargo run. Los programas de egui e Iced de esta página se compilaron con cargo check usando las versiones actuales. Dos secciones simulan una GUI en Rust puro que imprime texto en vez de dibujar una ventana, así que funcionan en el navegador. Busca los botones “Pruébalo tú mismo”.
La situación real de las GUI en Rust
Sección titulada «La situación real de las GUI en Rust»La web de la comunidad Are we GUI yet? (en inglés) lo resume en una frase: “Las raíces aún no son profundas, pero las semillas están plantadas”. Sigue siendo una descripción justa:
- Lo bueno: la velocidad, las apps pequeñas y la seguridad de Rust. egui, Iced y Slint lo dibujan todo ellos mismos, así que un solo código funciona en Windows, macOS y Linux. Tauri te deja aprovechar lo que sabes de desarrollo web y hace apps mucho más pequeñas que Electron, porque usa el webview (el motor web) que ya viene en el sistema.
- Lo que aún está creciendo: no hay un toolkit “estándar” único, como Qt en C++ o WPF en C#. Varios crates populares todavía están en la versión 0.x, así que una actualización puede romper tu código. La accesibilidad, el texto de derecha a izquierda y el aspecto nativo cambian mucho de un toolkit a otro.
- La buena noticia: no necesitas encontrar el toolkit perfecto. Para una primera app, cualquiera de la tabla de abajo funciona bien.
Si quieres una comparación más larga, la encuesta de 2025 sobre bibliotecas de GUI en Rust (en inglés) prueba muchos toolkits uno por uno.
Qué puedes construir
Sección titulada «Qué puedes construir»Apps reales que puedes ver hoy mismo:
- Rerun Viewer, una herramienta para ver datos de robots y de visión por computadora, está hecha con egui. (Rerun también patrocina el desarrollo de egui).
- COSMIC, el entorno de escritorio de System76 para Linux, está hecho con Iced. Lo mismo el cliente de chat IRC Halloy.
- Herramientas de escritorio con una interfaz de estilo web: apps de notas, paneles de control (dashboards) y pequeñas herramientas para negocios encajan muy bien con Tauri.
- Pantallas en dispositivos: Slint también funciona en microcontroladores, así que es una buena opción para pantallas táctiles en máquinas y electrodomésticos.
¿Qué crate elegir?
Sección titulada «¿Qué crate elegir?»Las versiones son las últimas publicadas en octubre de 2026.
| Crate | Versión | Cómo escribes la interfaz | Funciona en | Elígelo si |
|---|---|---|---|---|
| Tauri | 2.12 (3.0 en alfa) | HTML, CSS y JavaScript para la pantalla; Rust para la lógica | Windows, macOS, Linux, Android, iOS | Sabes desarrollo web, o quieres una interfaz rica y con mucho estilo |
| egui + eframe | 0.37 | Rust puro, modo inmediato | Windows, macOS, Linux, web (Wasm) | Herramientas, paneles de depuración, editores, y el arranque más rápido en Rust puro |
| Slint | 1.18 | Su propio lenguaje de marcado .slint, más Rust para la lógica |
Escritorio, embebido, móvil, web | Una app pulida, o una pantalla en un dispositivo embebido |
| Iced | 0.14 | Rust puro, arquitectura Elm (estado, mensajes, update, view) | Windows, macOS, Linux, web | Te gusta el código limpio y predecible, con todos los cambios en un solo lugar |
| gtk-rs (gtk4) | 0.11 | Bindings de Rust para GTK 4, modo retenido | Sobre todo Linux; Windows y macOS funcionan con más configuración | Apps con aspecto nativo para el escritorio GNOME en Linux |
| Dioxus | 0.7 | Componentes al estilo de React con una macro rsx! |
Web, escritorio (webview), móvil | Sabes React y quieres un solo código para web y escritorio |
Unas notas honestas sobre licencias y madurez:
- Slint es gratis bajo la GPLv3, o gratis para apps de código cerrado de escritorio, móvil y web con su licencia libre de regalías (tienes que mostrar que usas Slint). Los productos embebidos de código cerrado necesitan una licencia de pago. Lee slint.dev/pricing (en inglés) antes de publicar tu app.
- egui, Iced y Dioxus siguen en 0.x. Espera pequeños cambios incompatibles en cada versión, y comprueba que un tutorial coincida con tu versión. Por ejemplo, los tutoriales antiguos de egui usan
fn update(&mut self, ctx: &egui::Context, ...), mientras que eframe 0.37 usafn ui(&mut self, ui: &mut egui::Ui, ...). - Tauri, Dioxus para escritorio y gtk-rs dependen de bibliotecas del sistema. Consulta Requisitos de cada plataforma más abajo.
Modo inmediato vs modo retenido
Sección titulada «Modo inmediato vs modo retenido»Los toolkits no se ponen de acuerdo en una gran pregunta: ¿quién recuerda los widgets (los botones, etiquetas y demás piezas de la interfaz)?
En el modo retenido (gtk-rs, Slint, la página web de Tauri y la mayoría de los toolkits de otros lenguajes), creas un botón una sola vez. El toolkit lo guarda en un árbol de widgets. Le asignas un callback (una función que se llama más tarde), y cuando el usuario hace clic, el toolkit llama a tu callback. Para cambiar el texto del botón después, necesitas una referencia (un handle) a ese botón.
En el modo inmediato (egui), no se guarda nada. Tu función de interfaz se ejecuta otra vez en cada fotograma (frame) y simplemente describe lo que hay que mostrar ahora mismo. Un botón se dibuja y se comprueba en una sola línea: if ui.button("+").clicked() { ... }. No hay callbacks ni handles. El estado de tu app vive en tu propio struct, y la interfaz siempre se dibuja a partir de él, así que la pantalla nunca puede desincronizarse de tus datos.
El precio: el toolkit coloca toda la interfaz en cada fotograma, así que una interfaz enorme gasta más CPU, y algunos diseños (como centrar una ventana cuyo tamaño aún no se conoce) son más difíciles. egui solo dibuja un fotograma nuevo cuando pasa algo, como un movimiento del ratón, así que una app en reposo casi no usa CPU.
Aquí tienes un toolkit de modo inmediato diminuto y de mentira, en Rust puro. Cada paso del bucle es un fotograma. El array clicks hace el papel del ratón:
// Un "Ui" de mentira que anota lo que se dibujó en este fotograma.struct Ui { clicked: Option<&'static str>, // el botón en el que hizo clic el ratón en este fotograma drawn: Vec<String>,}
impl Ui { fn label(&mut self, text: &str) { self.drawn.push(text.to_string()); }
// Dibuja un botón Y te dice al momento si se hizo clic en él. fn button(&mut self, text: &'static str) -> bool { self.drawn.push(format!("[{text}]")); self.clicked == Some(text) }}
// Tu código de interfaz: se llama otra vez en cada fotograma.fn draw(count: &mut i32, ui: &mut Ui) { ui.label(&format!("Contador: {count}")); if ui.button("+") { *count += 1; } if ui.button("Reiniciar") { *count = 0; }}
fn main() { let mut count = 0; // el estado de la app vive fuera del código de interfaz let clicks = [None, Some("+"), Some("+"), None, Some("Reiniciar"), None];
for (frame, click) in clicks.into_iter().enumerate() { let mut ui = Ui { clicked: click, drawn: Vec::new() }; draw(&mut count, &mut ui); println!("fotograma {frame}: {}", ui.drawn.join(" ")); }}Salida:
fotograma 0: Contador: 0 [+] [Reiniciar]fotograma 1: Contador: 0 [+] [Reiniciar]fotograma 2: Contador: 1 [+] [Reiniciar]fotograma 3: Contador: 2 [+] [Reiniciar]fotograma 4: Contador: 2 [+] [Reiniciar]fotograma 5: Contador: 0 [+] [Reiniciar]Fíjate en el fotograma 1: haces clic en “+”, pero la etiqueta sigue diciendo 0. La etiqueta se dibujó antes de que el botón notara el clic. El valor nuevo aparece un fotograma después. El egui de verdad se encarga de esto por ti: cualquier clic o tecla hace que dibuje el siguiente fotograma enseguida, así que nunca llegas a ver el número viejo.
Tu primera app con egui: un contador
Sección titulada «Tu primera app con egui: un contador»eframe es el “framework de apps” oficial de egui: abre la ventana y ejecuta egui dentro de ella. Crea un proyecto y añádelo:
cargo new countercd counter[dependencies]eframe = "0.37"Reemplaza src/main.rs por esto:
use eframe::egui;
fn main() -> eframe::Result { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([300.0, 160.0]), ..Default::default() }; eframe::run_native( "Counter", options, Box::new(|_cc| Ok(Box::<CounterApp>::default())), )}
#[derive(Default)]struct CounterApp { count: i32,}
impl eframe::App for CounterApp { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ui, |ui| { ui.heading(format!("Count: {}", self.count)); ui.horizontal(|ui| { if ui.button("-").clicked() { self.count -= 1; } if ui.button("+").clicked() { self.count += 1; } if ui.button("Reset").clicked() { self.count = 0; } }); }); }}Ejecútalo con cargo run. La primera compilación descarga y compila unos cuantos cientos de crates, así que dale unos minutos. Las siguientes son rápidas. Se abre una ventanita con “Count: 0” y tres botones.
Cómo encaja todo:
CounterAppes tu estado.#[derive(Default)]hace quecountempiece en 0.run_nativeabre la ventana. La closure crea tu app una sola vez, dentro de unBox(mira Punteros inteligentes).- eframe llama a
uien cada fotograma.CentralPanelllena la ventana, yui.horizontalpone los tres botones en una fila. - Cada
if ui.button(..).clicked()es el patrón de modo inmediato de la sección anterior: dibuja el botón, compruébalo y cambia el estado.
Por eso también tienes que guardar el estado en self, nunca en una variable local dentro de ui: un let mut count = 0; ahí volvería a empezar en 0 en cada fotograma.
Un comando de Tauri: Rust detrás de una página web
Sección titulada «Un comando de Tauri: Rust detrás de una página web»Una app de Tauri tiene dos partes. El frontend es una página web normal (HTML, CSS y JavaScript, o un framework como React o Svelte) que se ejecuta en el webview del sistema. El backend es tu código Rust. El frontend llama a funciones de Rust que se llaman comandos.
La forma más fácil de empezar es el generador de proyectos oficial. Te pide un nombre y una plantilla para el frontend (la plantilla “vanilla”, sin framework, es una buena primera elección):
cargo install create-tauri-app --lockedcargo create-tauri-appTambién existe npm create tauri-app@latest si ya usas Node.js. Luego entra en la carpeta nueva y arranca la app con npm install y npm run tauri dev (para las plantillas de npm), o con cargo install tauri-cli --version "^2.0.0" --locked y cargo tauri dev (para las plantillas que solo usan Rust). La herramienta te muestra los comandos correctos para lo que hayas elegido.
El archivo generado src-tauri/src/lib.rs contiene tu primer comando:
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/#[tauri::command]fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name)}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application");}Lo dejamos tal cual lo genera Tauri, comentario en inglés incluido. Y el frontend (src/main.js en la plantilla vanilla, aquí acortado) lo llama así:
const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });Qué está pasando:
#[tauri::command]es una macro de atributo (mira Macros procedurales). Escribe el código de unión que convierte la llamada de JavaScript en una llamada normal de Rust.- Los argumentos llegan como JSON, así que cada tipo de argumento tiene que implementar
Deserializede serde, y el tipo de retorno tiene que implementarSerialize(mira JSON con Serde). En JavaScript, escribe los nombres de los argumentos en camelCase: un parámetro de Rustinvoke_messagese convierte en{ invokeMessage: "..." }. generate_handler![greet]registra el comando. Si te olvidas de añadir aquí un comando nuevo, la llamada desde JavaScript falla.- Devuelve un
Resultpara informar de errores:Okresuelve la promesa de JavaScript yErrla rechaza, así que puedes manejarla con.catch(). Por ejemplo,fn load(path: String) -> Result<String, String>. main.rssolo llama arun(). El código vive enlib.rspara que la misma app también se pueda compilar para Android e iOS.
Slint: la interfaz en su propio lenguaje de marcado
Sección titulada «Slint: la interfaz en su propio lenguaje de marcado»Slint describe la pantalla en un pequeño lenguaje declarativo, algo así como HTML y CSS juntos. Tu código Rust se encarga de la lógica. Este es el “hola mundo” oficial. Añade slint = "1.18" debajo de [dependencies] y pon esto en src/main.rs:
slint::slint!{ export component HelloWorld { Text { text: "hello world"; color: green; } }}
fn main() { HelloWorld::new().unwrap().run().unwrap();}La macro slint! compila el marcado durante la compilación y crea por ti un tipo de Rust llamado HelloWorld. En una app de verdad, el marcado va en su propio archivo .slint. Este contador viene de la plantilla oficial de Slint para Rust (en inglés). El archivo ui/app-window.slint:
import { Button, VerticalBox } from "std-widgets.slint";
export component AppWindow inherits Window { in-out property <int> counter: 42; callback request-increase-value(); VerticalBox { Text { text: "Counter: \{root.counter}"; }
Button { text: "Increase value"; clicked => { root.request-increase-value(); } } }}Y la parte de Rust, en src/main.rs:
slint::include_modules!();
fn main() -> Result<(), Box<dyn std::error::Error>> { let ui = AppWindow::new()?;
let ui_handle = ui.as_weak(); ui.on_request_increase_value(move || { let ui = ui_handle.unwrap(); ui.set_counter(ui.get_counter() + 1); });
ui.run()?; Ok(())}Slint usa el modo retenido: la propiedad counter vive en la interfaz, y el botón llama a un callback. Slint genera get_counter, set_counter y on_request_increase_value a partir del marcado (los guiones se convierten en guiones bajos). La closure guarda un handle débil (as_weak()) para que la ventana y su propio callback no se mantengan vivos el uno al otro para siempre (mira Weak y Cow). Para usar un archivo .slint también necesitas slint-build = "1.18" debajo de [build-dependencies] y un build.rs de una sola línea: slint_build::compile("ui/app-window.slint").expect("Slint build failed");.
Iced y la arquitectura Elm
Sección titulada «Iced y la arquitectura Elm»Iced sigue la arquitectura Elm, que se llama así por el lenguaje Elm. Toda app tiene cuatro partes:
- Estado: un struct con todo lo que la app recuerda.
- Mensajes: un enum con todo lo que puede pasar (“se pulsó el botón +”).
- update: recibe el estado y un mensaje, y cambia el estado. Es el único lugar donde ocurren cambios.
- view: construye los widgets a partir del estado. Los widgets no cambian nada por sí mismos; solo envían mensajes.
Este es el contador oficial de Iced (añade iced = "0.14" a [dependencies]):
use iced::widget::{button, column, text, Column};use iced::Center;
fn main() -> iced::Result { iced::run(Counter::update, Counter::view)}
#[derive(Default)]struct Counter { value: i64,}
#[derive(Debug, Clone, Copy)]enum Message { Increment, Decrement,}
impl Counter { fn update(&mut self, message: Message) { match message { Message::Increment => self.value += 1, Message::Decrement => self.value -= 1, } }
fn view(&self) -> Column<'_, Message> { column![ button("Increment").on_press(Message::Increment), text(self.value).size(50), button("Decrement").on_press(Message::Decrement), ] .padding(20) .align_x(Center) }}on_press(Message::Increment) no ejecuta ningún código. Solo dice “cuando hagan clic, envía este mensaje”. Luego Iced llama a update con ese mensaje y vuelve a llamar a view para mostrar el estado nuevo. Como cada cambio pasa por un único match, es fácil ver (y probar) todo lo que tu app puede hacer. Es un buen uso de los enums y de match.
Prueba la arquitectura Elm en tu navegador
Sección titulada «Prueba la arquitectura Elm en tu navegador»No necesitas Iced para practicar la idea. Este modelo de lista de tareas tiene las mismas cuatro partes. view devuelve texto en vez de widgets, y una lista de mensajes hace el papel de lo que el usuario escribe y pulsa:
// 1. El estado: todo lo que la app necesita recordar.#[derive(Default)]struct App { draft: String, todos: Vec<Todo>,}
struct Todo { title: String, done: bool,}
// 2. Los mensajes: todo lo que puede pasar.#[derive(Debug)]enum Message { DraftChanged(String), AddPressed, Toggle(usize), ClearDone,}
impl App { // 3. update: el ÚNICO lugar donde cambia el estado. fn update(&mut self, message: Message) { match message { Message::DraftChanged(text) => self.draft = text, Message::AddPressed => { let title = self.draft.trim(); if !title.is_empty() { self.todos.push(Todo { title: title.to_string(), done: false }); } self.draft.clear(); } Message::Toggle(index) => { if let Some(todo) = self.todos.get_mut(index) { todo.done = !todo.done; } } Message::ClearDone => self.todos.retain(|todo| !todo.done), } }
// 4. view: convierte el estado en lo que ve el usuario (aquí, texto). fn view(&self) -> String { let mut screen = format!("Nueva: [{}]\n", self.draft); for (index, todo) in self.todos.iter().enumerate() { let mark = if todo.done { "x" } else { " " }; screen.push_str(&format!("{index}. [{mark}] {}\n", todo.title)); } screen }}
fn main() { let mut app = App::default();
// Imagina que esto viene de teclas pulsadas y clics en botones. let messages = vec![ Message::DraftChanged("Comprar leche".to_string()), Message::AddPressed, Message::DraftChanged("Llamar al banco".to_string()), Message::AddPressed, Message::DraftChanged("Regar las plantas".to_string()), Message::Toggle(0), ];
for message in messages { println!("> {message:?}"); app.update(message); } print!("{}", app.view());
app.update(Message::ClearDone); println!("> después de ClearDone:"); print!("{}", app.view());}Salida:
> DraftChanged("Comprar leche")> AddPressed> DraftChanged("Llamar al banco")> AddPressed> DraftChanged("Regar las plantas")> Toggle(0)Nueva: [Regar las plantas]0. [x] Comprar leche1. [ ] Llamar al banco> después de ClearDone:Nueva: [Regar las plantas]0. [ ] Llamar al bancoFíjate en que view nunca cambia nada, y update nunca imprime nada. Esa separación es todo el truco. Para convertir esto en una app de Iced de verdad, solo tendrías que reescribir view para que devuelva widgets (un text_input que envía DraftChanged, un button que envía AddPressed). El estado, los mensajes y update se quedan exactamente igual.
Requisitos de cada plataforma
Sección titulada «Requisitos de cada plataforma»Los crates de GUI hablan con tu sistema operativo, así que necesitas un poco más que Rust a secas. Primero prepara lo básico de Herramientas de Rust. Después:
| Sistema | egui/eframe, Iced, Slint | Tauri además necesita |
|---|---|---|
| Windows | Nada aparte de las herramientas de C++ de MSVC | WebView2. Ya viene con Windows 10 (versión 1803 y posteriores) y con Windows 11 |
| macOS | Las herramientas de línea de comandos de Xcode (xcode-select --install) |
Nada más para apps de escritorio; el Xcode completo para iOS |
| Linux (Debian/Ubuntu) | El README de eframe indica libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev |
webkit2gtk: libwebkit2gtk-4.1-dev y algunos paquetes más de la página de requisitos de Tauri (en inglés) |
Si una compilación en Linux falla con un mensaje de pkg-config sobre una biblioteca que falta, instala el paquete -dev de esa biblioteca y vuelve a compilar. gtk-rs necesita los archivos de desarrollo de GTK 4 (libgtk-4-dev en Ubuntu).
Errores comunes
Sección titulada «Errores comunes»- Bloquear el hilo de la interfaz. Una descarga o un cálculo largo dentro de la función
uide egui o delupdatede Iced congela toda la ventana hasta que termina. Haz el trabajo lento en otro hilo o como tarea async (mira Concurrencia y Async/Await), y envía el resultado de vuelta a la interfaz como un mensaje. - Guardar el estado de egui en variables locales.
uise ejecuta otra vez en cada fotograma, así que unlet mutdentro de ella empieza de cero cada vez. Pon el estado en el struct de tu app. - Un comando de Tauri que nunca se registra. Cada comando tiene que aparecer en
tauri::generate_handler![...]. Además, enlib.rsno marques los comandos comopub: la documentación de Tauri explica que eso provoca un errorE0255“defined multiple times” (definido varias veces). - Seguir un tutorial antiguo. Con crates 0.x, un ejemplo del año pasado puede no compilar. Compara la versión del tutorial con la de tu
Cargo.toml, y mira la carpetaexamplesdel propio crate para la versión que usas.
Práctica
Sección titulada «Práctica»1. Un contador que nunca baja de cero
Sección titulada «1. Un contador que nunca baja de cero»Escribe un contador al estilo Elm con los mensajes Increment, Decrement y Reset. El valor nunca debe bajar de 0, aunque el usuario siga pulsando “-”. Pasa una lista de mensajes por update e imprime view() después de cada uno.
Pista: usa un u32 y saturating_sub(1), que se detiene en 0 en vez de desbordarse (mira Desbordamiento de enteros).
Ver solución
#[derive(Default)]struct Counter { value: u32,}
#[derive(Debug)]enum Message { Increment, Decrement, Reset,}
impl Counter { fn update(&mut self, message: Message) { match message { Message::Increment => self.value += 1, Message::Decrement => self.value = self.value.saturating_sub(1), Message::Reset => self.value = 0, } }
fn view(&self) -> String { format!("Contador: {}", self.value) }}
fn main() { let mut counter = Counter::default(); let messages = [ Message::Increment, Message::Increment, Message::Decrement, Message::Decrement, Message::Decrement, Message::Increment, Message::Reset, ];
for message in messages { let name = format!("{message:?}"); counter.update(message); println!("{name:<9} -> {}", counter.view()); }}Salida:
Increment -> Contador: 1Increment -> Contador: 2Decrement -> Contador: 1Decrement -> Contador: 0Decrement -> Contador: 0Increment -> Contador: 1Reset -> Contador: 0La regla “nunca por debajo de cero” vive en un único lugar: update. Formateamos el nombre del mensaje antes de llamar a update, porque update recibe el mensaje por valor y lo mueve.
2. Añade un botón de deshacer
Sección titulada «2. Añade un botón de deshacer»Amplía tu contador con un mensaje Undo (deshacer). Cada cambio recuerda el valor anterior, y Undo vuelve un paso atrás. Undo sin historial no hace nada. Muestra en view cuántos pasos para deshacer quedan.
Pista: guarda un history: Vec<u32> en el estado. Haz push del valor viejo antes de cada cambio, y pop cuando llegue Undo.
Ver solución
#[derive(Default)]struct Counter { value: u32, history: Vec<u32>, // valores anteriores, el más reciente al final}
#[derive(Debug)]enum Message { Increment, Decrement, Reset, Undo,}
impl Counter { fn update(&mut self, message: Message) { let new_value = match message { Message::Increment => self.value + 1, Message::Decrement => self.value.saturating_sub(1), Message::Reset => 0, Message::Undo => { if let Some(previous) = self.history.pop() { self.value = previous; } return; } }; self.history.push(self.value); // primero recuerda el valor viejo self.value = new_value; }
fn view(&self) -> String { format!("Contador: {} (pasos para deshacer: {})", self.value, self.history.len()) }}
fn main() { let mut counter = Counter::default(); let messages = [ Message::Increment, Message::Increment, Message::Decrement, Message::Increment, Message::Reset, Message::Undo, Message::Undo, Message::Undo, ];
for message in messages { let name = format!("{message:?}"); counter.update(message); println!("{name:<9} -> {}", counter.view()); }}Salida:
Increment -> Contador: 1 (pasos para deshacer: 1)Increment -> Contador: 2 (pasos para deshacer: 2)Decrement -> Contador: 1 (pasos para deshacer: 3)Increment -> Contador: 2 (pasos para deshacer: 4)Reset -> Contador: 0 (pasos para deshacer: 5)Undo -> Contador: 2 (pasos para deshacer: 4)Undo -> Contador: 1 (pasos para deshacer: 3)Undo -> Contador: 2 (pasos para deshacer: 2)Añadir el deshacer fue fácil porque cada cambio ya pasa por update. En una app de Iced de verdad, solo añadirías un botón más que envía Message::Undo.
Resumen
Sección titulada «Resumen»- Las GUI en Rust funcionan bien hoy, pero todavía no hay un toolkit estándar único, y varios crates populares siguen en 0.x.
- Tauri combina un frontend web con un backend en Rust; egui es modo inmediato en Rust puro; Slint usa su propio lenguaje de marcado y también funciona en dispositivos embebidos; Iced usa la arquitectura Elm.
- El modo retenido guarda un árbol de widgets y llama a tus callbacks. El modo inmediato vuelve a dibujar la interfaz a partir de tu estado en cada fotograma, sin callbacks.
- La arquitectura Elm divide una app en estado, mensajes,
updateyview, lo que hace que los cambios sean fáciles de seguir y de probar. - Tauri necesita WebView2 en Windows y webkit2gtk en Linux. Revisa Herramientas de Rust para lo básico.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. En egui, ¿por qué el valor del contador tiene que vivir en el struct de tu app y no en una variable local dentro de ui?
2. En una app de Tauri, ¿qué hace #[tauri::command]?
3. En la arquitectura Elm que usa Iced, ¿dónde cambia el estado de la app?
4. ¿Qué necesita Tauri en Linux que egui no necesita?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Are we GUI yet? (en inglés): el resumen de la comunidad sobre los crates de GUI en Rust, con etiquetas como “immediate mode” y “reactive”, y noticias recientes.
- Understanding immediate mode — documentación de egui (en inglés): cómo funciona egui, con sus ventajas y desventajas, explicado por su autor.
- Calling Rust from the Frontend — Tauri (en inglés): todo sobre los comandos: argumentos, valores de retorno, errores, comandos async y estado.
- Documentación de Slint para Rust (en inglés): la API de Rust, la macro
slint!y cómo usar archivos.slintconslint-build. - El libro de Iced: Architecture (en inglés): una explicación amable del estado, los mensajes, update y view.