Files
comparador-notas/MIGRATION_ICED.md
T
FelipeCN 8d00cfc4d7 Refactor UI components for layout management and results display
- Replaced the `renderizar` function in `layouts.rs` with a new `view` function using Iced for a more modern UI approach.
- Introduced a new `view_secao_layouts` function to handle the display of saved layouts.
- Updated the `resultado.rs` file to use Iced for rendering the results screen, including buttons for actions and pagination controls.
- Created a new `selecionar_aba.rs` file for the selection of XLSX sheet tabs, implementing a preview feature.
- Removed old rendering functions and replaced them with Iced components for better performance and maintainability.
- Added design tokens in `design_tokens.json` for consistent styling across the application.
- Created a mockup HTML file to visualize the UI design.
2026-03-04 10:33:40 -03:00

505 lines
17 KiB
Markdown

# 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<Message>
```
- `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<Message>`.
### 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<Message>`
```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<Message>)
pub fn new() -> (Self, Task<Message>) { ... }
```
### 6. Lifetimes em funções que retornam `Element`
Sempre usar `Element<'_, Message>` (não `Element<Message>`) em funções que recebem referências:
```rust
// ❌ Gera warning mismatched_lifetime_syntaxes
pub fn view(app: &App) -> Element<Message>
// ✅ CORRETO
pub fn view(app: &App) -> Element<'_, Message>
```
**Armadilha com Vec local:** criar `Vec<T>` 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<Mutex<Connection>>`
```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<Arc<Mutex<Connection>>>,
pub banco_foi_recriado: bool,
pub notas_importadas: Vec<Nota>,
pub caminho_arquivo: Option<PathBuf>, // ← 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<String>,
pub layouts_salvos: Vec<Layout>,
pub modal: Option<EstadoModal>,
pub avisos_importacao: Option<ResumoAvisos>,
pub pagina_faltantes: usize,
pub pagina_duplicatas: usize,
pub itens_por_pagina: usize,
pub preview_arquivo: Option<Vec<Vec<String>>>,
pub resultado_anterior: Option<ResultadoAnalise>,
}
```
**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<Mutex<Connection>>, bool, Vec<Layout>), String>),
LayoutsRecarregados(Vec<Layout>),
// Navegação
IrParaImportacao, IrParaConfiguracaoColunas, IrParaLayouts, Voltar,
// Arquivo
SelecionarArquivo,
ArquivoSelecionado(PathBuf),
AbaSelecionada(String),
AbaxlsxCarregadas { caminho: PathBuf, abas: Vec<String>, 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<PathBuf, String>),
// 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)