233 lines
8.1 KiB
Markdown
233 lines
8.1 KiB
Markdown
# Slint UI Migration Plan — VNOX Client
|
|
|
|
## Current Architecture (egui)
|
|
|
|
```
|
|
main.rs
|
|
└─ eframe::run_native() → VnoxApp::update() called every frame
|
|
├─ titlebar::show()
|
|
├─ sidebar::guild_bar()
|
|
├─ sidebar::show()
|
|
├─ members::show()
|
|
├─ CentralPanel:
|
|
│ ├─ Disconnected → connect::show()
|
|
│ └─ Connected → chat::show()
|
|
│ ├─ dm::dm_chat()
|
|
│ ├─ friends::show()
|
|
│ └─ text chat + voice panel
|
|
└─ settings::show_window()
|
|
```
|
|
|
|
State: `UiState` — single large struct (131+ lines) with conn, channels, messages, DMs, guilds, friends, audio settings, voice state. Mutated directly in place every frame.
|
|
|
|
UI: 80+ Rust files in `ui/`, all immediate-mode egui. Every widget redrawn every frame.
|
|
|
|
---
|
|
|
|
## Target Architecture (Slint)
|
|
|
|
```
|
|
main.rs
|
|
└─ slint::MainWindow::new() → .run()
|
|
│
|
|
├─ .slint files (declarative UI)
|
|
│ ├─ main.slint — window layout
|
|
│ ├─ sidebar.slint — guild bar + channel list
|
|
│ ├─ chat.slint — message list + input bar
|
|
│ ├─ voice.slint — voice channel panel
|
|
│ ├─ connect.slint — connect/bookmarks screen
|
|
│ ├─ settings.slint — settings dialog
|
|
│ └─ theme.slint — color/font/spacing definitions
|
|
│
|
|
└─ Rust code (logic only)
|
|
├─ app.rs — VnoxApp, init, event wiring
|
|
├─ bridge.rs — UiState ↔ Slint property sync
|
|
├─ state/ — UiState (unchanged, slimmed)
|
|
└─ net/ audio/ — no changes
|
|
```
|
|
|
|
Key difference: Slint only re-renders changed properties. UI is declared once in `.slint` files. Rust code pushes state updates via `in-out property` setters.
|
|
|
|
---
|
|
|
|
## Migration Phases
|
|
|
|
### Phase 1 — Infrastructure (day 1)
|
|
|
|
1. Add `slint` crate to `Cargo.toml`:
|
|
```toml
|
|
slint = { version = "1", features = ["backend-winit", "compat-1-2"] }
|
|
```
|
|
|
|
2. Create `ui/main.slint` — empty window with a placeholder text
|
|
|
|
3. Rewrite `main.rs`:
|
|
- Remove `eframe::run_native()`
|
|
- Create `slint::MainWindow`
|
|
- Spawn tokio runtime alongside slint event loop
|
|
|
|
4. Keep all egui code in repo (deleted only at the end)
|
|
|
|
**Result:** App boots in Slint window, shows placeholder. All real UI still in egui waiting to be ported.
|
|
|
|
---
|
|
|
|
### Phase 2 — Sidebar (day 2)
|
|
|
|
Sidebar is the most isolated component — no complex interaction, just a list of channels + guild bar + user bar.
|
|
|
|
```slint
|
|
// theme.slint
|
|
export global Theme {
|
|
in-out property <color> bg-surface: #1e1e2e;
|
|
in-out property <color> bg-base: #181825;
|
|
in-out property <color> accent: #cba6f7;
|
|
in-out property <color> text-primary: #cdd6f4;
|
|
in-out property <length> radius: 8px;
|
|
}
|
|
|
|
// sidebar.slint
|
|
component Sidebar { /* guild icons, channel list, user bar */ }
|
|
```
|
|
|
|
Rust bridge:
|
|
```rust
|
|
fn sync_sidebar(window: &MainWindow, state: &UiState) {
|
|
let channels: Vec<Channel> = state.channels.iter().map(|c| Channel { .. }).collect();
|
|
window.set_channels(ModelRc::from(VecModel::from(channels)));
|
|
window.on_channel_selected(|id| { /* net.send(JoinChannel { id }) */ });
|
|
}
|
|
```
|
|
|
|
**Files to create:** `ui/theme.slint`, `ui/sidebar.slint`
|
|
**Files to remove (end of phase):** `ui/sidebar/mod.rs`, `ui/sidebar/channel_list/`, `ui/sidebar/sections/`, `ui/sidebar/guild_bar/`
|
|
|
|
---
|
|
|
|
### Phase 3 — Chat (days 3-4)
|
|
|
|
The complex one: text messages, reactions, editing, typing indicators, reply chain, DMs, friends list.
|
|
|
|
```slint
|
|
component ChatPanel {
|
|
in-out property <[Message]> messages;
|
|
in-out property <string> input-text;
|
|
signal send-message(string);
|
|
signal load-history(string);
|
|
|
|
VerticalLayout {
|
|
ListView { /* scrollable messages */ }
|
|
HorizontalLayout {
|
|
LineEdit { text <=> input-text; }
|
|
Button { text: "Send"; }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Sub-steps:**
|
|
1. Text chat panel (messages + input)
|
|
2. Message context menu (react/edit/delete/reply/copy)
|
|
3. DM panel
|
|
4. Friends list + requests
|
|
5. Voice channel panel (speaking indicators, members)
|
|
6. Member list (right sidebar)
|
|
|
|
**Files to create:** `ui/chat.slint`, `ui/voice.slint`
|
|
**Files to remove (end of phase):** `ui/chat/`, `ui/members/`
|
|
|
|
---
|
|
|
|
### Phase 4 — Connect Screen (day 4)
|
|
|
|
```slint
|
|
component ConnectScreen {
|
|
in-out property <[Bookmark]> bookmarks;
|
|
in-out property <string> address-input;
|
|
signal connect(string);
|
|
signal remove-bookmark(int);
|
|
}
|
|
```
|
|
|
|
**Files to create:** `ui/connect.slint`
|
|
**Files to remove (end of phase):** `ui/connect/`
|
|
|
|
---
|
|
|
|
### Phase 5 — Settings + Polish (day 5)
|
|
|
|
Settings dialog with tabs: Audio, Network, Identity, Appearance.
|
|
|
|
**Files to create:** `ui/settings.slint`
|
|
**Files to remove (end of phase):** `ui/settings/`, `ui/theme/`, `ui/state/` (partially)
|
|
|
|
Final cleanup: remove `eframe` from `Cargo.toml`, delete all `ui/*mod.rs` that re-export egui components.
|
|
|
|
---
|
|
|
|
## Data Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ slint::MainWindow │
|
|
│ ┌───────────────────┐ ┌────────────────────────────┐ │
|
|
│ │ .slint UI │ │ Rust Backend │ │
|
|
│ │ │ │ │ │
|
|
│ │ property <value> │←──→ window.set_value() │ │
|
|
│ │ signal event() │───→ window.on_event(cb) │ │
|
|
│ └───────────────────┘ └────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────┐ │
|
|
│ │ UiState │ │
|
|
│ │ (Rust struct)│ │
|
|
│ └──────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────┐ │
|
|
│ │ NetHandle │ │
|
|
│ │ (tokio task) │ │
|
|
│ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
The bridge pattern:
|
|
- `NetHandle` receives events from gateway → updates `UiState`
|
|
- Every 16ms (60fps) or on event, `sync_ui()` reads `UiState` and calls `window.set_*()` methods
|
|
- User clicks in Slint → `signal` fires → Rust callback → `net.send(NetCommand::...)`
|
|
|
|
---
|
|
|
|
## Dependency changes
|
|
|
|
| Dependency | Action |
|
|
|------------|--------|
|
|
| `eframe` | Remove (end of Phase 5) |
|
|
| `egui` | Remove (end of Phase 5) |
|
|
| `slint` | Add (Phase 1) |
|
|
| `tokio` | Keep |
|
|
| `cpal` | Keep |
|
|
| `rodio` | Keep |
|
|
| `opus` | Keep |
|
|
|
|
---
|
|
|
|
## What stays the same
|
|
|
|
- **`net/`** — all networking code, `NetHandle`, `NetCommand`, `NetEvent`
|
|
- **`audio/`** — capture/playback pipeline, Opus encode/decode, RNNoise
|
|
- **`identity.rs`**, **`identity_vault.rs`** — keypair management
|
|
- **`jitter/`** — jitter buffer (copied from voice-node)
|
|
|
|
---
|
|
|
|
## Timeline estimate
|
|
|
|
| Phase | Scope | Time |
|
|
|-------|-------|------|
|
|
| 1 | Infrastructure (slint init, window, wiring) | 1 day |
|
|
| 2 | Sidebar (guild bar + channel list + user bar) | 1 day |
|
|
| 3 | Chat + DMs + Friends + Voice panel | 2 days |
|
|
| 4 | Connect screen + bookmarks | 0.5 day |
|
|
| 5 | Settings + theme + cleanup | 0.5 day |
|
|
| **Total** | | **~5 days** |
|