# Plano de Migração: egui → iced **Branch:** `change-ui` **Data:** 04/03/2026 **Versão iced alvo:** 0.13 --- ## 1. Escopo da Migração ### O que muda Apenas a camada `src/ui/` é reescrita. Nenhuma outra camada é tocada. | Camada | Status na migração | |-------------------|--------------------| | `src/domain/` | Sem alterações | | `src/application/`| Sem alterações | | `src/infrastructure/` | Sem alterações | | `src/ui/` | Reescrita completa | | `src/main.rs` | Reescrito (entry point muda) | | `Cargo.toml` | Substituição de dependências | --- ## 2. Mudanças no Cargo.toml ### Remover ```toml eframe = "0.31" egui = "0.31" image = { version = "0.25", default-features = false, features = ["ico"] } ``` ### Adicionar ```toml iced = { version = "0.13", features = ["tokio", "image"] } tokio = { version = "1", features = ["full"] } ``` > `rfd` permanece. Continua sendo usado para diálogos de arquivo nativos. > `image` pode ser removido — iced carrega ícones diretamente de bytes. --- ## 3. Arquitetura da UI em iced ### Modelo mental (Elm Architecture) ``` Estado (Model) → view() → Element (o que é renderizado) ↓ Interaction (usuário clica/digita) ↓ Message (enum tipado) ↓ update(msg) → muta o Estado ``` Não existe `&mut self` dentro de closures de renderização. A view é uma função pura que lê o estado e retorna `Element`. Mutações só ocorrem em `update()`. ### Estrutura de arquivos proposta ``` src/ ├── main.rs (entry point iced) ├── ui/ │ ├── mod.rs │ ├── app.rs (struct App, impl Application) │ ├── message.rs (enum Message — todos os eventos) │ ├── screens/ │ │ ├── mod.rs │ │ ├── import.rs (view da tela de importação) │ │ ├── selecionar_aba.rs (view da seleção de aba XLSX) │ │ ├── configuracao_colunas.rs (view do mapeamento de colunas) │ │ ├── resultado.rs (view dos resultados) │ │ └── layouts.rs (view do gerenciamento de layouts) │ └── components/ │ ├── mod.rs │ ├── modal.rs (componente reutilizável de modal) │ ├── tabela_preview.rs (componente de pré-visualização) │ └── paginacao.rs (componente reutilizável de paginação) ``` --- ## 4. Mapeamento: egui atual → iced equivalente ### 4.1 Estado global (`app.rs`) **egui atual:** ```rust pub struct App { pub estado: EstadoApp, // enum de tela ativa pub modal: Modal, // modal hand-rolled pub resultado_pendente: Option>, // ... 15 campos } impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { ... } } ``` **iced equivalente:** ```rust pub struct App { pub estado: EstadoApp, pub modal: Option, // ... mesmos campos de dados } impl iced::Application for App { type Message = Message; type Executor = iced::executor::Tokio; // async nativo type Theme = iced::Theme; type Flags = (); fn new(_flags: ()) -> (Self, Command) { let app = App::default(); (app, Command::perform(inicializar_banco(), Message::BancoInicializado)) } fn title(&self) -> String { "Comparador de Notas".into() } fn update(&mut self, message: Message) -> Command { ... } fn view(&self) -> Element { ... } } ``` **Diferença principal:** `update()` recebe `Message` em vez de operar diretamente sobre eventos de UI. `view()` é somente leitura (`&self`), sem mutações. --- ### 4.2 Enum `EstadoApp` — sem mudanças necessárias O enum pode ser mantido idêntico: ```rust pub enum EstadoApp { Importando, SelecionandoAba { abas: Vec, caminho: PathBuf }, ConfigurandoColunas, ExibindoResultado(ResultadoAnalise), ConfirmandoIntervalo { pre: ResultadoPreAnalise }, GerenciandoLayouts, Analisando, } ``` A diferença é que a navegação entre estados ocorre em `update()`, não dentro de render functions. --- ### 4.3 Sistema de mensagens (`message.rs`) Toda interação do usuário vira uma variante do enum `Message`. Este é o arquivo central da UI em iced. ```rust pub enum Message { // --- Navegação --- IrParaImportacao, IrParaConfiguracaoColunas, IrParaLayouts, Voltar, // --- Arquivo --- SelecionarArquivo, // abre rfd ArquivoSelecionado(PathBuf), AbaSelecionada(String), // --- Background tasks --- AnaliseCompleta(ResultadoPendente), // resultado do tokio task // --- Configuração CSV --- DelimitadorAlterado(char), EncodingAlterado(String), LinhaCabecalhoAlterada(usize), IndiceNumeroAlterado(usize), IndiceSerieAlterado(usize), IndiceValorToggle(bool), IndiceValorAlterado(usize), IndiceDataToggle(bool), IndiceDataAlterada(usize), IndiceDocTipoToggle(bool), IndiceDocTipoAlterado(usize), // --- Configuração 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, // --- Resultado --- PaginaFaltantesAlterada(usize), PaginaDuplicatasAlterada(usize), ItensPorPaginaAlterado(usize), CopiarFaltantes(ChaveSerie), CopiarDuplicatas(ChaveSerie), ExportarPdf, PdfExportado(Result), // --- Layouts --- LayoutSelecionado(i64), SalvarLayout, NomeLayoutAlterado(String), ExcluirLayout(i64), ExclusaoConfirmada(i64), ExportarLayoutJson(i64), ImportarLayoutJson, LayoutJsonImportado(String), // conteúdo do arquivo lido SobrescreverLayout(Layout), // --- Modal --- ModalTextoAlterado(String), ModalConfirmado, ModalCancelado, // --- Banco --- BancoInicializado(Result<(Connection, bool), String>), LayoutsRecarregados(Vec), } ``` --- ### 4.4 Background tasks — substituição do `mpsc::channel` **egui atual (manual):** ```rust // Spawn thread + channel + polling no update() let (tx, rx) = mpsc::channel(); app.resultado_pendente = Some(rx); std::thread::spawn(move || { let res = executar_trabalho(); let _ = tx.send(res); }); // No update(): try_recv() + ctx.request_repaint() ``` **iced equivalente:** ```rust // Em update(), retornar um Command que executa async e produz Message Command::perform( async move { let importado = importar_csv(&caminho, &layout).await; // ... pre_analisar, expandir_analise ResultadoPendente::Concluido { ... } }, Message::AnaliseCompleta ) ``` Sem `mpsc`, sem `request_repaint()`, sem polling. O iced gerencia o executor. **Funções que precisam de wrapper async:** - `importar_csv` → já é síncrona, wrappear em `tokio::task::spawn_blocking` - `importar_xlsx` → idem - `pre_analisar` → idem - `expandir_analise` → idem - `exportar_pdf` → idem --- ### 4.5 Sistema de modal **egui atual:** `Window::anchor(CENTER_CENTER)` simulando modal — não é bloqueante de fato. **iced equivalente:** Overlay real usando `iced::widget::modal` (disponível via `iced_aw` crate) ou implementação própria com `Stack` + `Container` centralizado: ```rust // Componente Modal reutilizável pub enum EstadoModal { Informacao { titulo: String, mensagem: String }, 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 }, } // No view(): fn view_com_modal<'a>(conteudo: Element<'a, Message>, modal: &EstadoModal) -> Element<'a, Message> { // Stack: conteúdo abaixo + overlay escuro + janela modal no centro iced::widget::stack![ conteudo, mouse_area( container(view_modal(modal)) .width(Length::Fill) .height(Length::Fill) .style(|_| container::Style { background: Some(Color::from_rgba(0.0, 0.0, 0.0, 0.5).into()), ..Default::default() }) .center(Length::Fill) ).on_press(Message::ModalCancelado) ].into() } ``` Este modal é **real**: a camada escura captura cliques, não sendo possível interagir com o conteúdo atrás. --- ### 4.6 Drag-and-drop de arquivos **egui atual:** Não suportado. Usuário precisa usar o botão "Selecionar arquivo". **iced equivalente:** ```rust // No view() da tela de importação: iced::widget::drop_zone( container(text("Arraste um arquivo CSV ou XLSX aqui")) .center(Length::Fill) .style(estilo_drop_zone) ) .on_drop(|paths| { paths.into_iter().next() .map(Message::ArquivoSelecionado) .unwrap_or(Message::Noop) }) ``` > `iced::widget::drop_zone` está disponível a partir do iced 0.13. --- ### 4.7 Temas (dark/light mode) **egui atual:** Sem suporte a temas. Usa o tema padrão da plataforma sem controle programático fácil. **iced equivalente:** ```rust fn theme(&self) -> iced::Theme { match self.tema { Tema::Claro => iced::Theme::Light, Tema::Escuro => iced::Theme::Dark, Tema::Sistema => iced::Theme::default(), // detecta preferência do SO } } ``` Adicionar ao estado: ```rust pub tema: Tema, ``` E uma mensagem: ```rust Message::TemaAlterado(Tema), ``` --- ### 4.8 Breadcrumb de etapas **egui atual:** ```rust egui::TopBottomPanel::top("breadcrumb").show(ctx, |ui| { ... }); ``` **iced equivalente:** ```rust fn view_breadcrumb(passo_ativo: usize) -> Element<'static, Message> { let passos = ["Arquivo", "Colunas", "Resultado"]; row(passos.iter().enumerate().map(|(i, label)| { let n = i + 1; let texto = format!("{}. {}", n, label); let estilo = if n == passo_ativo { text::Style::default().color(COR_ATIVO) } else if n < passo_ativo { text::Style::default() } else { text::Style::default().color(COR_FRACO) }; // ... separador " › " entre itens })) .into() } ``` --- ### 4.9 Tabela de pré-visualização **egui atual:** `egui::Grid` com `end_row()` manual, `id_salt` obrigatório. **iced equivalente:** Componente reutilizável via `iced::widget::scrollable` + `column`/`row`: ```rust pub fn tabela_preview(linhas: &[Vec]) -> Element { let num_colunas = linhas.iter().map(|l| l.len()).max().unwrap_or(0); let cabecalho = row( (0..num_colunas).map(|i| { text(format!("{} ({})", indice_para_letra(i), i)) .font(Font::MONOSPACE) .size(12) .width(Length::Fixed(120.0)) .into() }) ); let linhas_view = linhas.iter().map(|linha| { row((0..num_colunas).map(|col| { let celula = linha.get(col).map(|s| s.as_str()).unwrap_or(""); let truncado = truncar(celula, 30); text(truncado).font(Font::MONOSPACE).size(11).width(Length::Fixed(120.0)).into() })).into() }); scrollable( column(std::iter::once(cabecalho.into()).chain(linhas_view)) ) .direction(scrollable::Direction::Both { ... }) .into() } ``` Sem `id_salt`, sem `end_row()`. --- ### 4.10 Paginação **egui atual:** Código duplicado em `renderizar_faltantes` e `renderizar_duplicatas`, estado `pagina_faltantes`/`pagina_duplicatas` separado no `App`. **iced equivalente:** Componente reutilizável: ```rust pub fn controles_paginacao( pagina_atual: usize, total_paginas: usize, msg_anterior: Message, msg_proximo: Message, ) -> Element { row![ button("◀").on_press_maybe((pagina_atual > 0).then_some(msg_anterior)), text(format!("Página {} / {}", pagina_atual + 1, total_paginas)), button("▶").on_press_maybe((pagina_atual + 1 < total_paginas).then_some(msg_proximo)), ] .spacing(8) .into() } ``` --- ### 4.11 Layout right-to-left (botões alinhados à direita) **egui atual:** `ui.with_layout(egui::Layout::right_to_left(...))` — inverte a ordem visual dos botões. **iced equivalente:** ```rust row![ text(layout.nome()), Space::with_width(Length::Fill), // empurra botões para a direita button("📂 Carregar").on_press(...), button("📤 Exportar JSON").on_press(...), button("🗑 Excluir").on_press(...), ] .spacing(8) .align_y(Alignment::Center) ``` Ordem no código = ordem visual. Sem inversão. --- ### 4.12 Diálogos de arquivo (`rfd`) **egui atual:** `rfd::FileDialog::new().pick_file()` chamado diretamente dentro da render function — bloqueia a thread da UI. **iced equivalente:** Chamar via `Command::perform` para não bloquear: ```rust Command::perform( async { rfd::AsyncFileDialog::new().pick_file().await.map(|h| h.path().to_path_buf()) }, |resultado| match resultado { Some(caminho) => Message::ArquivoSelecionado(caminho), None => Message::Noop, } ) ``` > `rfd::AsyncFileDialog` é o equivalente async. Não bloqueia. --- ## 5. Tela por Tela — O Que Reescrever ### 5.1 `import.rs` → `screens/import.rs` | Elemento atual | Equivalente iced | Observação | |---|---|---| | `rfd::FileDialog::pick_file()` síncrono | `rfd::AsyncFileDialog` via `Command::perform` | Não bloqueia | | `egui::ComboBox` para layouts | `iced::widget::pick_list` | API mais simples | | Botão "Selecionar arquivo" | Botão + **drop_zone** | Adiciona drag-and-drop | | Navegação via `app.estado = ...` | `Message::IrParaConfiguracaoColunas` | Via update() | **Novidade:** A zona de drop de arquivos vai nesta tela, substituindo/complementando o botão de seleção. --- ### 5.2 `import.rs::renderizar_selecao_aba` → `screens/selecionar_aba.rs` | Elemento atual | Equivalente iced | |---|---| | `for aba in &abas { ui.selectable_label(...) }` | `column` de `radio` ou `button` por aba | | Pré-visualização da aba | Componente `tabela_preview` reutilizável | --- ### 5.3 `configuracao_colunas.rs` → `screens/configuracao_colunas.rs` | Elemento atual | Equivalente iced | |---|---| | `egui::DragValue` para índices numéricos | `text_input` com validação numérica ou `number_input` (iced_aw) | | `egui::Checkbox` para campos opcionais | `iced::widget::checkbox` | | `egui::ComboBox` para delimitador/encoding/aba | `iced::widget::pick_list` | | `ui.add_enabled_ui(...)` para desabilitar botão | `button(...).on_press_maybe(valido.then_some(msg))` | | `ui.colored_label(RED, erro)` | `text(erro).style(Color::from_rgb(0.8, 0.0, 0.0))` | | Atualização de preview ao mudar delimitador | `Message::DelimitadorAlterado` → `update()` regenera preview | --- ### 5.4 `resultado.rs` → `screens/resultado.rs` | Elemento atual | Equivalente iced | |---|---| | `resultado.clone()` a cada frame | `&self.estado` lido em `view(&self)` sem clone | | Paginação duplicada para faltantes e duplicatas | Componente `controles_paginacao` reutilizável | | `ui.ctx().copy_text(...)` | `iced::clipboard::write(texto)` via Command | | `egui::ScrollArea::vertical()` | `iced::widget::scrollable` | | Exportar PDF via `rfd` síncrono | `rfd::AsyncFileDialog` via `Command::perform` | **Melhoria possível aqui:** barra de busca/filtro por número, que a tela de resultado atual não tem. Adicionar `Message::FiltroBusca(String)` e filtrar `faltantes_por_serie` na view. --- ### 5.5 `layouts.rs` → `screens/layouts.rs` | Elemento atual | Equivalente iced | |---|---| | `ui.with_layout(right_to_left)` para alinhar botões | `row![ Space::Fill, botão1, botão2 ]` | | `rfd` síncrono para importar/exportar JSON | `rfd::AsyncFileDialog` via `Command::perform` | | `egui::ScrollArea::vertical()` | `iced::widget::scrollable` | --- ### 5.6 `app.rs` — Tela "Analisando" **egui atual:** ```rust ui.centered_and_justified(|ui| { ui.label(egui::RichText::new("⏳ Analisando... aguarde.").size(22.0).strong()); }); ``` **iced equivalente:** ```rust container( column![ text("⏳ Analisando... aguarde.").size(22), // Opcional: iced::widget::progress_bar indeterminado ] .align_x(Alignment::Center) ) .center(Length::Fill) .into() ``` --- ## 6. Funcionalidades Novas Habilitadas pela Migração Estas funcionalidades não existem no egui mas ficam acessíveis com iced: | Funcionalidade | Como implementar em iced | |---|---| | **Drag-and-drop de arquivos** | `iced::widget::drop_zone` + `Message::ArquivoSelecionado` | | **Dark/Light mode** | `fn theme(&self) -> iced::Theme` + `Message::TemaAlterado` | | **Busca/filtro nos resultados** | `text_input` + filtrar `faltantes_por_serie` na view | | **Histórico de análises** | Nova tela + tabela + persistência via SQLite (infra não muda) | | **Gráficos** | `plotters` com backend iced ou `iced_charts` | | **Modal real bloqueante** | `Stack` + overlay escuro captura eventos — impede cliques atrás | | **Diálogos não-bloqueantes** | `rfd::AsyncFileDialog` — não trava a UI durante seleção | --- ## 7. O Que Não Muda Tudo fora de `src/ui/` permanece intacto: - `domain/entities/`: `Nota`, `ChaveSerie`, `Layout`, `ResultadoAnalise`, `ResultadoPreAnalise` - `domain/services/`: `detector_sequencia`, `detector_duplicidade`, `parser_monetario` - `domain/errors.rs`: `ErroLayout`, `ResumoAvisos`, todos os erros tipados - `application/usecases/`: `importar_arquivo`, `executar_analise`, `exportar_pdf`, `layouts` - `infrastructure/`: `csv_reader`, `xlsx_reader`, `pdf_generator`, `sqlite/` --- ## 8. Ordem Recomendada de Implementação A migração deve ser feita de forma que o projeto compile e funcione a cada etapa, nunca quebrando por mais de uma sessão de trabalho. ### Fase 1 — Esqueleto (sem funcionalidade) 1. Substituir `eframe`/`egui` por `iced` no `Cargo.toml` 2. Reescrever `main.rs` com `iced::application()` 3. Criar `ui/message.rs` com o enum `Message` completo 4. Criar `ui/app.rs` com struct `App`, impl `Application` mínimo (view retorna `text("em construção")`) 5. Confirmar que compila ### Fase 2 — Tela de Importação 1. Implementar `screens/import.rs` com seleção de arquivo (botão + drop_zone) 2. Implementar `Message::ArquivoSelecionado` em `update()` 3. Dropdown de layouts funcionando 4. Navegação básica entre estados ### Fase 3 — Configuração de Colunas 1. Implementar `screens/configuracao_colunas.rs` 2. Formulário CSV completo com validação 3. Formulário XLSX completo 4. Componente `tabela_preview` 5. Background task de importação via `Command::perform` ### Fase 4 — Resultado 1. Implementar `screens/resultado.rs` 2. Componente `controles_paginacao` reutilizável 3. Copiar para clipboard 4. Exportar PDF via `rfd::AsyncFileDialog` ### Fase 5 — Layouts e Modal 1. Implementar `screens/layouts.rs` 2. Implementar componente `modal.rs` 3. Todas as ações de modal (confirmação, input de texto, erro, aviso) ### Fase 6 — Funcionalidades Novas 1. Dark/Light mode 2. Busca/filtro nos resultados 3. Histórico de análises (requer nova tela + schema SQLite v4) --- ## 9. Riscos e Mitigações | Risco | Probabilidade | Mitigação | |---|---|---| | API do iced quebra em versão minor | Alta (projeto pré-1.0) | Fixar versão exata no Cargo.toml: `iced = "=0.13.x"` | | `iced_aw` (componentes extras) desatualizado | Média | Implementar `number_input` e `modal` internamente se necessário | | Performance com 100.000 registros na view | Baixa | iced tem renderização incremental; usar `lazy` widget para listas longas | | `plotters` + iced requer configuração extra | Média | Avaliar na Fase 6; pode ser substituído por barras simples manuais | | Quebra de compatibilidade na branch `master` | Zero | Branch `change-ui` é isolada; `master` não é afetada | --- ## 10. Referências - Repositório iced: https://github.com/iced-rs/iced - Exemplos oficiais: https://github.com/iced-rs/iced/tree/master/examples - `iced_aw` (componentes extras): https://github.com/iced-rs/iced_aw - `rfd` async: https://docs.rs/rfd/latest/rfd/struct.AsyncFileDialog.html - `plotters` com iced: https://github.com/plotters-rs/plotters-iced