Apps com interface gráfica em Rust: Tauri, egui, Slint e Iced
Mais cedo ou mais tarde você vai querer uma janela de verdade, com botões, e não só texto no terminal. Dá para fazer isso em Rust? Dá, sim. Apps com interface gráfica (GUI) em Rust são rápidos, pequenos e seguros, e hoje existem vários bons toolkits. Mas o mundo das GUIs em Rust é mais jovem do que, por exemplo, C# com WPF ou JavaScript com Electron, então vale a pena conhecer o terreno antes de escolher um.
Esta página traz uma visão geral honesta, uma tabela para ajudar na escolha, a ideia por trás do immediate mode (modo imediato) e do retained mode (modo retido) e um primeiro programa pequeno para os toolkits mais populares.
Onde dá para rodar o código? Uma GUI precisa de uma janela de verdade, então os programas com egui, Tauri, Slint e Iced são para o seu próprio computador, com cargo run. Os programas com egui e Iced desta página foram compilados com cargo check nas versões atuais. Duas seções simulam uma GUI em Rust puro, imprimindo texto em vez de desenhar uma janela, e por isso rodam no navegador. Procure os botões “Experimente você mesmo”.
A situação real das GUIs em Rust
Seção intitulada “A situação real das GUIs em Rust”O site da comunidade Are we GUI yet? (em inglês) resume tudo em uma frase: “The roots aren’t deep but the seeds are planted” (“as raízes ainda não são profundas, mas as sementes foram plantadas”). Isso continua sendo justo:
- O que é ótimo: velocidade, apps pequenos e a segurança do Rust. egui, Iced e Slint desenham tudo por conta própria, então um único código roda no Windows, no macOS e no Linux. O Tauri permite reaproveitar o que você sabe de web e deixa o app bem menor do que no Electron, porque usa o webview que já vem no sistema.
- O que ainda está crescendo: não existe um toolkit “padrão” único, como o Qt em C++ ou o WPF em C#. Vários crates (pacotes) populares ainda estão na versão 0.x, então uma atualização pode quebrar o seu código. Acessibilidade, texto da direita para a esquerda e aparência nativa variam de toolkit para toolkit.
- A boa notícia: você não precisa encontrar o toolkit perfeito. Para um primeiro app, qualquer um da tabela abaixo funciona bem.
Para uma comparação mais longa, a pesquisa de 2025 sobre bibliotecas de GUI em Rust (em inglês) testa muitos toolkits, um por um.
O que você pode construir
Seção intitulada “O que você pode construir”Apps reais que você pode conferir hoje:
- O Rerun Viewer, uma ferramenta para visualizar dados de robôs e de visão computacional, é feito com egui. (A Rerun também patrocina o desenvolvimento do egui.)
- O COSMIC, o ambiente de desktop da System76 para Linux, é feito com Iced. O cliente de chat IRC Halloy também.
- Ferramentas de desktop com interface no estilo web: apps de notas, painéis e pequenas ferramentas para empresas combinam muito com o Tauri.
- Telas em dispositivos: o Slint também roda em microcontroladores, então é uma boa escolha para telas sensíveis ao toque em máquinas e eletrodomésticos.
Qual crate escolher?
Seção intitulada “Qual crate escolher?”As versões são as mais recentes em outubro de 2026.
| Crate | Versão | Como você escreve a interface | Roda em | Escolha quando |
|---|---|---|---|---|
| Tauri | 2.12 (3.0 em alfa) | HTML, CSS e JavaScript para a tela; Rust para a lógica | Windows, macOS, Linux, Android, iOS | Você conhece desenvolvimento web ou quer uma interface rica e estilizada |
| egui + eframe | 0.37 | Rust puro, immediate mode | Windows, macOS, Linux, web (Wasm) | Ferramentas, painéis de depuração, editores e o começo mais rápido em Rust puro |
| Slint | 1.18 | Uma linguagem de marcação própria, .slint, mais Rust para a lógica |
Desktop, embarcados, celular, web | Um app caprichado, ou uma tela em um dispositivo embarcado |
| Iced | 0.14 | Rust puro, arquitetura Elm (estado, mensagens, update, view) | Windows, macOS, Linux, web | Você gosta de código limpo e previsível, com todas as mudanças em um só lugar |
| gtk-rs (gtk4) | 0.11 | Bindings em Rust para o GTK 4, retained mode | Linux em primeiro lugar; Windows e macOS funcionam com mais configuração | Apps com cara nativa para o desktop GNOME no Linux |
| Dioxus | 0.7 | Componentes no estilo React, com uma macro rsx! |
Web, desktop (webview), celular | Você conhece React e quer um único código para web e desktop |
Algumas observações honestas sobre licenças e maturidade:
- O Slint é grátis sob a GPLv3, ou grátis para apps de código fechado para desktop, celular e web sob a sua licença sem royalties (você precisa mostrar que usa o Slint). Produtos embarcados de código fechado precisam de uma licença paga. Leia slint.dev/pricing (em inglês) antes de publicar.
- egui, Iced e Dioxus ainda estão na versão 0.x. Espere pequenas mudanças incompatíveis a cada versão e confira se um tutorial corresponde à sua versão. Por exemplo, tutoriais antigos de egui usam
fn update(&mut self, ctx: &egui::Context, ...), enquanto o eframe 0.37 usafn ui(&mut self, ui: &mut egui::Ui, ...). - Tauri, Dioxus desktop e gtk-rs dependem de bibliotecas do sistema. Veja Pré-requisitos por plataforma mais abaixo.
Immediate mode e retained mode
Seção intitulada “Immediate mode e retained mode”Os toolkits discordam em uma grande pergunta: quem se lembra dos widgets?
No retained mode (gtk-rs, Slint, a página web do Tauri e a maioria dos toolkits em outras linguagens), você cria um botão uma vez. O toolkit o guarda em uma árvore de widgets. Você conecta um callback e, quando o usuário clica, o toolkit chama esse callback. Para mudar o texto do botão depois, você precisa de uma referência (um “handle”) para aquele botão.
No immediate mode (egui), nada fica guardado. A sua função de interface roda de novo a cada quadro (frame) e simplesmente descreve o que mostrar agora. Um botão é desenhado e verificado em uma linha só: if ui.button("+").clicked() { ... }. Não há callbacks nem handles. O estado do seu app mora na sua própria struct, e a interface é sempre desenhada a partir dele, então a tela nunca fica fora de sincronia com os seus dados.
O custo: o toolkit monta o layout da interface inteira a cada quadro, então uma interface enorme gasta mais tempo de CPU, e alguns layouts (como centralizar uma janela cujo tamanho ainda não se conhece) ficam mais difíceis. O egui só desenha um novo quadro quando algo acontece, como um movimento do mouse, então um app parado quase não usa CPU.
Aqui está um toolkit de immediate mode de mentirinha, minúsculo, em Rust puro. Cada volta do laço é um quadro. O array clicks faz o papel do mouse:
// Uma "Ui" de mentirinha que registra o que foi desenhado neste quadro.struct Ui { clicked: Option<&'static str>, // o botão que o mouse clicou neste quadro drawn: Vec<String>,}
impl Ui { fn label(&mut self, text: &str) { self.drawn.push(text.to_string()); }
// Desenha um botão E já diz se ele foi clicado. fn button(&mut self, text: &'static str) -> bool { self.drawn.push(format!("[{text}]")); self.clicked == Some(text) }}
// O código da sua interface: chamado de novo a cada quadro.fn draw(count: &mut i32, ui: &mut Ui) { ui.label(&format!("Contagem: {count}")); if ui.button("+") { *count += 1; } if ui.button("Zerar") { *count = 0; }}
fn main() { let mut count = 0; // o estado do app mora fora do código da interface let clicks = [None, Some("+"), Some("+"), None, Some("Zerar"), None];
for (frame, click) in clicks.into_iter().enumerate() { let mut ui = Ui { clicked: click, drawn: Vec::new() }; draw(&mut count, &mut ui); println!("quadro {frame}: {}", ui.drawn.join(" ")); }}Saída:
quadro 0: Contagem: 0 [+] [Zerar]quadro 1: Contagem: 0 [+] [Zerar]quadro 2: Contagem: 1 [+] [Zerar]quadro 3: Contagem: 2 [+] [Zerar]quadro 4: Contagem: 2 [+] [Zerar]quadro 5: Contagem: 0 [+] [Zerar]Olhe o quadro 1: você clica em “+”, mas o rótulo ainda mostra 0. O rótulo foi desenhado antes de o botão perceber o clique. O novo valor só aparece um quadro depois. O egui de verdade cuida disso para você: qualquer clique ou tecla pressionada faz com que ele desenhe o próximo quadro na mesma hora, então você nunca vê o número antigo.
Seu primeiro app com egui: um contador
Seção intitulada “Seu primeiro app com egui: um contador”O eframe é o “framework de apps” oficial do egui: ele abre a janela e roda o egui dentro dela. Crie um projeto e adicione o eframe:
cargo new countercd counter[dependencies]eframe = "0.37"Troque o conteúdo de src/main.rs por este:
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; } }); }); }}Rode com cargo run. O primeiro build baixa e compila algumas centenas de crates, então espere alguns minutos. Os builds seguintes são rápidos. Uma janelinha abre com “Count: 0” e três botões.
Como as peças se encaixam:
CounterAppé o seu estado.#[derive(Default)]começacountem 0.run_nativeabre a janela. A closure (função anônima) cria o seu app uma única vez, dentro de umBox(veja Ponteiros inteligentes).- O eframe chama
uia cada quadro.CentralPanelpreenche a janela, eui.horizontalcoloca os três botões lado a lado. - Cada
if ui.button(..).clicked()é o padrão de immediate mode da seção anterior: desenhe o botão, verifique-o, mude o estado.
É também por isso que o estado precisa ficar em self, e nunca em uma variável local dentro de ui: um let mut count = 0; ali voltaria a 0 a cada quadro.
Um comando do Tauri: Rust por trás de uma página web
Seção intitulada “Um comando do Tauri: Rust por trás de uma página web”Um app Tauri tem duas partes. O frontend é uma página web normal (HTML, CSS e JavaScript, ou um framework como React ou Svelte) que roda no webview do sistema. O backend é o seu código Rust. O frontend chama funções Rust chamadas commands (comandos).
O jeito mais fácil de começar é o gerador de projetos oficial. Ele pede um nome e um template de frontend (o template “vanilla”, sem framework, é uma boa primeira escolha):
cargo install create-tauri-app --lockedcargo create-tauri-appTambém existe npm create tauri-app@latest, se você já usa Node.js. Depois, entre na nova pasta e inicie o app com npm install e npm run tauri dev (nos templates com npm), ou com cargo install tauri-cli --version "^2.0.0" --locked e cargo tauri dev (nos templates só com Rust). A ferramenta mostra os comandos certos para a sua escolha.
O arquivo gerado src-tauri/src/lib.rs contém o seu primeiro 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 o frontend (src/main.js no template vanilla, resumido aqui) o chama assim:
const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });O que está acontecendo:
#[tauri::command]é uma macro de atributo (veja Macros procedurais). Ela escreve o código de ligação que transforma a chamada do JavaScript em uma chamada Rust normal.- Os argumentos chegam como JSON, então cada tipo de argumento precisa implementar o
Deserializedo serde, e o tipo de retorno precisa implementarSerialize(veja JSON com Serde). No JavaScript, escreva os nomes dos argumentos em camelCase: um parâmetro Rustinvoke_messagevira{ invokeMessage: "..." }. generate_handler![greet]registra o comando. Se você esquecer de acrescentar um comando novo aqui, a chamada pelo JavaScript falha.- Retorne um
Resultpara informar erros:Okresolve a promise do JavaScript, eErra rejeita, então você pode tratá-la com.catch(). Por exemplo:fn load(path: String) -> Result<String, String>. - O
main.rssó chamarun(). O código fica emlib.rspara que o mesmo app também possa ser compilado para Android e iOS.
Slint: a interface em uma linguagem de marcação própria
Seção intitulada “Slint: a interface em uma linguagem de marcação própria”O Slint descreve a tela em uma pequena linguagem declarativa, um pouco como HTML e CSS juntos. O seu código Rust cuida da lógica. Aqui está o hello world oficial. Acrescente slint = "1.18" em [dependencies] e coloque isto em src/main.rs:
slint::slint!{ export component HelloWorld { Text { text: "hello world"; color: green; } }}
fn main() { HelloWorld::new().unwrap().run().unwrap();}A macro slint! compila a marcação na hora do build e cria um tipo Rust HelloWorld para você. Em um app de verdade, a marcação fica em um arquivo .slint próprio. Este contador vem do template oficial de Rust do Slint (em inglês). O arquivo 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 o lado Rust, em 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(())}O Slint é retained mode: a propriedade counter mora na interface, e o botão chama um callback. O Slint gera get_counter, set_counter e on_request_increase_value a partir da marcação (os hífens viram sublinhados). A closure guarda um handle fraco (as_weak()), para que a janela e o seu próprio callback não mantenham um ao outro vivos para sempre (veja Weak e Cow). Para um arquivo .slint, você também precisa de slint-build = "1.18" em [build-dependencies] e de um build.rs de uma linha: slint_build::compile("ui/app-window.slint").expect("Slint build failed");.
Iced e a arquitetura Elm
Seção intitulada “Iced e a arquitetura Elm”O Iced segue a arquitetura Elm, que tem esse nome por causa da linguagem Elm. Todo app tem quatro partes:
- Estado: uma struct com tudo o que o app lembra.
- Mensagens: um enum com tudo o que pode acontecer (“o botão + foi pressionado”).
- update: recebe o estado e uma mensagem, e muda o estado. É o único lugar onde as mudanças acontecem.
- view: monta os widgets a partir do estado. Os widgets não mudam nada sozinhos; eles só enviam mensagens.
Aqui está o contador oficial do Iced (acrescente iced = "0.14" em [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ão roda código nenhum. Ele só diz “quando clicarem, envie esta mensagem”. Aí o Iced chama update com ela e chama view de novo para mostrar o novo estado. Como toda mudança passa por um único match, fica fácil ver (e testar) tudo o que o seu app sabe fazer. É um ótimo uso de enums e de match.
Experimente a arquitetura Elm no navegador
Seção intitulada “Experimente a arquitetura Elm no navegador”Você não precisa do Iced para praticar a ideia. Este modelo de lista de tarefas tem as mesmas quatro partes. A view devolve texto em vez de widgets, e uma lista de mensagens faz o papel do que o usuário digita e clica:
// 1. O estado: tudo o que o app precisa lembrar.#[derive(Default)]struct App { draft: String, todos: Vec<Todo>,}
struct Todo { title: String, done: bool,}
// 2. As mensagens: tudo o que pode acontecer.#[derive(Debug)]enum Message { DraftChanged(String), AddPressed, Toggle(usize), ClearDone,}
impl App { // 3. update: o ÚNICO lugar onde o estado muda. 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: transforma o estado no que o usuário vê (aqui, texto). fn view(&self) -> String { let mut screen = format!("Nova: [{}]\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();
// Faz de conta que isto veio de teclas e cliques em botões. let messages = vec![ Message::DraftChanged("Comprar leite".to_string()), Message::AddPressed, Message::DraftChanged("Ligar para o banco".to_string()), Message::AddPressed, Message::DraftChanged("Regar as plantas".to_string()), Message::Toggle(0), ];
for message in messages { println!("> {message:?}"); app.update(message); } print!("{}", app.view());
app.update(Message::ClearDone); println!("> depois de ClearDone:"); print!("{}", app.view());}Saída:
> DraftChanged("Comprar leite")> AddPressed> DraftChanged("Ligar para o banco")> AddPressed> DraftChanged("Regar as plantas")> Toggle(0)Nova: [Regar as plantas]0. [x] Comprar leite1. [ ] Ligar para o banco> depois de ClearDone:Nova: [Regar as plantas]0. [ ] Ligar para o bancoRepare que view nunca muda nada, e update nunca imprime nada. Essa separação é todo o truque. Para transformar isto em um app Iced de verdade, você só reescreveria a view para devolver widgets (um text_input que envia DraftChanged, um button que envia AddPressed). O estado, as mensagens e o update continuam exatamente iguais.
Pré-requisitos por plataforma
Seção intitulada “Pré-requisitos por plataforma”Os crates de GUI conversam com o seu sistema operacional, então você precisa de um pouco mais do que o Rust puro. Primeiro, prepare o básico em Ferramentas do Rust. Depois:
| Sistema | egui/eframe, Iced, Slint | O Tauri também precisa de |
|---|---|---|
| Windows | Nada além das ferramentas de C++ do MSVC | WebView2. Ele já faz parte do Windows 10 (versão 1803 em diante) e do Windows 11 |
| macOS | Ferramentas de linha de comando do Xcode (xcode-select --install) |
Nada mais para apps de desktop; o Xcode completo para iOS |
| Linux (Debian/Ubuntu) | O README do eframe lista libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev |
webkit2gtk: libwebkit2gtk-4.1-dev e mais alguns pacotes da página de pré-requisitos do Tauri (em inglês) |
Se um build no Linux falhar com uma mensagem do pkg-config sobre uma biblioteca faltando, instale o pacote -dev dessa biblioteca e compile de novo. O gtk-rs precisa dos arquivos de desenvolvimento do GTK 4 (libgtk-4-dev no Ubuntu).
Erros comuns
Seção intitulada “Erros comuns”- Travar a thread da interface. Um download ou um cálculo demorado dentro da função
uido egui ou de umupdatedo Iced congela a janela inteira até terminar. Faça o trabalho lento em outra thread ou em uma tarefa async (veja Concorrência em Rust e Async/Await em Rust) e mande o resultado de volta para a interface como uma mensagem. - Guardar o estado do egui em variáveis locais.
uiroda de novo a cada quadro, então umlet mutlá dentro recomeça do zero toda vez. Coloque o estado na struct do seu app. - Um comando do Tauri que nunca foi registrado. Todo comando precisa aparecer em
tauri::generate_handler![...]. E, nolib.rs, não marque os comandos comopub: a documentação do Tauri explica que isso causa um erroE0255“defined multiple times” (definido mais de uma vez). - Seguir um tutorial antigo. Com crates 0.x, um exemplo do ano passado pode não compilar. Compare a versão do tutorial com o seu
Cargo.tomle consulte a pastaexamplesdo próprio crate, na versão que você usa.
Prática
Seção intitulada “Prática”1. Um contador que nunca fica abaixo de zero
Seção intitulada “1. Um contador que nunca fica abaixo de zero”Escreva um contador no estilo Elm com as mensagens Increment, Decrement e Reset. O valor nunca pode ficar abaixo de 0, mesmo que o usuário continue apertando “-”. Passe uma lista de mensagens pelo update e imprima view() depois de cada uma.
Dica: use um u32 e saturating_sub(1), que para em 0 em vez de dar overflow (veja Overflow de inteiros).
Ver solução
#[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!("Contagem: {}", 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()); }}Saída:
Increment -> Contagem: 1Increment -> Contagem: 2Decrement -> Contagem: 1Decrement -> Contagem: 0Decrement -> Contagem: 0Increment -> Contagem: 1Reset -> Contagem: 0A regra “nunca abaixo de zero” mora em um lugar só: o update. Formatamos o nome da mensagem antes de chamar update, porque update recebe a mensagem por valor e a move.
2. Acrescente um botão de desfazer
Seção intitulada “2. Acrescente um botão de desfazer”Estenda o seu contador com uma mensagem Undo. Toda mudança guarda o valor antigo, e Undo volta um passo. Undo sem histórico não faz nada. Mostre na view quantos passos ainda dá para desfazer.
Dica: guarde um history: Vec<u32> no estado. Faça push do valor antigo antes de cada mudança e pop no Undo.
Ver solução
#[derive(Default)]struct Counter { value: u32, history: Vec<u32>, // valores antigos, o mais recente por último}
#[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); // primeiro guarda o valor antigo self.value = new_value; }
fn view(&self) -> String { format!("Contagem: {} (passos para desfazer: {})", 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()); }}Saída:
Increment -> Contagem: 1 (passos para desfazer: 1)Increment -> Contagem: 2 (passos para desfazer: 2)Decrement -> Contagem: 1 (passos para desfazer: 3)Increment -> Contagem: 2 (passos para desfazer: 4)Reset -> Contagem: 0 (passos para desfazer: 5)Undo -> Contagem: 2 (passos para desfazer: 4)Undo -> Contagem: 1 (passos para desfazer: 3)Undo -> Contagem: 2 (passos para desfazer: 2)Foi fácil acrescentar o desfazer porque toda mudança já passa pelo update. Em um app Iced de verdade, você só acrescentaria mais um botão que envia Message::Undo.
- GUI em Rust funciona bem hoje, mas ainda não existe um toolkit padrão único, e vários crates populares ainda estão na versão 0.x.
- O Tauri junta um frontend web com um backend em Rust; o egui é immediate mode em Rust puro; o Slint usa uma marcação própria e também roda em dispositivos embarcados; o Iced usa a arquitetura Elm.
- O retained mode guarda uma árvore de widgets e chama os seus callbacks. O immediate mode redesenha a interface a partir do seu estado a cada quadro, sem callbacks.
- A arquitetura Elm divide um app em estado, mensagens,
updateeview, o que deixa as mudanças fáceis de acompanhar e de testar. - O Tauri precisa do WebView2 no Windows e do webkit2gtk no Linux. Confira o básico em Ferramentas do Rust.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. No egui, por que o valor do contador precisa ficar na struct do app, e não em uma variável local dentro de ui?
2. Em um app Tauri, o que #[tauri::command] faz?
3. Na arquitetura Elm usada pelo Iced, onde o estado do app muda?
4. Do que o Tauri precisa no Linux que o egui não precisa?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Are we GUI yet? (em inglês): a visão geral da comunidade sobre os crates de GUI em Rust, com etiquetas como “immediate mode” e “reactive”, e notícias recentes.
- Understanding immediate mode — documentação do egui (em inglês): como o egui funciona, com vantagens e desvantagens, explicado pelo próprio autor.
- Calling Rust from the Frontend — Tauri (em inglês): tudo sobre comandos: argumentos, valores de retorno, erros, comandos async e estado.
- Documentação do Slint para Rust (em inglês): a API em Rust, a macro
slint!e como usar arquivos.slintcom oslint-build. - The Iced book: Architecture (em inglês): uma explicação amigável de estado, mensagens, update e view.