- 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.
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: ✅ 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, errossrc/application/— casos de usosrc/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 paraConfigurandoColunas
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ó atualizaself.nome_layout_atual(string), não preenche o formulárioLayoutSelecionado(id)— chamaaplicar_layout(), que preenchelayout_csv_atualoulayout_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
-
selecionar_aba.rs— botões sem diferenciação visual entre aba selecionada e não selecionada — ambos os branches doif selecionadasão idênticos (sem estilo diferente). A aba ativa deveria ter destaque visual. -
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 umpick_listde layouts na telaSelecionandoAba. -
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) -
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.
-
Sem filtro/busca nos resultados — sem
text_inputpara filtrarfaltantes_por_serie. -
EstadoModal::Informacaonunca é 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