---
title: "egui e eframe: Crie uma GUI em Rust | Rust Brasil"
url: "https://rustlang.com.br/blog/egui-eframe-interface-grafica-rust/"
markdown_url: "https://rustlang.com.br/blog/egui-eframe-interface-grafica-rust.MD"
description: "Crie uma interface gráfica em Rust com egui e eframe: lista de tarefas, estado, eventos, testes e cuidados para não bloquear a janela durante operações de I/O."
date: "2026-10-07"
author: "Equipe Rust Brasil"
---

# egui e eframe: Crie uma GUI em Rust | Rust Brasil

Crie uma interface gráfica em Rust com egui e eframe: lista de tarefas, estado, eventos, testes e cuidados para não bloquear a janela durante operações de I/O.


**Para criar uma interface gráfica em Rust com egui, use eframe para abrir a janela e implemente `eframe::App` para desenhar os widgets e atualizar o estado.** Neste tutorial, você vai construir uma lista de tarefas desktop com campo de texto, botão de inclusão, checkboxes e remoção das tarefas concluídas. Toda a interface será escrita em Rust, sem HTML ou JavaScript.

O objetivo não é escolher o melhor framework para qualquer produto. Se você ainda está decidindo entre Tauri, Iced, egui e Slint, consulte o [comparativo de frameworks GUI em Rust](/blog/rust-gui-2026/). Aqui, vamos transformar a escolha por egui em um aplicativo pequeno, compreensível e com lógica testável.

## egui e eframe: quem faz o quê?

| Componente | Responsabilidade neste projeto |
|---|---|
| `egui` | Widgets, layout, interação e descrição da interface |
| `eframe` | Janela, loop de eventos e integração com a renderização |
| Sua struct `ListaApp` | Texto em edição e coleção de tarefas |
| Métodos do modelo | Inclusão, validação e remoção de tarefas |
| Cargo | Dependências, compilação, execução e testes |

O egui usa **immediate mode**: a cada atualização da interface, você descreve o que deve aparecer a partir do estado atual. Isso não significa recriar os dados do aplicativo a cada frame, nem executar uma operação de negócio sempre que um widget é desenhado.

Por exemplo, `ui.button("Adicionar").clicked()` informa se houve um clique naquela atualização. A inclusão acontece dentro desse `if`, não toda vez que `update` roda. A biblioteca também mantém informações internas de interação, como foco; seu modelo de domínio continua sendo responsabilidade da aplicação.

## 1. Crie o projeto e fixe a versão do exemplo

Você precisa de [Rust e Cargo instalados](/instalacao/) e de um ambiente desktop para abrir a janela. Crie o projeto:

```bash
cargo new lista-egui
cd lista-egui
```

Substitua o `Cargo.toml` por:

```toml
[package]
name = "lista-egui"
version = "0.1.0"
edition = "2021"

[dependencies]
eframe = "=0.30.0"
```

**Este tutorial usa deliberadamente eframe 0.30.0, não a versão mais recente.** A versão fixa acompanha a API `App::update` apresentada aqui e facilita comparar o exemplo com o guia de GUI do site. Versões posteriores podem mudar APIs; ao atualizar, consulte a documentação da versão escolhida em vez de misturar trechos de diferentes releases.

A crate eframe 0.30.0 declara Rust 1.80 como versão mínima, mas as dependências transitivas resolvidas hoje podem exigir uma toolchain mais nova. Use uma toolchain stable atualizada e mantenha o `Cargo.lock` no controle de versão do aplicativo para registrar a resolução efetiva das dependências. Uma versão fixa de eframe, sozinha, não fixa todo o grafo.

Não é necessário adicionar uma dependência direta de `egui`: vamos usar a reexportação `eframe::egui`, evitando combinar acidentalmente versões incompatíveis.

## 2. Implemente a lista de tarefas completa

Substitua `src/main.rs` pelo código abaixo:

```rust
use eframe::egui;

struct Tarefa {
    titulo: String,
    concluida: bool,
}

#[derive(Default)]
struct ListaApp {
    entrada: String,
    tarefas: Vec<Tarefa>,
}

impl ListaApp {
    fn adicionar(&mut self) {
        let titulo = self.entrada.trim().to_owned();
        if titulo.is_empty() {
            return;
        }

        self.tarefas.push(Tarefa {
            titulo,
            concluida: false,
        });
        self.entrada.clear();
    }

    fn remover_concluidas(&mut self) {
        self.tarefas.retain(|tarefa| !tarefa.concluida);
    }
}

impl eframe::App for ListaApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Minha lista de tarefas");

            ui.horizontal(|ui| {
                ui.text_edit_singleline(&mut self.entrada);
                let pode_adicionar = !self.entrada.trim().is_empty();
                if ui
                    .add_enabled(pode_adicionar, egui::Button::new("Adicionar"))
                    .clicked()
                {
                    self.adicionar();
                }
            });

            ui.separator();
            let pendentes = self.tarefas.iter()
                .filter(|tarefa| !tarefa.concluida)
                .count();
            ui.label(format!("{pendentes} tarefa(s) pendente(s)"));

            egui::ScrollArea::vertical().show(ui, |ui| {
                for tarefa in &mut self.tarefas {
                    ui.checkbox(&mut tarefa.concluida, &tarefa.titulo);
                }
            });

            ui.separator();
            if ui.button("Remover concluídas").clicked() {
                self.remover_concluidas();
            }
        });
    }
}

fn main() -> eframe::Result {
    let opcoes = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([480.0, 360.0]),
        ..Default::default()
    };

    eframe::run_native(
        "Lista de tarefas",
        opcoes,
        Box::new(|_cc| Ok(Box::new(ListaApp::default()))),
    )
}
```

Execute:

```bash
cargo run
```

A janela deve mostrar um campo vazio e o botão **Adicionar** desabilitado. Digite uma tarefa, clique no botão, marque o checkbox e use **Remover concluídas**. O contador acompanha as tarefas ainda pendentes; como ele é calculado antes dos checkboxes, uma mudança pode aparecer na atualização seguinte.

O retorno `Ok(Box::new(...))` na closure de criação faz parte da API desta versão. Exemplos antigos que retornam apenas `Box::new(...)` podem não compilar com o manifesto acima.

### Como o estado sobrevive às atualizações?

`ListaApp` é criada uma vez e entregue ao eframe. Seus campos continuam existindo entre chamadas de `update`. Já uma declaração como `let mut tarefas = Vec::new()` dentro de `update` criaria uma coleção vazia a cada chamada e perderia o histórico.

O `&mut self.entrada` permite que o widget de edição altere o texto. O `&mut tarefa.concluida` faz o mesmo com o checkbox. Isso é um uso direto de [ownership e borrowing](/tutoriais/ownership-borrowing/), não exige `Arc<Mutex<_>>` porque o exemplo manipula o modelo na thread da interface.

Observe também a separação entre percorrer a coleção e remover itens: a chamada a `retain` acontece depois que o loop de checkboxes terminou. Tentar remover elementos do mesmo `Vec` enquanto você mantém referências mutáveis a seus itens cria problemas de empréstimo e de indexação.

## 3. Teste a lógica sem abrir uma janela

Adicione este módulo ao final do mesmo `src/main.rs`:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ignora_entrada_vazia() {
        let mut app = ListaApp {
            entrada: "   ".to_owned(),
            ..Default::default()
        };
        app.adicionar();
        assert!(app.tarefas.is_empty());
    }

    #[test]
    fn adiciona_titulo_sem_espacos_nas_bordas() {
        let mut app = ListaApp {
            entrada: "  Estudar Rust  ".to_owned(),
            ..Default::default()
        };
        app.adicionar();
        assert_eq!(app.tarefas.len(), 1);
        assert_eq!(app.tarefas[0].titulo, "Estudar Rust");
        assert!(!app.tarefas[0].concluida);
        assert!(app.entrada.is_empty());
    }

    #[test]
    fn remove_apenas_as_concluidas() {
        let mut app = ListaApp::default();
        app.entrada = "Ler documentação".to_owned();
        app.adicionar();
        app.entrada = "Praticar".to_owned();
        app.adicionar();
        app.tarefas[0].concluida = true;
        app.remover_concluidas();
        assert_eq!(app.tarefas.len(), 1);
        assert_eq!(app.tarefas[0].titulo, "Praticar");
    }
}
```

Rode `cargo test`. Esses testes validam regras do modelo, não layout, navegação por teclado ou comportamento visual. A compilação ainda precisa das dependências de GUI, mas os testes não chamam `run_native` nem abrem uma janela. Para ampliar a estratégia, veja o [guia de testes em Rust](/blog/testes-rust-estrategias-boas-praticas-2026/).

## 4. Evite bloquear a interface com I/O

O exemplo só faz operações pequenas em memória. Quando você adicionar consulta HTTP, leitura de arquivos grandes ou processamento pesado, não coloque essas tarefas diretamente dentro de `update`. Essa função participa do fluxo da interface: bloqueá-la atrasa cliques, redesenho e respostas ao usuário.

Um desenho simples para trabalho bloqueante é:

1. Ao receber um clique, marque a operação como em andamento e inicie um worker.
2. Mova para o worker apenas os dados necessários, não toda a aplicação.
3. Envie o resultado por um canal, incluindo falhas em um `Result`.
4. No `update`, use `try_recv`, não `recv`, para consultar o canal sem esperar.
5. Ao terminar, o worker chama `request_repaint()` em um clone do `egui::Context` para acordar a interface.
6. Atualize o modelo e apresente sucesso ou erro na próxima atualização.

Evite iniciar um novo worker em cada frame. O clique deve disparar a operação uma única vez, e o estado de carregamento pode desabilitar o botão até a conclusão. O tutorial de [funções em outra thread em Rust](/blog/executar-funcao-outra-thread-rust/) explica passagem de argumentos e posse dos dados; neste caso, não use `join()` dentro de `update`, pois ele espera pelo worker e volta a bloquear a janela.

## 5. Limites do exemplo e problemas frequentes

| Sintoma ou necessidade | O que verificar |
|---|---|
| Erro de tipos na closure de `run_native` | Confirme eframe 0.30.0 e o retorno `Ok(Box::new(...))` |
| Erro de biblioteca nativa no Linux | Confira dependências do backend e pacotes de desenvolvimento indicados pelo erro |
| A janela não abre em servidor ou contêiner | É necessário acesso a uma sessão gráfica e a um backend de renderização compatível |
| As tarefas somem ao reiniciar | O exemplo não implementa persistência |
| Interface congelada ao buscar dados | Mova I/O bloqueante para fora de `update` |
| Texto ou seleção muda de forma inesperada em listas complexas | Avalie IDs estáveis dos widgets ao reordenar e inserir itens |

Para salvar tarefas, você pode serializar o modelo ou usar [SQLite em aplicações Rust](/blog/rust-sqlite-embedded-apps-cli-edge-2026/). Planeje o tratamento de arquivo corrompido, falha de escrita e versão do formato antes de considerar os dados duráveis.

Para entregar o aplicativo a outras pessoas, `cargo build --release` é apenas o começo. Teste o binário no sistema-alvo, confira dependências nativas e planeje instaladores, assinatura e atualizações. A possibilidade de executar egui na web não torna este projeto automaticamente WebAssembly: a integração web exige entrada e configuração próprias.

## Perguntas frequentes

### egui cria widgets nativos do sistema operacional?

Não no sentido de usar os controles padrão de Windows, macOS ou GTK para cada botão e campo. O egui desenha seus widgets. Se aparência nativa, leitores de tela ou integrações específicas forem requisitos, valide esses pontos no sistema-alvo antes de escolher o toolkit.

### Preciso de Tokio para usar eframe?

Não. A lista usa apenas eframe e a biblioteca padrão. Tokio pode entrar para operações assíncronas, mas exige uma integração deliberada com o loop da interface; adicionar o runtime não torna código bloqueante seguro dentro de `update`.

### Quando este tipo de GUI faz sentido?

Para ferramentas internas, visualizadores, editores técnicos e protótipos em que uma interface escrita em Rust simplifica o projeto. Para uma equipe que já domina frontend web ou um produto cheio de componentes web, avalie também [Tauri versus Electron](/blog/tauri-vs-electron-2026/).

## Próximo passo

Antes de aumentar a aplicação, faça o ciclo completo: adicionar, concluir, remover e testar. Depois escolha uma evolução concreta, como persistência ou importação em background. Assim você pratica estado, empréstimos e separação entre domínio e interface sem transformar a primeira janela em uma arquitetura difícil de manter.

Se ainda precisa consolidar a linguagem antes de avançar no desktop, siga a [trilha de aprendizado](/aprenda/) ou conheça o [curso de Rust](/curso/).

### Referências da versão utilizada

- [eframe 0.30.0: documentação e exemplo inicial](https://docs.rs/eframe/0.30.0/eframe/)
- [Trait eframe::App 0.30.0](https://docs.rs/eframe/0.30.0/eframe/trait.App.html)
- [egui 0.30.0: widgets e contexto](https://docs.rs/egui/0.30.0/egui/)
- [Repositório oficial egui e eframe](https://github.com/emilk/egui)
