Pular para o conteúdo

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

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.

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.

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

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]
▶ Experimente você mesmo

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.

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:

Terminal window
cargo new counter
cd 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ça count em 0.
  • run_native abre a janela. A closure (função anônima) cria o seu app uma única vez, dentro de um Box (veja Ponteiros inteligentes).
  • O eframe chama ui a cada quadro. CentralPanel preenche a janela, e ui.horizontal coloca 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):

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

També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 Deserialize do serde, e o tipo de retorno precisa implementar Serialize (veja JSON com Serde). No JavaScript, escreva os nomes dos argumentos em camelCase: um parâmetro Rust invoke_message vira { invokeMessage: "..." }.
  • generate_handler![greet] registra o comando. Se você esquecer de acrescentar um comando novo aqui, a chamada pelo JavaScript falha.
  • Retorne um Result para informar erros: Ok resolve a promise do JavaScript, e Err a rejeita, então você pode tratá-la com .catch(). Por exemplo: fn load(path: String) -> Result<String, String>.
  • O main.rs só chama run(). O código fica em lib.rs para 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");.

O Iced segue a arquitetura Elm, que tem esse nome por causa da linguagem Elm. Todo app tem quatro partes:

  1. Estado: uma struct com tudo o que o app lembra.
  2. Mensagens: um enum com tudo o que pode acontecer (“o botão + foi pressionado”).
  3. update: recebe o estado e uma mensagem, e muda o estado. É o único lugar onde as mudanças acontecem.
  4. 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.

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 leite
1. [ ] Ligar para o banco
> depois de ClearDone:
Nova: [Regar as plantas]
0. [ ] Ligar para o banco
▶ Experimente você mesmo

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

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

  • Travar a thread da interface. Um download ou um cálculo demorado dentro da função ui do egui ou de um update do 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. ui roda de novo a cada quadro, então um let mut lá 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, no lib.rs, não marque os comandos como pub: a documentação do Tauri explica que isso causa um erro E0255 “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.toml e consulte a pasta examples do próprio crate, na versão que você usa.

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: 1
Increment -> Contagem: 2
Decrement -> Contagem: 1
Decrement -> Contagem: 0
Decrement -> Contagem: 0
Increment -> Contagem: 1
Reset -> Contagem: 0
▶ Experimente você mesmo

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

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)
▶ Experimente você mesmo

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, update e view, 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

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