Applications graphiques en Rust : Tauri, egui, Slint et Iced
Tôt ou tard, vous voudrez une vraie fenêtre avec des boutons, et pas seulement du texte dans un terminal. Peut-on faire ça en Rust ? Oui. Les applications graphiques (GUI) en Rust sont rapides, légères et sûres, et plusieurs bons toolkits existent déjà. Mais le monde des GUI en Rust est plus jeune que, par exemple, C# avec WPF ou JavaScript avec Electron. Mieux vaut donc connaître le paysage avant de choisir.
Cette page vous donne un tour d’horizon honnête, un tableau pour vous aider à choisir, l’idée derrière le mode immédiat et le mode retenu, et un premier petit programme pour les toolkits les plus populaires.
Où exécuter le code ? Une GUI a besoin d’une vraie fenêtre : les programmes egui, Tauri, Slint et Iced sont donc faits pour votre propre ordinateur, avec cargo run. Les programmes egui et Iced de cette page ont été compilés avec cargo check sur les versions actuelles. Deux sections imitent une GUI en Rust pur, qui affiche du texte au lieu de dessiner une fenêtre : celles-là tournent dans le navigateur. Repérez les boutons « Essayez vous-même ».
L’état réel des GUI en Rust
Section intitulée « L’état réel des GUI en Rust »Le site communautaire Are we GUI yet? (en anglais) le résume en une phrase : « Les racines ne sont pas profondes, mais les graines sont plantées. » C’est toujours assez juste :
- Ce qui est excellent : la vitesse, des applications légères et la sûreté de Rust. egui, Iced et Slint dessinent tout eux-mêmes, donc un seul code tourne sur Windows, macOS et Linux. Tauri vous permet de réutiliser vos compétences web et garde l’application bien plus légère qu’Electron, car il utilise la webview déjà présente sur le système.
- Ce qui grandit encore : il n’existe pas un toolkit « standard » unique, comme Qt en C++ ou WPF en C#. Plusieurs crates populaires sont encore en version 0.x, donc une mise à jour peut casser votre code. L’accessibilité, le texte de droite à gauche et l’apparence native varient d’un toolkit à l’autre.
- La bonne nouvelle : inutile de trouver le toolkit parfait. Pour une première application, n’importe lequel du tableau ci-dessous fera très bien l’affaire.
Pour une comparaison plus longue, la revue 2025 des bibliothèques GUI pour Rust (en anglais) teste de nombreux toolkits un par un.
Ce que vous pouvez construire
Section intitulée « Ce que vous pouvez construire »De vraies applications à regarder dès aujourd’hui :
- Le Rerun Viewer, un outil pour visualiser des données de robots et de vision par ordinateur, est construit avec egui. (Rerun finance d’ailleurs le développement d’egui.)
- COSMIC, l’environnement de bureau Linux de System76, est construit avec Iced. Tout comme le client de discussion IRC Halloy.
- Des outils de bureau à l’interface façon web : applications de notes, tableaux de bord et petits outils métier vont très bien avec Tauri.
- Des écrans sur des appareils : Slint tourne aussi sur des microcontrôleurs, c’est donc un bon choix pour les écrans tactiles de machines et d’appareils ménagers.
Quelle crate choisir ?
Section intitulée « Quelle crate choisir ? »Les versions sont les dernières sorties en octobre 2026.
| Crate | Version | Comment on écrit l’interface | Tourne sur | À choisir quand |
|---|---|---|---|---|
| Tauri | 2.12 (3.0 en alpha) | HTML, CSS et JavaScript pour l’écran ; Rust pour la logique | Windows, macOS, Linux, Android, iOS | Vous connaissez le développement web, ou vous voulez une interface riche et stylée |
| egui + eframe | 0.37 | Rust pur, mode immédiat | Windows, macOS, Linux, web (Wasm) | Outils, panneaux de débogage, éditeurs, et le démarrage le plus rapide en Rust pur |
| Slint | 1.18 | Son propre langage de balisage .slint, plus Rust pour la logique |
Bureau, embarqué, mobile, web | Une application soignée, ou un écran sur un appareil embarqué |
| Iced | 0.14 | Rust pur, architecture Elm (état, messages, update, view) | Windows, macOS, Linux, web | Vous aimez un code propre et prévisible, avec tous les changements au même endroit |
| gtk-rs (gtk4) | 0.11 | Bindings Rust pour GTK 4, mode retenu | Linux d’abord ; Windows et macOS avec plus de configuration | Des applications d’aspect natif pour le bureau GNOME sous Linux |
| Dioxus | 0.7 | Des composants façon React avec une macro rsx! |
Web, bureau (webview), mobile | Vous connaissez React et voulez un seul code pour le web et le bureau |
Quelques remarques honnêtes sur les licences et la maturité :
- Slint est gratuit sous GPLv3, ou gratuit pour les applications de bureau, mobiles et web à code fermé sous sa licence libre de redevances (vous devez indiquer que vous utilisez Slint). Les produits embarqués à code fermé demandent une licence payante. Lisez slint.dev/pricing (en anglais) avant de publier.
- egui, Iced et Dioxus sont encore en 0.x. Attendez-vous à de petits changements incompatibles à chaque version, et vérifiez qu’un tutoriel correspond à votre version. Par exemple, les anciens tutoriels egui utilisent
fn update(&mut self, ctx: &egui::Context, ...), alors qu’eframe 0.37 utilisefn ui(&mut self, ui: &mut egui::Ui, ...). - Tauri, Dioxus en version bureau et gtk-rs dépendent de bibliothèques système. Voir Prérequis selon la plateforme plus bas.
Mode immédiat ou mode retenu
Section intitulée « Mode immédiat ou mode retenu »Les toolkits ne sont pas d’accord sur une grande question : qui se souvient des widgets ?
En mode retenu (retained mode : gtk-rs, Slint, la page web de Tauri et la plupart des toolkits dans les autres langages), vous créez un bouton une seule fois. Le toolkit le garde dans un arbre de widgets. Vous y attachez un callback (une fonction de rappel), et quand l’utilisateur clique, le toolkit appelle votre callback. Pour changer plus tard le texte du bouton, il vous faut une référence vers ce bouton.
En mode immédiat (immediate mode : egui), rien n’est gardé. Votre fonction d’interface est rappelée à chaque image (frame) et décrit simplement ce qu’il faut afficher maintenant. Un bouton est dessiné et vérifié en une seule ligne : if ui.button("+").clicked() { ... }. Pas de callbacks, pas de références à garder. L’état de votre application vit dans votre propre struct, et l’interface est toujours dessinée à partir de lui : l’écran ne peut donc jamais se désynchroniser de vos données.
Le prix à payer : le toolkit recalcule la mise en page de toute l’interface à chaque image, donc une interface énorme consomme plus de CPU, et certaines mises en page (comme centrer une fenêtre dont on ne connaît pas encore la taille) sont plus difficiles. egui ne dessine une nouvelle image que lorsqu’il se passe quelque chose, comme un mouvement de souris : une application au repos n’utilise presque pas de CPU.
Voici un minuscule toolkit en mode immédiat, pour de faux, en Rust pur. Chaque tour de boucle est une image. Le tableau clicks joue le rôle de la souris :
// Un « Ui » pour de faux, qui note ce qui a été dessiné pendant cette image.struct Ui { clicked: Option<&'static str>, // le bouton cliqué par la souris pendant cette image drawn: Vec<String>,}
impl Ui { fn label(&mut self, text: &str) { self.drawn.push(text.to_string()); }
// Dessine un bouton ET vous dit tout de suite s'il a été cliqué. fn button(&mut self, text: &'static str) -> bool { self.drawn.push(format!("[{text}]")); self.clicked == Some(text) }}
// Votre code d'interface : rappelé à chaque image.fn draw(count: &mut i32, ui: &mut Ui) { ui.label(&format!("Compteur : {count}")); if ui.button("+") { *count += 1; } if ui.button("Remettre à zéro") { *count = 0; }}
fn main() { let mut count = 0; // l'état de l'application vit en dehors du code d'interface let clicks = [None, Some("+"), Some("+"), None, Some("Remettre à zéro"), None];
for (frame, click) in clicks.into_iter().enumerate() { let mut ui = Ui { clicked: click, drawn: Vec::new() }; draw(&mut count, &mut ui); println!("image {frame} : {}", ui.drawn.join(" ")); }}Sortie :
image 0 : Compteur : 0 [+] [Remettre à zéro]image 1 : Compteur : 0 [+] [Remettre à zéro]image 2 : Compteur : 1 [+] [Remettre à zéro]image 3 : Compteur : 2 [+] [Remettre à zéro]image 4 : Compteur : 2 [+] [Remettre à zéro]image 5 : Compteur : 0 [+] [Remettre à zéro]Regardez l’image 1 : vous cliquez sur « + », mais le libellé affiche encore 0. Le libellé a été dessiné avant que le bouton ne remarque le clic. La nouvelle valeur n’apparaît qu’à l’image suivante. Le vrai egui s’en occupe pour vous : un clic ou une touche pressée lui fait dessiner l’image suivante tout de suite, donc vous ne voyez jamais l’ancien nombre.
Votre première application egui : un compteur
Section intitulée « Votre première application egui : un compteur »eframe est le « framework d’application » officiel d’egui : il ouvre la fenêtre et fait tourner egui dedans. Créez un projet et ajoutez-le :
cargo new countercd counter[dependencies]eframe = "0.37"Remplacez src/main.rs par ceci :
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; } }); }); }}Lancez-le avec cargo run. La première compilation télécharge et compile quelques centaines de crates : comptez quelques minutes. Les suivantes sont rapides. Une petite fenêtre s’ouvre avec « Count: 0 » et trois boutons.
Comment tout ça s’assemble :
CounterAppest votre état.#[derive(Default)]fait démarrercountà 0.run_nativeouvre la fenêtre. La closure crée votre application une seule fois, dans uneBox(voir Pointeurs intelligents).- eframe appelle
uià chaque image.CentralPanelremplit la fenêtre, etui.horizontalmet les trois boutons sur une ligne. - Chaque
if ui.button(..).clicked()est le motif du mode immédiat vu plus haut : dessiner le bouton, le vérifier, modifier l’état.
C’est aussi pour cela que l’état doit rester dans self, jamais dans une variable locale à l’intérieur de ui : un let mut count = 0; à cet endroit repartirait de 0 à chaque image.
Une commande Tauri : du Rust derrière une page web
Section intitulée « Une commande Tauri : du Rust derrière une page web »Une application Tauri a deux parties. Le frontend est une page web normale (HTML, CSS et JavaScript, ou un framework comme React ou Svelte) qui tourne dans la webview du système. Le backend est votre code Rust. Le frontend appelle des fonctions Rust qu’on appelle des commandes.
Le plus simple pour démarrer est le générateur de projet officiel. Il vous demande un nom et un modèle de frontend (le modèle « vanilla », sans framework, est un bon premier choix) :
cargo install create-tauri-app --lockedcargo create-tauri-appIl existe aussi npm create tauri-app@latest si vous utilisez déjà Node.js. Entrez ensuite dans le nouveau dossier et lancez l’application avec npm install puis npm run tauri dev (pour les modèles npm), ou avec cargo install tauri-cli --version "^2.0.0" --locked puis cargo tauri dev (pour les modèles 100 % Rust). L’outil affiche les bonnes commandes selon votre choix.
Le fichier généré src-tauri/src/lib.rs contient votre première commande. Nous le laissons tel que Tauri le génère, commentaire en anglais compris :
// 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");}Et le frontend (src/main.js dans le modèle vanilla, raccourci ici) l’appelle comme ceci :
const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });Ce qui se passe :
#[tauri::command]est une macro d’attribut (voir Macros procédurales). Elle écrit le code de liaison qui transforme l’appel JavaScript en un appel Rust normal.- Les arguments arrivent en JSON : chaque type d’argument doit donc implémenter
Deserializede serde, et le type de retour doit implémenterSerialize(voir JSON avec Serde). En JavaScript, écrivez les noms d’arguments en camelCase : un paramètre Rustinvoke_messagedevient{ invokeMessage: "..." }. generate_handler![greet]enregistre la commande. Si vous oubliez d’y ajouter une nouvelle commande, l’appel depuis JavaScript échoue.- Renvoyez un
Resultpour signaler les erreurs :Okrésout la promesse JavaScript, etErrla rejette, ce que vous pouvez gérer avec.catch(). Par exemple,fn load(path: String) -> Result<String, String>. main.rsse contente d’appelerrun(). Le code vit danslib.rspour que la même application puisse aussi être compilée pour Android et iOS.
Slint : l’interface dans son propre langage de balisage
Section intitulée « Slint : l’interface dans son propre langage de balisage »Slint décrit l’écran dans un petit langage déclaratif, un peu comme HTML et CSS réunis. Votre code Rust s’occupe de la logique. Voici le « hello world » officiel. Ajoutez slint = "1.18" sous [dependencies] et mettez ceci dans src/main.rs :
slint::slint!{ export component HelloWorld { Text { text: "hello world"; color: green; } }}
fn main() { HelloWorld::new().unwrap().run().unwrap();}La macro slint! compile le balisage au moment de la compilation et crée pour vous un type Rust HelloWorld. Dans une vraie application, le balisage va dans son propre fichier .slint. Ce compteur vient du modèle Rust officiel de Slint (en anglais). Le fichier 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(); } } }}Et le côté Rust, dans 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 fonctionne en mode retenu : la propriété counter vit dans l’interface, et le bouton appelle un callback. Slint génère get_counter, set_counter et on_request_increase_value à partir du balisage (les tirets deviennent des tirets bas). La closure garde une référence faible (as_weak()) pour que la fenêtre et son propre callback ne se maintiennent pas en vie l’un l’autre pour toujours (voir Weak et Cow). Pour un fichier .slint, il vous faut aussi slint-build = "1.18" sous [build-dependencies] et un build.rs d’une ligne : slint_build::compile("ui/app-window.slint").expect("Slint build failed");.
Iced et l’architecture Elm
Section intitulée « Iced et l’architecture Elm »Iced suit l’architecture Elm, du nom du langage Elm. Chaque application a quatre parties :
- L’état : une struct qui contient tout ce dont l’application se souvient.
- Les messages : un enum de tout ce qui peut arriver (« le bouton + a été pressé »).
- update : prend l’état et un message, et modifie l’état. C’est le seul endroit où les changements ont lieu.
- view : construit les widgets à partir de l’état. Les widgets ne changent rien eux-mêmes ; ils se contentent d’envoyer des messages.
Voici le compteur officiel d’Iced (ajoutez iced = "0.14" dans [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) n’exécute aucun code. Il dit seulement « quand on clique, envoie ce message ». Iced appelle alors update avec ce message, puis rappelle view pour afficher le nouvel état. Comme chaque changement passe par un seul match, il est facile de voir (et de tester) tout ce que votre application sait faire. C’est une belle utilisation des enums et de match.
Essayez l’architecture Elm dans votre navigateur
Section intitulée « Essayez l’architecture Elm dans votre navigateur »Pas besoin d’Iced pour pratiquer l’idée. Ce modèle de liste de tâches a les mêmes quatre parties. La fonction view renvoie du texte au lieu de widgets, et une liste de messages remplace la saisie et les clics de l’utilisateur :
// 1. L'état : tout ce dont l'application doit se souvenir.#[derive(Default)]struct App { draft: String, todos: Vec<Todo>,}
struct Todo { title: String, done: bool,}
// 2. Les messages : tout ce qui peut arriver.#[derive(Debug)]enum Message { DraftChanged(String), AddPressed, Toggle(usize), ClearDone,}
impl App { // 3. update : le SEUL endroit où l'état change. 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 : transforme l'état en ce que voit l'utilisateur (ici, du texte). fn view(&self) -> String { let mut screen = format!("Nouvelle : [{}]\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();
// Imaginez que ces messages viennent de touches pressées et de clics. let messages = vec![ Message::DraftChanged("Acheter du lait".to_string()), Message::AddPressed, Message::DraftChanged("Appeler la banque".to_string()), Message::AddPressed, Message::DraftChanged("Arroser les plantes".to_string()), Message::Toggle(0), ];
for message in messages { println!("> {message:?}"); app.update(message); } print!("{}", app.view());
app.update(Message::ClearDone); println!("> après ClearDone :"); print!("{}", app.view());}Sortie :
> DraftChanged("Acheter du lait")> AddPressed> DraftChanged("Appeler la banque")> AddPressed> DraftChanged("Arroser les plantes")> Toggle(0)Nouvelle : [Arroser les plantes]0. [x] Acheter du lait1. [ ] Appeler la banque> après ClearDone :Nouvelle : [Arroser les plantes]0. [ ] Appeler la banqueRemarquez que view ne modifie jamais rien, et que update n’affiche jamais rien. Tout le secret est dans cette séparation. Pour en faire une vraie application Iced, il suffirait de réécrire view pour qu’elle renvoie des widgets (un text_input qui envoie DraftChanged, un button qui envoie AddPressed). L’état, les messages et update restent exactement les mêmes.
Prérequis selon la plateforme
Section intitulée « Prérequis selon la plateforme »Les crates GUI parlent à votre système d’exploitation : il vous faut donc un peu plus que Rust tout seul. Installez d’abord la base décrite dans Les outils Rust. Ensuite :
| Système | egui/eframe, Iced, Slint | Tauri demande en plus |
|---|---|---|
| Windows | Rien de plus que les outils C++ MSVC | WebView2. Il fait déjà partie de Windows 10 (version 1803 et suivantes) et de Windows 11 |
| macOS | Les outils en ligne de commande Xcode (xcode-select --install) |
Rien de plus pour les applications de bureau ; Xcode complet pour iOS |
| Linux (Debian/Ubuntu) | Le README d’eframe liste libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev |
webkit2gtk : libwebkit2gtk-4.1-dev plus quelques autres paquets, listés sur la page des prérequis de Tauri (en anglais) |
Si une compilation sous Linux échoue avec un message de pkg-config sur une bibliothèque manquante, installez le paquet -dev de cette bibliothèque et recompilez. gtk-rs a besoin des fichiers de développement de GTK 4 (libgtk-4-dev sous Ubuntu).
Erreurs courantes
Section intitulée « Erreurs courantes »- Bloquer le thread de l’interface. Un téléchargement ou un long calcul dans la fonction
uid’egui ou dans unupdated’Iced fige toute la fenêtre jusqu’à ce qu’il se termine. Faites le travail lent dans un autre thread ou dans une tâche async (voir Concurrence et Async/Await), et renvoyez le résultat à l’interface sous forme de message. - Garder l’état egui dans des variables locales.
uiest rappelée à chaque image, donc unlet mutà l’intérieur repart de zéro à chaque fois. Mettez l’état dans la struct de votre application. - Une commande Tauri jamais enregistrée. Chaque commande doit apparaître dans
tauri::generate_handler![...]. Et danslib.rs, ne marquez pas les commandes avecpub: la documentation de Tauri explique que cela provoque une erreurE0255« defined multiple times » (défini plusieurs fois). - Suivre un vieux tutoriel. Avec des crates en 0.x, un exemple de l’an dernier peut ne plus compiler. Comparez la version du tutoriel avec celle de votre
Cargo.toml, et regardez le dossierexamplesde la crate pour la version que vous utilisez.
Exercices
Section intitulée « Exercices »1. Un compteur qui ne descend jamais sous zéro
Section intitulée « 1. Un compteur qui ne descend jamais sous zéro »Écrivez un compteur façon Elm avec les messages Increment, Decrement et Reset. La valeur ne doit jamais passer sous 0, même si l’utilisateur continue d’appuyer sur « - ». Faites passer une liste de messages dans update et affichez view() après chacun d’eux.
Indice : utilisez un u32 et saturating_sub(1), qui s’arrête à 0 au lieu de déborder (voir Le dépassement d’entier).
Voir la solution
#[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!("Compteur : {}", 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()); }}Sortie :
Increment -> Compteur : 1Increment -> Compteur : 2Decrement -> Compteur : 1Decrement -> Compteur : 0Decrement -> Compteur : 0Increment -> Compteur : 1Reset -> Compteur : 0La règle « jamais sous zéro » vit à un seul endroit : update. On formate le nom du message avant d’appeler update, parce que update prend le message par valeur et le déplace (move).
2. Ajoutez un bouton Annuler
Section intitulée « 2. Ajoutez un bouton Annuler »Ajoutez à votre compteur un message Undo (annuler). Chaque changement mémorise l’ancienne valeur, et Undo revient d’un pas en arrière. Undo sans historique ne fait rien. Affichez dans view le nombre d’annulations encore possibles.
Indice : gardez un history: Vec<u32> dans l’état. Faites un push de l’ancienne valeur avant chaque changement, et un pop sur Undo.
Voir la solution
#[derive(Default)]struct Counter { value: u32, history: Vec<u32>, // les anciennes valeurs, la plus récente à la fin}
#[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); // d'abord mémoriser l'ancienne valeur self.value = new_value; }
fn view(&self) -> String { format!("Compteur : {} (annulations possibles : {})", 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()); }}Sortie :
Increment -> Compteur : 1 (annulations possibles : 1)Increment -> Compteur : 2 (annulations possibles : 2)Decrement -> Compteur : 1 (annulations possibles : 3)Increment -> Compteur : 2 (annulations possibles : 4)Reset -> Compteur : 0 (annulations possibles : 5)Undo -> Compteur : 2 (annulations possibles : 4)Undo -> Compteur : 1 (annulations possibles : 3)Undo -> Compteur : 2 (annulations possibles : 2)Annuler a été facile à ajouter, parce que chaque changement passe déjà par update. Dans une vraie application Iced, il suffirait d’ajouter un bouton de plus qui envoie Message::Undo.
Récapitulatif
Section intitulée « Récapitulatif »- Les GUI en Rust fonctionnent bien aujourd’hui, mais il n’existe pas encore de toolkit standard unique, et plusieurs crates populaires sont encore en 0.x.
- Tauri associe un frontend web à un backend Rust ; egui fonctionne en mode immédiat, en Rust pur ; Slint utilise son propre langage de balisage et tourne aussi sur des appareils embarqués ; Iced utilise l’architecture Elm.
- Le mode retenu garde un arbre de widgets et appelle vos callbacks. Le mode immédiat redessine l’interface à partir de votre état à chaque image, sans callbacks.
- L’architecture Elm découpe une application en état, messages,
updateetview, ce qui rend les changements faciles à suivre et à tester. - Tauri a besoin de WebView2 sous Windows et de webkit2gtk sous Linux. Voyez Les outils Rust pour la base.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Avec egui, pourquoi la valeur du compteur doit-elle vivre dans la struct de l'application et pas dans une variable locale à l'intérieur de ui ?
2. Dans une application Tauri, que fait #[tauri::command] ?
3. Dans l'architecture Elm utilisée par Iced, où l'état de l'application change-t-il ?
4. De quoi Tauri a-t-il besoin sous Linux, contrairement à egui ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Are we GUI yet? (en anglais) : le panorama communautaire des crates GUI pour Rust, avec des étiquettes comme « immediate mode » et « reactive », et les dernières nouvelles.
- Understanding immediate mode — documentation d’egui (en anglais) : comment egui fonctionne, avec ses avantages et ses inconvénients, expliqué par son auteur.
- Calling Rust from the Frontend — Tauri (en anglais) : tout sur les commandes : arguments, valeurs de retour, erreurs, commandes async et état.
- Documentation Rust de Slint (en anglais) : l’API Rust, la macro
slint!, et comment utiliser des fichiers.slintavecslint-build. - The Iced book : Architecture (en anglais) : une explication accessible de l’état, des messages, d’update et de view.