Create a MIGRATION_ICED.md
This commit is contained in:
@@ -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<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
|
||||
Reference in New Issue
Block a user