676 lines
20 KiB
Markdown
676 lines
20 KiB
Markdown
# 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<mpsc::Receiver<ResultadoPendente>>,
|
||
// ... 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<EstadoModal>,
|
||
// ... 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<Message>) {
|
||
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<Message> { ... }
|
||
|
||
fn view(&self) -> Element<Message> { ... }
|
||
}
|
||
```
|
||
|
||
**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<String>, 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<PathBuf, String>),
|
||
|
||
// --- 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<Layout>),
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 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<String>]) -> Element<Message> {
|
||
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<Message> {
|
||
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
|