From 2d1d29ce3d832f43c0fd6a199801f82607960765 Mon Sep 17 00:00:00 2001 From: FelipeCN <57776624+felipecaninnovaes@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:43:43 -0300 Subject: [PATCH] Create a `MIGRATION_ICED.md` --- MIGRATION_ICED.md | 675 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 675 insertions(+) create mode 100644 MIGRATION_ICED.md diff --git a/MIGRATION_ICED.md b/MIGRATION_ICED.md new file mode 100644 index 0000000..145295d --- /dev/null +++ b/MIGRATION_ICED.md @@ -0,0 +1,675 @@ +# 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