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

17 KiB

Migração egui → iced: Estado e Referência

Branch: change-ui Versão iced: 0.13.1 Última atualização: 04/03/2026 Status: COMPILANDOcargo 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

// ❌ 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

// ❌ 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

// ❌ 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>

// Pode ser retornado diretamente de update():
return iced::clipboard::write(texto);

5. Entry point usa API funcional, não trait Application

// 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:

// ❌ 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>>

// ❌ 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:

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):

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

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.

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)

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:

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:

// 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):

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:

    // 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

# 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