Ir al contenido

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

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.

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 usa fn 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.

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]
▶ Pruébalo tú mismo

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.

eframe es el “framework de apps” oficial de egui: abre la ventana y ejecuta egui dentro de ella. Crea un proyecto y añádelo:

Terminal window
cargo new counter
cd 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:

  • CounterApp es tu estado. #[derive(Default)] hace que count empiece en 0.
  • run_native abre la ventana. La closure crea tu app una sola vez, dentro de un Box (mira Punteros inteligentes).
  • eframe llama a ui en cada fotograma. CentralPanel llena la ventana, y ui.horizontal pone 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):

Terminal window
cargo install create-tauri-app --locked
cargo create-tauri-app

Tambié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 Deserialize de serde, y el tipo de retorno tiene que implementar Serialize (mira JSON con Serde). En JavaScript, escribe los nombres de los argumentos en camelCase: un parámetro de Rust invoke_message se 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 Result para informar de errores: Ok resuelve la promesa de JavaScript y Err la rechaza, así que puedes manejarla con .catch(). Por ejemplo, fn load(path: String) -> Result<String, String>.
  • main.rs solo llama a run(). El código vive en lib.rs para 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 sigue la arquitectura Elm, que se llama así por el lenguaje Elm. Toda app tiene cuatro partes:

  1. Estado: un struct con todo lo que la app recuerda.
  2. Mensajes: un enum con todo lo que puede pasar (“se pulsó el botón +”).
  3. update: recibe el estado y un mensaje, y cambia el estado. Es el único lugar donde ocurren cambios.
  4. 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.

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 leche
1. [ ] Llamar al banco
> después de ClearDone:
Nueva: [Regar las plantas]
0. [ ] Llamar al banco
▶ Pruébalo tú mismo

Fí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.

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

  • Bloquear el hilo de la interfaz. Una descarga o un cálculo largo dentro de la función ui de egui o del update de 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. ui se ejecuta otra vez en cada fotograma, así que un let mut dentro 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, en lib.rs no marques los comandos como pub: la documentación de Tauri explica que eso provoca un error E0255 “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 carpeta examples del propio crate para la versión que usas.

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: 1
Increment -> Contador: 2
Decrement -> Contador: 1
Decrement -> Contador: 0
Decrement -> Contador: 0
Increment -> Contador: 1
Reset -> Contador: 0
▶ Pruébalo tú mismo

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

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)
▶ Pruébalo tú mismo

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.

  • 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, update y view, 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 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