Salta ai contenuti

App con interfaccia grafica in Rust: Tauri, egui, Slint e Iced

Prima o poi vorrai una vera finestra con dei pulsanti, non solo testo in un terminale. Si può fare in Rust? Sì. Le app con interfaccia grafica (GUI) in Rust sono veloci, leggere e sicure, e oggi esistono diversi buoni toolkit. Però il mondo delle GUI in Rust è più giovane di, per esempio, C# con WPF o JavaScript con Electron, quindi conviene conoscere il panorama prima di sceglierne uno.

Questa pagina ti dà una panoramica onesta, una tabella che ti aiuta a scegliere, l’idea dietro immediate mode e retained mode, e un primo piccolo programma per i toolkit più popolari.

Dove puoi eseguire il codice? Una GUI ha bisogno di una finestra vera, quindi i programmi con egui, Tauri, Slint e Iced sono da provare sul tuo computer con cargo run. I programmi egui e Iced di questa pagina sono stati compilati con cargo check sulle versioni attuali. Due sezioni simulano una GUI in Rust puro che stampa testo invece di disegnare una finestra, quindi girano nel browser. Cerca i pulsanti «Provalo tu».

Il sito della community Are we GUI yet? (in inglese) lo riassume in una riga: «The roots aren’t deep but the seeds are planted», cioè «le radici non sono profonde, ma i semi sono stati piantati». È ancora una descrizione giusta:

  • Ottimo: velocità, app leggere e la sicurezza di Rust. egui, Iced e Slint disegnano tutto da soli, quindi un solo codice gira su Windows, macOS e Linux. Tauri ti fa riusare le competenze web e mantiene l’app molto più piccola di Electron, perché usa la webview già presente nel sistema.
  • Ancora in crescita: non c’è un unico toolkit «standard» come Qt in C++ o WPF in C#. Diversi crate popolari sono ancora alla versione 0.x, quindi un aggiornamento può rompere il tuo codice. Accessibilità, testo da destra a sinistra e aspetto nativo cambiano da un toolkit all’altro.
  • La buona notizia: non devi trovare il toolkit perfetto. Per una prima app, qualsiasi toolkit della tabella qui sotto va benissimo.

Per un confronto più lungo, il 2025 survey of Rust GUI libraries (in inglese) prova tanti toolkit uno per uno.

App vere che puoi guardare già oggi:

  • Il Rerun Viewer, uno strumento per esaminare i dati di robot e visione artificiale, è fatto con egui. (Rerun sponsorizza anche lo sviluppo di egui.)
  • COSMIC, l’ambiente desktop di System76 per Linux, è fatto con Iced. Lo stesso vale per il client di chat IRC Halloy.
  • Strumenti desktop con un’interfaccia in stile web: app per appunti, dashboard e piccoli strumenti aziendali sono spesso un buon caso per Tauri.
  • Schermi su dispositivi: Slint gira anche sui microcontrollori, quindi è adatto ai touch screen di macchine ed elettrodomestici.

Le versioni sono le ultime uscite a ottobre 2026.

Crate Versione Come scrivi l’interfaccia Gira su Sceglilo quando
Tauri 2.12 (3.0 in alpha) HTML, CSS e JavaScript per lo schermo; Rust per la logica Windows, macOS, Linux, Android, iOS Conosci lo sviluppo web, o vuoi un’interfaccia ricca e curata nello stile
egui + eframe 0.37 Rust puro, immediate mode Windows, macOS, Linux, web (Wasm) Tool, pannelli di debug, editor, e la partenza più rapida in Rust puro
Slint 1.18 Il suo markup .slint, più Rust per la logica Desktop, embedded, mobile, web Un’app rifinita, o uno schermo su un dispositivo embedded
Iced 0.14 Rust puro, architettura Elm (stato, messaggi, update, view) Windows, macOS, Linux, web Ti piace un codice pulito e prevedibile, con tutte le modifiche in un solo posto
gtk-rs (gtk4) 0.11 Binding Rust per GTK 4, retained mode Prima di tutto Linux; Windows e macOS funzionano con più configurazione App dall’aspetto nativo per il desktop GNOME su Linux
Dioxus 0.7 Componenti in stile React con una macro rsx! Web, desktop (webview), mobile Conosci React e vuoi un solo codice per web e desktop

Qualche nota onesta su licenze e maturità:

  • Slint è gratuito con la GPLv3, oppure gratuito per app desktop, mobile e web a codice chiuso con la sua licenza royalty-free (devi mostrare che usi Slint). I prodotti embedded a codice chiuso richiedono una licenza a pagamento. Leggi slint.dev/pricing (in inglese) prima di pubblicare.
  • egui, Iced e Dioxus sono ancora alla 0.x. Aspettati piccole modifiche incompatibili a ogni release, e controlla che un tutorial corrisponda alla tua versione. Per esempio, i tutorial di egui più vecchi usano fn update(&mut self, ctx: &egui::Context, ...), mentre eframe 0.37 usa fn ui(&mut self, ui: &mut egui::Ui, ...).
  • Tauri, Dioxus desktop e gtk-rs dipendono da librerie di sistema. Vedi Prerequisiti per ogni sistema operativo più sotto.

I toolkit non sono d’accordo su una grande domanda: chi si ricorda dei widget?

In retained mode (gtk-rs, Slint, la pagina web di Tauri e la maggior parte dei toolkit in altri linguaggi) crei un pulsante una volta sola. Il toolkit lo conserva in un albero di widget. Colleghi una callback e, quando l’utente fa clic, il toolkit chiama la tua callback. Per cambiare più tardi il testo del pulsante, ti serve un riferimento (handle) a quel pulsante.

In immediate mode (egui) non si conserva niente. La tua funzione di interfaccia viene eseguita di nuovo a ogni frame e descrive semplicemente cosa mostrare in questo momento. Un pulsante si disegna e si controlla in una riga: if ui.button("+").clicked() { ... }. Niente callback e niente handle. Lo stato dell’app vive nella tua struct, e l’interfaccia viene sempre disegnata a partire da lì, quindi lo schermo non può mai andare fuori sincrono con i tuoi dati.

Il prezzo: il toolkit ricalcola il layout di tutta l’interfaccia a ogni frame, quindi un’interfaccia enorme consuma più CPU, e alcuni layout (come centrare una finestra di cui non si conosce ancora la dimensione) sono più difficili. egui disegna un nuovo frame solo quando succede qualcosa, come un movimento del mouse, quindi un’app ferma non usa quasi CPU.

Ecco un minuscolo toolkit immediate mode finto, in Rust puro. Ogni passo del ciclo è un frame. L’array clicks fa la parte del mouse:

// Una "Ui" finta che registra cosa è stato disegnato in questo frame.
struct Ui {
clicked: Option<&'static str>, // il pulsante cliccato dal mouse in questo frame
drawn: Vec<String>,
}
impl Ui {
fn label(&mut self, text: &str) {
self.drawn.push(text.to_string());
}
// Disegna un pulsante E ti dice subito se è stato cliccato.
fn button(&mut self, text: &'static str) -> bool {
self.drawn.push(format!("[{text}]"));
self.clicked == Some(text)
}
}
// Il tuo codice di interfaccia: viene richiamato a ogni frame.
fn draw(count: &mut i32, ui: &mut Ui) {
ui.label(&format!("Conteggio: {count}"));
if ui.button("+") {
*count += 1;
}
if ui.button("Reset") {
*count = 0;
}
}
fn main() {
let mut count = 0; // lo stato dell'app vive fuori dal codice di interfaccia
let clicks = [None, Some("+"), Some("+"), None, Some("Reset"), None];
for (frame, click) in clicks.into_iter().enumerate() {
let mut ui = Ui { clicked: click, drawn: Vec::new() };
draw(&mut count, &mut ui);
println!("frame {frame}: {}", ui.drawn.join(" "));
}
}

Output:

frame 0: Conteggio: 0 [+] [Reset]
frame 1: Conteggio: 0 [+] [Reset]
frame 2: Conteggio: 1 [+] [Reset]
frame 3: Conteggio: 2 [+] [Reset]
frame 4: Conteggio: 2 [+] [Reset]
frame 5: Conteggio: 0 [+] [Reset]
▶ Provalo tu

Guarda il frame 1: fai clic su «+», ma l’etichetta dice ancora 0. L’etichetta è stata disegnata prima che il pulsante notasse il clic. Il nuovo valore compare un frame dopo. Il vero egui lo gestisce per te: ogni clic o tasto premuto gli fa disegnare subito il frame successivo, quindi non vedi mai il numero vecchio.

eframe è il «framework per app» ufficiale di egui: apre la finestra e ci fa girare dentro egui. Crea un progetto e aggiungilo:

Terminal window
cargo new counter
cd counter
[dependencies]
eframe = "0.37"

Sostituisci src/main.rs con questo:

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

Eseguilo con cargo run. La prima build scarica e compila qualche centinaio di crate, quindi dagli qualche minuto. Le build successive sono veloci. Si apre una piccola finestra con «Count: 0» e tre pulsanti.

Come si incastrano i pezzi:

  • CounterApp è il tuo stato. #[derive(Default)] fa partire count da 0.
  • run_native apre la finestra. La closure crea la tua app una volta sola, dentro un Box (vedi Smart pointer).
  • eframe chiama ui a ogni frame. CentralPanel riempie la finestra, e ui.horizontal mette i tre pulsanti in fila.
  • Ogni if ui.button(..).clicked() è il pattern immediate mode della sezione precedente: disegna il pulsante, controllalo, cambia lo stato.

Ecco anche perché devi tenere lo stato in self, mai in una variabile locale dentro ui: un let mut count = 0; lì ripartirebbe da 0 a ogni frame.

Un’app Tauri ha due parti. Il frontend è una normale pagina web (HTML, CSS e JavaScript, oppure un framework come React o Svelte) che gira nella webview del sistema. Il backend è il tuo codice Rust. Il frontend chiama funzioni Rust chiamate comandi (command).

Il modo più facile per iniziare è il generatore di progetti ufficiale. Ti chiede un nome e un template per il frontend (il template «vanilla», senza framework, è una buona prima scelta):

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

C’è anche npm create tauri-app@latest se usi già Node.js. Poi entra nella nuova cartella e avvia l’app con npm install e npm run tauri dev (per i template npm), oppure con cargo install tauri-cli --version "^2.0.0" --locked e cargo tauri dev (per i template solo Rust). Lo strumento ti stampa i comandi giusti per la tua scelta.

Il file generato src-tauri/src/lib.rs contiene il tuo primo 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");
}

E il frontend (src/main.js nel template vanilla, qui accorciato) lo chiama così:

const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });

Cosa succede:

  • #[tauri::command] è una macro di attributo (vedi Macro procedurali). Scrive il codice di collegamento che trasforma la chiamata JavaScript in una normale chiamata Rust.
  • Gli argomenti arrivano come JSON, quindi ogni tipo di argomento deve implementare Deserialize di serde, e il tipo restituito deve implementare Serialize (vedi JSON con Serde). In JavaScript scrivi i nomi degli argomenti in camelCase: un parametro Rust invoke_message diventa { invokeMessage: "..." }.
  • generate_handler![greet] registra il comando. Se ti dimentichi di aggiungere qui un nuovo comando, la chiamata da JavaScript fallisce.
  • Restituisci un Result per segnalare gli errori: Ok risolve la promise JavaScript ed Err la rifiuta, così puoi gestirlo con .catch(). Per esempio, fn load(path: String) -> Result<String, String>.
  • main.rs si limita a chiamare run(). Il codice sta in lib.rs così la stessa app si può compilare anche per Android e iOS.

Slint: l’interfaccia in un suo linguaggio di markup

Sezione intitolata “Slint: l’interfaccia in un suo linguaggio di markup”

Slint descrive lo schermo in un piccolo linguaggio dichiarativo, un po’ come HTML e CSS messi insieme. Il tuo codice Rust si occupa della logica. Ecco l’hello world ufficiale. Aggiungi slint = "1.18" sotto [dependencies] e metti questo in 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 il markup durante la build e crea per te un tipo Rust HelloWorld. In un’app vera il markup va in un suo file .slint. Questo contatore viene dal template Rust ufficiale di Slint (in inglese). Il file 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();
}
}
}
}

E la parte Rust in 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 è retained mode: la proprietà counter vive nell’interfaccia, e il pulsante chiama una callback. Slint genera get_counter, set_counter e on_request_increase_value dal markup (i trattini diventano underscore). La closure tiene un handle debole (as_weak()), così la finestra e la sua callback non si tengono in vita a vicenda per sempre (vedi Weak e Cow). Per un file .slint ti servono anche slint-build = "1.18" sotto [build-dependencies] e un build.rs di una riga: slint_build::compile("ui/app-window.slint").expect("Slint build failed");.

Iced segue l’architettura Elm, che prende il nome dal linguaggio Elm. Ogni app ha quattro parti:

  1. Stato: una struct con tutto quello che l’app ricorda.
  2. Messaggi: un enum di tutto quello che può succedere («è stato premuto il pulsante +»).
  3. update: prende lo stato e un messaggio, e cambia lo stato. È l’unico posto dove avvengono modifiche.
  4. view: costruisce i widget a partire dallo stato. I widget non cambiano niente da soli; mandano solo messaggi.

Ecco il contatore ufficiale di Iced (aggiungi 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) non esegue nessun codice. Dice solo «quando vieni cliccato, manda questo messaggio». Iced poi chiama update con quel messaggio e chiama di nuovo view per mostrare il nuovo stato. Siccome ogni modifica passa da un unico match, è facile vedere (e testare) tutto quello che la tua app sa fare. È un ottimo uso di enum e match.

Non ti serve Iced per esercitarti con l’idea. Questo modello di lista di cose da fare ha le stesse quattro parti. view restituisce testo invece di widget, e una lista di messaggi fa le veci dell’utente che scrive e clicca:

// 1. Lo stato: tutto quello che l'app deve ricordare.
#[derive(Default)]
struct App {
draft: String,
todos: Vec<Todo>,
}
struct Todo {
title: String,
done: bool,
}
// 2. I messaggi: tutto quello che può succedere.
#[derive(Debug)]
enum Message {
DraftChanged(String),
AddPressed,
Toggle(usize),
ClearDone,
}
impl App {
// 3. update: l'UNICO posto in cui cambia lo stato.
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: trasforma lo stato in quello che vede l'utente (qui, testo).
fn view(&self) -> String {
let mut screen = format!("Nuova: [{}]\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();
// Facciamo finta che arrivino da tasti premuti e clic sui pulsanti.
let messages = vec![
Message::DraftChanged("Comprare il latte".to_string()),
Message::AddPressed,
Message::DraftChanged("Chiamare la banca".to_string()),
Message::AddPressed,
Message::DraftChanged("Annaffiare le piante".to_string()),
Message::Toggle(0),
];
for message in messages {
println!("> {message:?}");
app.update(message);
}
print!("{}", app.view());
app.update(Message::ClearDone);
println!("> dopo ClearDone:");
print!("{}", app.view());
}

Output:

> DraftChanged("Comprare il latte")
> AddPressed
> DraftChanged("Chiamare la banca")
> AddPressed
> DraftChanged("Annaffiare le piante")
> Toggle(0)
Nuova: [Annaffiare le piante]
0. [x] Comprare il latte
1. [ ] Chiamare la banca
> dopo ClearDone:
Nuova: [Annaffiare le piante]
0. [ ] Chiamare la banca
▶ Provalo tu

Nota che view non cambia mai niente, e update non stampa mai niente. Tutto il trucco sta in questa separazione. Per trasformarlo in una vera app Iced, dovresti solo riscrivere view in modo che restituisca widget (un text_input che manda DraftChanged, un button che manda AddPressed). Lo stato, i messaggi e update restano identici.

I crate per le GUI parlano con il sistema operativo, quindi ti serve qualcosa in più del semplice Rust. Prima prepara le basi da Strumenti e configurazione. Poi:

Sistema egui/eframe, Iced, Slint Tauri richiede anche
Windows Niente oltre agli strumenti C++ di MSVC WebView2. È già incluso in Windows 10 (versione 1803 e successive) e in Windows 11
macOS Gli strumenti da riga di comando di Xcode (xcode-select --install) Nient’altro per le app desktop; Xcode completo per iOS
Linux (Debian/Ubuntu) Il README di eframe elenca libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev webkit2gtk: libwebkit2gtk-4.1-dev più qualche altro pacchetto dalla pagina dei prerequisiti di Tauri (in inglese)

Se una build su Linux fallisce con un messaggio di pkg-config su una libreria mancante, installa il pacchetto -dev di quella libreria e ricompila. gtk-rs ha bisogno dei file di sviluppo di GTK 4 (libgtk-4-dev su Ubuntu).

  • Bloccare il thread dell’interfaccia. Un download o un calcolo lungo dentro la funzione ui di egui o in un update di Iced congela tutta la finestra finché non finisce. Fai il lavoro lento su un altro thread o in un task async (vedi Concorrenza e Async/Await), e rimanda il risultato all’interfaccia come messaggio.
  • Tenere lo stato di egui in variabili locali. ui viene eseguita di nuovo a ogni frame, quindi un let mut lì dentro riparte da capo ogni volta. Metti lo stato nella struct della tua app.
  • Un comando Tauri mai registrato. Ogni comando deve comparire in tauri::generate_handler![...]. Inoltre, in lib.rs, non segnare i comandi come pub: la documentazione di Tauri spiega che questo causa un errore E0255 «defined multiple times».
  • Seguire un tutorial vecchio. Con i crate 0.x, un esempio dell’anno scorso potrebbe non compilare. Confronta la versione del tutorial con il tuo Cargo.toml, e guarda la cartella examples del crate per la versione che usi.

Scrivi un contatore in stile Elm con i messaggi Increment, Decrement e Reset. Il valore non deve mai scendere sotto 0, anche se l’utente continua a premere «-». Fai passare una lista di messaggi da update e stampa view() dopo ognuno.

Suggerimento: usa un u32 e saturating_sub(1), che si ferma a 0 invece di andare in overflow (vedi Overflow degli interi).

Mostra la soluzione
#[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!("Conteggio: {}", 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());
}
}

Output:

Increment -> Conteggio: 1
Increment -> Conteggio: 2
Decrement -> Conteggio: 1
Decrement -> Conteggio: 0
Decrement -> Conteggio: 0
Increment -> Conteggio: 1
Reset -> Conteggio: 0
▶ Provalo tu

La regola «mai sotto zero» vive in un solo posto, update. Formattiamo il nome del messaggio prima di chiamare update, perché update prende il messaggio per valore e lo sposta (move).

Estendi il tuo contatore con un messaggio Undo. Ogni modifica si ricorda il valore precedente, e Undo torna indietro di un passo. Undo senza cronologia non fa niente. Mostra in view quanti passi di annullamento restano.

Suggerimento: tieni nello stato una history: Vec<u32>. Fai push del vecchio valore prima di ogni modifica, e pop quando arriva Undo.

Mostra la soluzione
#[derive(Default)]
struct Counter {
value: u32,
history: Vec<u32>, // valori precedenti, il più recente in fondo
}
#[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); // prima ricorda il vecchio valore
self.value = new_value;
}
fn view(&self) -> String {
format!("Conteggio: {} (annullamenti: {})", 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());
}
}

Output:

Increment -> Conteggio: 1 (annullamenti: 1)
Increment -> Conteggio: 2 (annullamenti: 2)
Decrement -> Conteggio: 1 (annullamenti: 3)
Increment -> Conteggio: 2 (annullamenti: 4)
Reset -> Conteggio: 0 (annullamenti: 5)
Undo -> Conteggio: 2 (annullamenti: 4)
Undo -> Conteggio: 1 (annullamenti: 3)
Undo -> Conteggio: 2 (annullamenti: 2)
▶ Provalo tu

Aggiungere l’annullamento è stato facile perché ogni modifica passa già da update. In una vera app Iced, dovresti solo aggiungere un altro pulsante che manda Message::Undo.

  • Le GUI in Rust oggi funzionano bene, ma non esiste ancora un unico toolkit standard, e diversi crate popolari sono ancora 0.x.
  • Tauri abbina un frontend web a un backend Rust; egui è immediate mode in Rust puro; Slint usa un suo markup e gira anche su dispositivi embedded; Iced usa l’architettura Elm.
  • La retained mode conserva un albero di widget e chiama le tue callback. L’immediate mode ridisegna l’interfaccia dal tuo stato a ogni frame, senza callback.
  • L’architettura Elm divide un’app in stato, messaggi, update e view, e così le modifiche sono facili da seguire e da testare.
  • Tauri ha bisogno di WebView2 su Windows e di webkit2gtk su Linux. Per le basi guarda Strumenti e configurazione.

Verifica veloce

1. In egui, perché il valore del contatore deve stare nella struct dell'app e non in una variabile locale dentro ui?

2. In un'app Tauri, cosa fa #[tauri::command]?

3. Nell'architettura Elm usata da Iced, dove cambia lo stato dell'app?

4. Cosa serve a Tauri su Linux che a egui non serve?

Punteggio: 0 / 4