# Migração egui → iced: Estado e Referência **Branch:** `change-ui` **Versão iced:** 0.13.1 **Última atualização:** 04/03/2026 **Status:** ✅ **COMPILANDO** — `cargo build` passa sem erros (5 warnings não-bloqueantes, todos em camadas intocáveis) --- ## Estado Atual ### O que foi feito (100% completo) | Arquivo | Status | Descrição | |---|---|---| | `Cargo.toml` | ✅ | `eframe`/`egui`/`image` removidos; `iced 0.13` + `tokio` adicionados | | `src/main.rs` | ✅ | Reescrito com `iced::application(...)` | | `src/ui/mod.rs` | ✅ | `pub mod app; pub mod components; pub mod message; pub mod screens;` | | `src/ui/message.rs` | ✅ | Enum `Message` completo + enum `ResultadoPendente` | | `src/ui/app.rs` | ✅ | Reescrito para iced (Elm architecture) — `App`, `update()`, `view()` | | `src/ui/screens/mod.rs` | ✅ | Re-exports + `pub fn indice_para_letra(idx: usize) -> String` | | `src/ui/screens/import.rs` | ✅ | Tela de importação — pick_list de layout usa `LayoutSelecionado` | | `src/ui/screens/selecionar_aba.rs` | ✅ | Tela de seleção de aba XLSX | | `src/ui/screens/configuracao_colunas.rs` | ✅ | Tela de configuração — pick_list de layout usa `LayoutSelecionado` | | `src/ui/screens/resultado.rs` | ✅ | Tela de resultado — botão "Nova Análise" usa `Message::NovaAnalise` | | `src/ui/screens/layouts.rs` | ✅ | Tela de gerenciamento de layouts | | `src/ui/components/mod.rs` | ✅ | `pub mod modal; pub mod paginacao; pub mod tabela_preview;` | | `src/ui/components/modal.rs` | ✅ | Overlay real com `stack!` + `mouse_area` | | `src/ui/components/tabela_preview.rs` | ✅ | Tabela com scroll horizontal e cabeçalho estilo Excel | | `src/ui/components/paginacao.rs` | ✅ | Controles reutilizáveis de paginação | ### O que NÃO foi alterado (intocado por design) - `src/domain/` — entidades, serviços, erros - `src/application/` — casos de uso - `src/infrastructure/` — leitores CSV/XLSX, gerador PDF, SQLite --- ## Arquitetura da UI ### Padrão Elm (iced) ``` App (estado) ──→ view(&self) ──→ Element (widgets renderizados) ↑ ↓ │ usuário interage │ ↓ └── update(&mut self, msg) ←── Message (enum) retorna Task ``` - `view()` é **pura e somente leitura** — nunca muta estado - Toda mutação acontece **exclusivamente** em `update()` - Background tasks retornam via `Task::perform(async { ... }, Message::Variante)` ### Estrutura de arquivos ``` src/ ├── main.rs └── ui/ ├── mod.rs ├── app.rs (struct App + update + view) ├── message.rs (enum Message + ResultadoPendente) ├── screens/ │ ├── mod.rs (re-exports + indice_para_letra) │ ├── import.rs │ ├── selecionar_aba.rs │ ├── configuracao_colunas.rs │ ├── resultado.rs │ └── layouts.rs └── components/ ├── mod.rs ├── modal.rs ├── tabela_preview.rs └── paginacao.rs ``` --- ## Diferenças Críticas da API — iced 0.13 Estas diferenças causaram erros de compilação e **devem ser lembradas** em qualquer adição futura: ### 1. `Command` foi renomeado para `Task` ```rust // ❌ NÃO EXISTE em iced 0.13 use iced::Command; Command::none() // ✅ CORRETO use iced::Task; Task::none() Task::perform(future, mapper) ``` Todos os retornos de `update()` são `Task`. ### 2. `.align_items()` foi dividido ```rust // ❌ NÃO EXISTE em iced 0.13 row![...].align_items(Alignment::Center) // ✅ CORRETO — Row usa align_y, Column usa align_x row![...].align_y(Alignment::Center) column![...].align_x(Alignment::Center) ``` ### 3. `.center_x()` / `.center_y()` exigem argumento `Length` ```rust // ❌ NÃO COMPILA container(...).center_x().center_y() // ✅ CORRETO container(...).center_x(Length::Fill).center_y(Length::Fill) ``` ### 4. `iced::clipboard::write` retorna `Task` ```rust // Pode ser retornado diretamente de update(): return iced::clipboard::write(texto); ``` ### 5. Entry point usa API funcional, não trait `Application` ```rust // main.rs fn main() -> iced::Result { iced::application("Título", App::update, App::view) .window(iced::window::Settings { ... }) .run_with(App::new) } // App::new retorna (Self, Task) pub fn new() -> (Self, Task) { ... } ``` ### 6. Lifetimes em funções que retornam `Element` Sempre usar `Element<'_, Message>` (não `Element`) em funções que recebem referências: ```rust // ❌ Gera warning mismatched_lifetime_syntaxes pub fn view(app: &App) -> Element // ✅ CORRETO pub fn view(app: &App) -> Element<'_, Message> ``` **Armadilha com Vec local:** criar `Vec` local e passar `&vec` para função que retorna `Element<'a>` causa E0515. Solução: filtrar diretamente de dados que vivem no `&app`. ### 7. Borrow checker com `Arc>` ```rust // ❌ Mantém borrow imutável de &self enquanto tenta &mut self if let Some(conn_arc) = &self.conn { let conn = conn_arc.lock().unwrap(); self.exibir_erro(...); // ERRO: borrow ativo } // ✅ Clonar o Arc primeiro (O(1), não clona a Connection) let conn_arc = self.conn.as_ref().map(Arc::clone); if let Some(conn_arc) = conn_arc { let conn = conn_arc.lock().unwrap(); drop(conn); // liberar antes de &mut self self.exibir_erro(...); } ``` --- ## Componentes Implementados ### `App` (src/ui/app.rs) **Estado:** ```rust pub struct App { pub estado: EstadoApp, pub conn: Option>>, pub banco_foi_recriado: bool, pub notas_importadas: Vec, pub caminho_arquivo: Option, // ← populado para CSV e XLSX pub nome_arquivo: String, pub tipo_arquivo_atual: TipoArquivo, pub layout_csv_atual: LayoutCsv, pub layout_xlsx_atual: LayoutXlsx, pub nome_layout_atual: String, pub abas_xlsx: Vec, pub layouts_salvos: Vec, pub modal: Option, pub avisos_importacao: Option, pub pagina_faltantes: usize, pub pagina_duplicatas: usize, pub itens_por_pagina: usize, pub preview_arquivo: Option>>, pub resultado_anterior: Option, } ``` **Máquina de estados (`EstadoApp`):** ``` Importando → ArquivoSelecionado (CSV) → ConfigurandoColunas → ArquivoSelecionado (XLSX) → SelecionandoAba → ConfigurandoColunas ConfigurandoColunas → ExecutarImportacao → Analisando Analisando → AnaliseCompleta (ok, sem excessivos) → ExibindoResultado → AnaliseCompleta (excessivos) → ConfirmandoIntervalo (+ modal) ConfirmandoIntervalo → ModalConfirmado (ConfirmarExpansaoFaltantes) → Analisando → ExibindoResultado → ModalCancelado → ConfigurandoColunas ExibindoResultado → ReanalisarArquivo → Analisando → IrParaConfiguracaoColunas → ConfigurandoColunas → NovaAnalise (modal) → Importando (após confirmação) GerenciandoLayouts → IrParaImportacao → Importando ``` **Modal (`EstadoModal` / `AcaoModal`):** ```rust pub enum EstadoModal { Informacao { titulo: String, mensagem: String }, // definido, não usado ainda Aviso { titulo: String, mensagem: String }, Erro { titulo: String, mensagem: String }, Confirmacao { titulo: String, mensagem: String, acao: AcaoModal }, InputTexto { titulo: String, mensagem: String, texto: String, acao: AcaoModal }, } pub enum AcaoModal { ConfirmarExpansaoFaltantes, ConfirmarExclusaoLayout(i64), SobrescreverLayout(Layout), ConfirmarNovaAnalise, SalvarLayoutConfig, } ``` **Comportamento do `ModalCancelado`:** - Fecha o modal (`self.modal = None`) - Se o estado for `ConfirmandoIntervalo`, **também** reseta para `ConfigurandoColunas` ### `Message` (src/ui/message.rs) — enum completo ```rust pub enum Message { // Inicialização BancoInicializado(Result<(Arc>, bool, Vec), String>), LayoutsRecarregados(Vec), // Navegação IrParaImportacao, IrParaConfiguracaoColunas, IrParaLayouts, Voltar, // Arquivo SelecionarArquivo, ArquivoSelecionado(PathBuf), AbaSelecionada(String), AbaxlsxCarregadas { caminho: PathBuf, abas: Vec, layout_xlsx: LayoutXlsx, nome_layout: String }, XlsxErroAoCarregar(String), // Background AnaliseCompleta(ResultadoPendente), // Config CSV DelimitadorAlterado(char), EncodingAlterado(String), LinhaCabecalhoAlterada(usize), IndiceNumeroAlterado(usize), IndiceSerieAlterado(usize), IndiceValorToggle(bool), IndiceValorAlterado(usize), IndiceDataToggle(bool), IndiceDataAlterado(usize), IndiceDocTipoToggle(bool), IndiceDocTipoAlterado(usize), // Config XLSX AbaXlsxAlterada(String), PosNumeroAlterada(String), PosSerieAlterada(String), PosValorToggle(bool), PosValorAlterada(String), PosDataToggle(bool), PosDataAlterada(String), PosDocTipoToggle(bool), PosDocTipoAlterada(String), // Análise ExecutarImportacao, ReanalisarArquivo, ConfirmarExpansaoFaltantes, CancelarExpansao, // arm existe em update() mas nenhum widget o dispara diretamente NovaAnalise, // abre modal de confirmação (AcaoModal::ConfirmarNovaAnalise) // Resultado PaginaFaltantesAlterada(usize), PaginaDuplicatasAlterada(usize), ItensPorPaginaAlterado(usize), CopiarFaltantes(ChaveSerie), CopiarDuplicatas(ChaveSerie), ExportarPdf, PdfExportado(Result), // Layouts LayoutSelecionado(i64), // aplica layout (chama aplicar_layout) + atualiza nome SalvarLayout, // abre modal InputTexto NomeLayoutAlterado(String), ExcluirLayout(i64), ExclusaoConfirmada(i64), ExportarLayoutJson(i64), ImportarLayoutJson, LayoutJsonImportado(String), SobrescreverLayout(Layout), // Modal ModalTextoAlterado(String), ModalConfirmado, ModalCancelado, Noop, } ``` ### `modal.rs` (src/ui/components/modal.rs) Overlay real: `stack![conteudo, overlay_escuro]`. O `mouse_area` captura cliques fora da caixa e dispara `ModalCancelado`. ```rust pub fn view_com_modal<'a>( conteudo: Element<'a, Message>, modal: &'a EstadoModal, ) -> Element<'a, Message> ``` Botões da caixa modal: - Sempre: **"Fechar"** → `ModalCancelado` - Se `com_confirmar`: **"Confirmar"** → `ModalConfirmado` ### `tabela_preview.rs` (src/ui/components/tabela_preview.rs) Cabeçalho com letras estilo Excel (A, B, C...) + índice numérico. Scroll horizontal. Trunca células em 30 caracteres. Altura fixa de 160px. ### `paginacao.rs` (src/ui/components/paginacao.rs) ```rust pub fn controles_paginacao( pagina_atual: usize, total_paginas: usize, msg_anterior: Message, msg_proximo: Message, ) -> Element<'static, Message> ``` Botões ◀ / ▶ desabilitados automaticamente na primeira/última página via `on_press_maybe`. --- ## Regras de Seleção de Layout (pick_list) **Em todas as telas** que exibem um `pick_list` de layouts, o handler **deve** usar `LayoutSelecionado(id)`, não `NomeLayoutAlterado`: ```rust pick_list(opcoes_layout, nome_sel, { let layouts = app.layouts_salvos.clone(); move |nome: String| { if let Some(id) = layouts.iter().find(|l| l.nome() == nome).and_then(|l| l.id()) { Message::LayoutSelecionado(id) } else { Message::NomeLayoutAlterado(nome) // fallback (nunca deve ocorrer com layouts do banco) } } }) ``` Isso vale para: `import.rs`, `configuracao_colunas.rs`. A diferença: - `NomeLayoutAlterado` — só atualiza `self.nome_layout_atual` (string), **não preenche o formulário** - `LayoutSelecionado(id)` — chama `aplicar_layout()`, que preenche `layout_csv_atual` ou `layout_xlsx_atual` --- ## Fluxo XLSX — Detalhe Crítico ### Onde `caminho_arquivo` é populado Para **CSV**: em `processar_arquivo_selecionado()`, logo após detectar a extensão. Para **XLSX**: o caminho fica dentro do estado `SelecionandoAba { caminho }` e é copiado para `self.caminho_arquivo` **somente quando o usuário seleciona uma aba** (handler `AbaSelecionada`). ``` ArquivoSelecionado(xlsx) → AbaxlsxCarregadas → aba já definida no layout e existe no arquivo? Sim → caminho_arquivo setado aqui → disparar_importacao() Não → estado = SelecionandoAba { caminho } ← caminho_arquivo ainda None aqui → AbaSelecionada(aba) → caminho_arquivo = Some(caminho) ← só aqui é setado → estado permanece SelecionandoAba → IrParaConfiguracaoColunas → estado = ConfigurandoColunas → tem_arquivo = true ✓ ``` **Consequência:** nunca verificar `app.caminho_arquivo.is_some()` como condição de habilitação antes do usuário ter selecionado uma aba no fluxo XLSX. --- ## Background Tasks Todas as operações pesadas usam `Task::perform`: ```rust // Padrão para operações síncronas em background: Task::perform( async move { tokio::task::spawn_blocking(move || { // código síncrono (rusqlite, calamine, etc.) }) .await .unwrap_or_else(|e| ResultadoPendente::Erro(e.to_string())) }, Message::AnaliseCompleta, ) ``` | Operação | Message de retorno | |---|---| | Abrir banco + migrations + listar layouts | `BancoInicializado` | | Listar layouts após salvar/excluir | `LayoutsRecarregados` | | Listar abas de arquivo XLSX | `AbaxlsxCarregadas` / `XlsxErroAoCarregar` | | Importar CSV/XLSX + analisar | `AnaliseCompleta` | | Expandir análise com faltantes | `AnaliseCompleta` | | Exportar PDF | `PdfExportado` | | Exportar layout JSON | `Noop` (salva diretamente) | | Importar layout JSON | `LayoutJsonImportado` | **Diálogos de arquivo** usam `rfd::AsyncFileDialog` (não bloqueia): ```rust Task::perform( async { rfd::AsyncFileDialog::new() .add_filter("Planilhas", &["csv", "xlsx", "xls"]) .pick_file() .await .map(|h| h.path().to_path_buf()) }, |r| r.map(Message::ArquivoSelecionado).unwrap_or(Message::Noop), ) ``` --- ## Warnings Existentes (não-bloqueantes) | Warning | Local | Situação | |---|---|---| | `field data is never read` | `domain/nota.rs` | Domínio intocável — ignorar | | `fields minimo/maximo never read` | `domain/resultado_analise.rs` | Domínio intocável — ignorar | | `method sem_inconsistencias never used` | `domain/resultado_analise.rs` | Domínio intocável — ignorar | | `variant Informacao never constructed` | `ui/app.rs` | Mantido para uso futuro (modal informativo) | | `variant CancelarExpansao never constructed` | `ui/message.rs` | O arm existe em `update()`, mas nenhum widget o dispara; o cancelamento do intervalo ocorre via `ModalCancelado` | --- ## O Que Falta Implementar ### Bugs/lacunas conhecidos 1. **`selecionar_aba.rs` — botões sem diferenciação visual entre aba selecionada e não selecionada** — ambos os branches do `if selecionada` são idênticos (sem estilo diferente). A aba ativa deveria ter destaque visual. 2. **`selecionar_aba.rs` — layout não é aplicável nessa tela** — se o usuário chegou aqui sem ter selecionado um layout previamente, não há como selecionar um layout antes de avançar para ConfigurandoColunas. Considerar adicionar um `pick_list` de layouts na tela `SelecionandoAba`. 3. **Sem tema dark/light** — usa o tema padrão do sistema. Para adicionar: ```rust // Em main.rs: encadear .theme(|app, _| app.tema.clone()) // Em App: campo pub tema: iced::Theme // Mensagem: Message::TemaAlterado(iced::Theme) ``` 4. **Sem drag-and-drop de arquivos** — importação tem apenas o botão de seleção. Verificar suporte nativo em iced 0.13 antes de implementar. 5. **Sem filtro/busca nos resultados** — sem `text_input` para filtrar `faltantes_por_serie`. 6. **`EstadoModal::Informacao` nunca é usado** — definido mas sem chamadas. Remover ou usar. ### Funcionalidades futuras (Fase 6 do plano original) - Histórico de análises (nova tela + schema SQLite v4) - Gráficos de completude por série - Exportação para Excel/CSV --- ## Como Rodar ```bash # Verificar sem compilar binário (rápido) cargo check # Compilar (primeira vez ~1 min por baixar iced + wgpu) cargo build # Executar em modo debug cargo run # Build de release (otimizado, sem console no Windows) cargo build --release ``` --- ## Referências - [iced 0.13 changelog](https://github.com/iced-rs/iced/blob/master/CHANGELOG.md) - [iced exemplos oficiais](https://github.com/iced-rs/iced/tree/master/examples) - [rfd AsyncFileDialog](https://docs.rs/rfd/latest/rfd/struct.AsyncFileDialog.html) - [iced::clipboard::write](https://docs.rs/iced/0.13.1/iced/clipboard/fn.write.html)