# 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 bg-surface: #1e1e2e; in-out property bg-base: #181825; in-out property accent: #cba6f7; in-out property text-primary: #cdd6f4; in-out property 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 = 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 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 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 │←──→ 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** |