diff --git a/.github/workflows/build-gpui.yaml b/.github/workflows/build-gpui.yaml new file mode 100644 index 0000000..106cb48 --- /dev/null +++ b/.github/workflows/build-gpui.yaml @@ -0,0 +1,133 @@ +--- +#------------------------------------------------------------------------------- +# Workflow configuration for GPUI desktop app +#------------------------------------------------------------------------------- + +name: "GPUI Desktop Build" +on: + pull_request: + paths: + - ".github/workflows/build-gpui.yaml" + - "Cargo.toml" + - "crates/wsrx-desktop-gpui/**" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +#------------------------------------------------------------------------------- +# Workflow jobs +#------------------------------------------------------------------------------- + +jobs: + build-linux: + name: "Build GPUI Desktop on Linux" + runs-on: ubuntu-22.04 + steps: + # Checkout repository + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + # Get version + - name: Get git version + id: git_tag_version + run: | + export BUILD_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0-dev") + echo "Build at version $BUILD_VERSION" + echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_OUTPUT + + # Install dependencies + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libgl1-mesa-dev + + # Build application + - name: Build GPUI desktop application + run: | + rustup update stable && rustup default stable + cargo build --release -p wsrx-desktop-gpui + + # Upload package + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: wsrx-desktop-gpui-${{steps.git_tag_version.outputs.BUILD_VERSION}}-linux-x64 + path: target/release/wsrx-desktop-gpui + compression-level: 6 + + build-windows: + name: "Build GPUI Desktop on Windows" + runs-on: windows-2022 + steps: + # Checkout repository + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + # Get version + - name: Get git version + id: git_tag_version + run: | + echo BUILD_VERSION=$(git describe --tags --abbrev=0 2>$null) | Out-File -FilePath $env:GITHUB_OUTPUT -Append + if ($LASTEXITCODE -ne 0) { + echo "BUILD_VERSION=v0.0.0-dev" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + } + + - name: Install NASM for aws-lc-rs + uses: ilammy/setup-nasm@v1 + + - name: Install ninja-build + uses: seanmiddleditch/gha-setup-ninja@v5 + + # Build application + - name: Build GPUI desktop application + run: | + rustup update stable && rustup default stable + cargo build --release -p wsrx-desktop-gpui + + # Upload package + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: wsrx-desktop-gpui-${{steps.git_tag_version.outputs.BUILD_VERSION}}-windows-x64 + path: target/release/wsrx-desktop-gpui.exe + compression-level: 6 + + build-mac: + name: "Build GPUI Desktop on MacOS" + runs-on: macos-latest + steps: + # Checkout repository + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + # Get version + - name: Get git version + id: git_tag_version + run: | + export BUILD_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0-dev") + echo "Build at version $BUILD_VERSION" + echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_OUTPUT + + # Build for ARM64 + - name: Build GPUI desktop for ARM64 + run: | + rustup update stable && rustup default stable + cargo build --release -p wsrx-desktop-gpui + + # Upload ARM64 package + - name: Upload ARM64 package + uses: actions/upload-artifact@v4 + with: + name: wsrx-desktop-gpui-${{steps.git_tag_version.outputs.BUILD_VERSION}}-macos-arm64 + path: target/release/wsrx-desktop-gpui + compression-level: 6 diff --git a/crates/wsrx-desktop-gpui/Cargo.toml b/crates/wsrx-desktop-gpui/Cargo.toml index aec624d..b000485 100644 --- a/crates/wsrx-desktop-gpui/Cargo.toml +++ b/crates/wsrx-desktop-gpui/Cargo.toml @@ -50,6 +50,9 @@ chrono = { workspace = true } thiserror = { workspace = true } url = { workspace = true } +# Internationalization +rust-i18n = "3" + # Network axum = { workspace = true } reqwest = { workspace = true } @@ -65,6 +68,7 @@ build-target = { workspace = true } git-version = { workspace = true } rustc_version = { workspace = true } winres = { workspace = true } +rust-i18n = "3" [[bin]] name = "wsrx-desktop-gpui" diff --git a/crates/wsrx-desktop-gpui/IMPLEMENTATION_CHECKLIST.md b/crates/wsrx-desktop-gpui/IMPLEMENTATION_CHECKLIST.md new file mode 100644 index 0000000..0e8e8f4 --- /dev/null +++ b/crates/wsrx-desktop-gpui/IMPLEMENTATION_CHECKLIST.md @@ -0,0 +1,211 @@ +# GPUI vs Slint Implementation Checklist + +This document compares the GPUI implementation with the original Slint implementation to track progress and identify gaps. + +## Design System / Styling + +### Colors & Theming +- [x] **Dark mode palette** - Fully aligned with Slint colors + - [x] Window foreground (#cdd6f4) + - [x] Window background (#151515) + - [x] Window alternate background (#1e1e1e) + - [x] Primary background (#0078D6) + - [x] Border colors (window, element, popup) + - [x] Semantic colors (error, warning, success, info, debug) + - [x] Layer colors (layer-1 through layer-5) +- [ ] **Light mode palette** - Not yet implemented (Slint has it) +- [ ] **Theme switching** - No auto-detect or toggle mechanism yet + +### Typography +- [x] **Font sizes** - Aligned with Slint sizing (16px base) + - [x] XS (12px), SM (14px), Base (16px), LG (18px), XL (20px), 2XL (24px) +- [ ] **Font family** - Not set (Slint uses "Reverier Mono") +- [ ] **Font weight variations** - Not implemented + +### Spacing System +- [x] **Padding constants** (p-xs through p-xl) - Fully aligned +- [x] **Spacing constants** (s-xs through s-xl) - Fully aligned +- [x] **Border radius** (r-xs through r-xl) - Implemented +- [x] **Height constants** (h-xs through h-xl) - Implemented +- [ ] **Line height** - Not explicitly defined + +### Animations & Transitions +- [ ] **Duration constants** - Not implemented (Slint has short/mid/long) +- [ ] **Easing functions** - Not implemented +- [ ] **Animated transitions** - Not implemented (Slint has smooth transitions) + +## Layout & Structure + +### Main Window +- [x] **Root view** - Implemented with Entity management +- [x] **Sidebar** - Implemented with navigation +- [x] **Main content area** - Implemented with page switching +- [ ] **Frameless window** - Not implemented (Slint has custom window chrome) +- [ ] **Window controls** - Placeholder only (minimize, maximize, close) +- [ ] **Title bar** - Placeholder only + +### Sidebar +- [x] **Navigation tabs** - Implemented with 4 pages +- [x] **Active state indicator** - Implemented with left border + highlight +- [x] **Hover effects** - Implemented +- [ ] **Logo/branding** - Not displayed +- [ ] **Icons** - Not implemented (Slint uses SVG icons) +- [ ] **Scope selector** - Not implemented (Slint has scope dropdown) +- [ ] **System info display** - Not implemented in sidebar + +## Pages / Views + +### Get Started Page +- [x] **Basic structure** - Placeholder implemented +- [ ] **Welcome message** - Not styled/detailed +- [ ] **Update notification** - Not implemented +- [ ] **Quick actions** - Not implemented +- [ ] **Onboarding content** - Not implemented + +### Connections Page +- [x] **Tunnel list** - Basic structure implemented +- [x] **Empty state** - Implemented +- [x] **Status indicators** - Color-coded dots +- [x] **Add tunnel button** - Implemented (no functionality) +- [ ] **Tunnel cards styling** - Basic, needs polish +- [ ] **Edit/Delete actions** - Not implemented +- [ ] **Enable/Disable toggle** - Not implemented +- [ ] **Connection statistics** - Not displayed +- [ ] **Scope filtering** - Not implemented + +### Network Logs Page +- [x] **Log display** - Basic list implemented +- [x] **Severity color coding** - Implemented (DEBUG, INFO, WARN, ERROR) +- [x] **Clear button** - Implemented +- [ ] **Log filtering** - Not implemented +- [ ] **Auto-scroll toggle** - Not implemented +- [ ] **Log export** - Not implemented +- [ ] **Timestamp formatting** - Basic string display +- [ ] **Search/filter** - Not implemented + +### Settings Page +- [x] **Settings sections** - Basic structure implemented +- [x] **Settings display** - Read-only display +- [ ] **Interactive controls** - Not implemented (toggles, selects) +- [ ] **Daemon settings** - Display only +- [ ] **Theme toggle** - Not implemented +- [ ] **Log level selector** - Not implemented +- [ ] **Save/Apply buttons** - Not implemented +- [ ] **Settings persistence** - Bridge exists but not connected + +## Components Library + +### Implemented Components +- [x] **Button** - With variants (Primary, Secondary, Danger) - *needs Zed-style refactor* +- [x] **IconButton** - Icon-only button with styles (Subtle, Filled, Danger) - *NEW* +- [x] **Checkbox** - Interactive checkbox with label - *NEW* +- [x] **Modal** - Dialog overlay with backdrop +- [x] **Input** - Text input with placeholder (not fully functional) +- [x] **StatusIndicator** - Color-coded status dots + +### Missing Components (Slint has) +- [ ] **ButtonIndicator** - Button with active state indicator +- [ ] **LineEdit** - Functional text input with editing +- [ ] **ScrollView** - Scrollable container +- [x] **ComboBox/Select** - Dropdown selection +- [ ] **Tab control** - Tabbed interface +- [ ] **Progress bar** - Loading indicator +- [ ] **Tooltip** - Hover info display + +**Total**: 8 of 14 components implemented (57%) + +### Component Improvements Needed +- [x] **Add prelude module** - Export common types and traits - *DONE* +- [x] **Refactor Button** - Follow Zed's pattern with traits (Clickable, Disableable, etc.) - *DONE* +- [x] **Add component traits** - Clickable, Disableable, Fixed, StyledExt, Toggleable - *DONE* +- [ ] **Improve styling** - Use consistent spacing/color helpers + +## Bridge Layer / Integration + +### Implemented Bridges +- [x] **DaemonBridge** - Basic structure (no actual daemon control) +- [x] **SettingsBridge** - TOML persistence (not connected) +- [x] **SystemInfoBridge** - CPU/memory monitoring (not displayed) + +### Missing Functionality +- [ ] **Daemon start/stop** - Not functional +- [ ] **Tunnel management** - Not connected to wsrx core +- [ ] **Real-time log streaming** - Not implemented +- [ ] **Settings load/save UI** - Not wired up +- [ ] **System info display** - Bridge exists but not shown +- [ ] **WebSocket communication** - Not implemented +- [ ] **Scope management** - Not implemented + +## Internationalization +- [ ] **i18n support** - Not implemented (Slint has @tr() macros) +- [ ] **Language switching** - Not implemented +- [ ] **Translation files** - Not created + +## Platform-Specific Features + +### macOS +- [x] **Build configuration** - Fixed linker flag issue +- [ ] **Custom window chrome** - Not implemented +- [ ] **Title bar handling** - Not implemented +- [ ] **DMG packaging** - Not set up for GPUI app + +### Windows +- [x] **Build configuration** - Working with NASM/Ninja +- [ ] **NSIS installer** - Not set up for GPUI app +- [ ] **Portable package** - Not set up +- [ ] **Window chrome** - Not implemented + +### Linux +- [x] **Build configuration** - Working with X11 dependencies +- [ ] **AppImage** - Not set up for GPUI app +- [ ] **Desktop file** - Not created +- [ ] **Window chrome** - Not implemented + +## Build & Deployment +- [x] **GitHub workflow** - Created for Linux/Windows/macOS +- [x] **macOS build fix** - Removed unsupported linker flag +- [ ] **Artifact generation** - Workflow ready but untested +- [ ] **Release automation** - Not configured +- [ ] **Code signing** - Not configured +- [ ] **Update mechanism** - Not implemented + +## Code Quality & Patterns + +### Following Zed Patterns +- [x] **Component prelude** - Implemented with common imports - *DONE* +- [ ] **Component traits** - Not implemented (Clickable, Disableable, etc.) +- [ ] **Styled extensions** - Partial (could be more comprehensive) +- [ ] **Builder patterns** - Basic (needs enhancement) +- [ ] **Documentation** - Minimal (Zed has extensive docs) + +### Needed Improvements +- [ ] **Refactor Button** - Use Zed's ButtonLike + traits pattern +- [ ] **Add animation helpers** - Duration, easing, direction +- [ ] **Color system** - Add Color enum like Zed +- [ ] **Spacing helpers** - h_flex, v_flex, h_group, v_group +- [ ] **Typography traits** - StyledTypography trait + +## Summary Statistics + +**Overall Progress**: ~50% complete (up from 48%) + +### By Category: +- **Design System**: 60% (colors good, animations missing) +- **Layout**: 65% (structure done, window controls working) +- **Pages**: 40% (structure exists, functionality missing) +- **Components**: 50% (7/14 components, prelude added) +- **Bridges**: 40% (structure exists, not functional) +- **i18n**: 30% (framework setup, macro issues) +- **Platform Features**: 30% (build fixes, window config aligned) +- **Build System**: 85% (macOS fixed, workflow ready) +- **Code Patterns**: 30% (prelude added, need traits) + +### Priority Items for Next Phase: +1. **✅ Fix macOS build** - DONE (removed unsupported linker flag) +2. **Refactor Button with Zed patterns** - Add traits and builder methods +3. **Add component prelude** - Export common types and traits +4. **Implement missing components** - Focus on Checkbox, Select first +5. **Wire up bridges** - Make daemon control functional +6. **Add animations** - Duration constants and transitions +7. **Improve documentation** - Add examples and usage docs +8. **Test build artifacts** - Verify workflow produces working binaries diff --git a/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md b/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md index 8059206..cc3cab6 100644 --- a/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md +++ b/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md @@ -216,23 +216,25 @@ crates/wsrx-desktop-gpui/ ├── Cargo.toml # GPUI dependency configuration ├── build.rs # Build script ├── src/ -│ ├── main.rs # Application entry point -│ ├── lib.rs # Library root +│ ├── main.rs # Application entry point with i18n macro │ ├── logging.rs # Logging initialization +│ ├── i18n.rs # Internationalization setup │ ├── models/mod.rs # Data model definitions │ ├── styles/mod.rs # Themes and styles │ ├── views/ │ │ ├── mod.rs -│ │ ├── root.rs # Placeholder -│ │ ├── get_started.rs # Placeholder -│ │ ├── connections.rs # Placeholder -│ │ ├── network_logs.rs # Placeholder -│ │ ├── settings.rs # Placeholder -│ │ └── sidebar.rs # Placeholder +│ │ ├── root.rs # Main window root view +│ │ ├── get_started.rs # Onboarding page +│ │ ├── connections.rs # Tunnel management +│ │ ├── network_logs.rs # Log display +│ │ ├── settings.rs # Settings page +│ │ └── sidebar.rs # Navigation sidebar │ ├── components/ │ │ ├── mod.rs -│ │ ├── title_bar.rs # Placeholder -│ │ ├── window_controls.rs # Placeholder +│ │ ├── prelude.rs # Common component imports +│ │ ├── title_bar.rs # Window title bar with drag support +│ │ ├── window_controls.rs # Platform-aware window controls + │ │ └── tab_navigation.rs # Placeholder │ └── bridges/ │ ├── mod.rs diff --git a/crates/wsrx-desktop-gpui/build.rs b/crates/wsrx-desktop-gpui/build.rs index 46024be..806c8fb 100644 --- a/crates/wsrx-desktop-gpui/build.rs +++ b/crates/wsrx-desktop-gpui/build.rs @@ -40,8 +40,5 @@ pub const TARGET_ARCH: &str = "{arch}"; } } - #[cfg(target_os = "macos")] - { - println!("cargo:rustc-link-arg=-fapple-link-runtime"); - } + // No special macOS linker flags needed - GPUI handles this internally } diff --git a/crates/wsrx-desktop-gpui/icons/arrow-sync-off.svg b/crates/wsrx-desktop-gpui/icons/arrow-sync-off.svg new file mode 100644 index 0000000..3c9d615 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/arrow-sync-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/arrow-up-right.svg b/crates/wsrx-desktop-gpui/icons/arrow-up-right.svg new file mode 100644 index 0000000..6afce3b --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/arrow-up-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/checkbox-unchecked.svg b/crates/wsrx-desktop-gpui/icons/checkbox-unchecked.svg new file mode 100644 index 0000000..4805c19 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/checkbox-unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/checkmark.svg b/crates/wsrx-desktop-gpui/icons/checkmark.svg new file mode 100644 index 0000000..6400e71 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/code.svg b/crates/wsrx-desktop-gpui/icons/code.svg new file mode 100644 index 0000000..b8353c3 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/code.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/dismiss.svg b/crates/wsrx-desktop-gpui/icons/dismiss.svg new file mode 100644 index 0000000..14099b5 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/dismiss.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/globe-star.svg b/crates/wsrx-desktop-gpui/icons/globe-star.svg new file mode 100644 index 0000000..7a29807 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/globe-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/home.svg b/crates/wsrx-desktop-gpui/icons/home.svg new file mode 100644 index 0000000..3107f93 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/lock-closed.svg b/crates/wsrx-desktop-gpui/icons/lock-closed.svg new file mode 100644 index 0000000..76f3b30 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/lock-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/logo-stroked.svg b/crates/wsrx-desktop-gpui/icons/logo-stroked.svg new file mode 100644 index 0000000..b24b4e5 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/logo-stroked.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/crates/wsrx-desktop-gpui/icons/logo.svg b/crates/wsrx-desktop-gpui/icons/logo.svg new file mode 100644 index 0000000..b622af6 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/wsrx-desktop-gpui/icons/maximize.svg b/crates/wsrx-desktop-gpui/icons/maximize.svg new file mode 100644 index 0000000..9b49186 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/maximize.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/navigation.svg b/crates/wsrx-desktop-gpui/icons/navigation.svg new file mode 100644 index 0000000..9b968e0 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/navigation.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/settings.svg b/crates/wsrx-desktop-gpui/icons/settings.svg new file mode 100644 index 0000000..fddb389 --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/subtract.svg b/crates/wsrx-desktop-gpui/icons/subtract.svg new file mode 100644 index 0000000..2f33f0b --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/subtract.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/icons/warning.svg b/crates/wsrx-desktop-gpui/icons/warning.svg new file mode 100644 index 0000000..4f4301c --- /dev/null +++ b/crates/wsrx-desktop-gpui/icons/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/wsrx-desktop-gpui/locales/en.toml b/crates/wsrx-desktop-gpui/locales/en.toml new file mode 100644 index 0000000..4b5394c --- /dev/null +++ b/crates/wsrx-desktop-gpui/locales/en.toml @@ -0,0 +1,24 @@ +# English translations +get_started = "Get Started" +network_logs = "Network Logs" +connections = "Connections" +settings = "Settings" + +# Buttons and actions +add_tunnel = "Add Tunnel" +clear_logs = "Clear Logs" + +# Status messages +no_tunnels = "No tunnels configured" +no_logs = "No logs available" + +# Settings sections +application = "Application" +appearance = "Appearance" +logging = "Logging" +about = "About" + +# About info +app_name = "WebSocket Reflector X" +app_description = "TCP-over-WebSocket tunneling tool" +version = "Version" diff --git a/crates/wsrx-desktop-gpui/locales/zh-CN.toml b/crates/wsrx-desktop-gpui/locales/zh-CN.toml new file mode 100644 index 0000000..4b1c81e --- /dev/null +++ b/crates/wsrx-desktop-gpui/locales/zh-CN.toml @@ -0,0 +1,24 @@ +# Chinese Simplified translations +get_started = "开始" +network_logs = "网络日志" +connections = "连接" +settings = "设置" + +# Buttons and actions +add_tunnel = "添加隧道" +clear_logs = "清除日志" + +# Status messages +no_tunnels = "未配置隧道" +no_logs = "无可用日志" + +# Settings sections +application = "应用程序" +appearance = "外观" +logging = "日志" +about = "关于" + +# About info +app_name = "WebSocket Reflector X" +app_description = "TCP-over-WebSocket 隧道工具" +version = "版本" diff --git a/crates/wsrx-desktop-gpui/src/bridges/daemon.rs b/crates/wsrx-desktop-gpui/src/bridges/daemon.rs index 051c3d9..9cb1f33 100644 --- a/crates/wsrx-desktop-gpui/src/bridges/daemon.rs +++ b/crates/wsrx-desktop-gpui/src/bridges/daemon.rs @@ -1,6 +1,77 @@ // Daemon bridge - Communication with wsrx daemon subprocess +use std::sync::Arc; + +use anyhow::Result; +use tokio::sync::Mutex; + +/// Status of the daemon process +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DaemonStatus { + Stopped, + Starting, + Running, + Stopping, + Error, +} + +/// Bridge for managing the wsrx daemon subprocess pub struct DaemonBridge { - // TODO: Define state + /// Current daemon status + status: Arc>, + + /// Process handle (when running) + #[allow(dead_code)] + process: Arc>>, +} + +impl DaemonBridge { + /// Create a new daemon bridge + pub fn new() -> Self { + Self { + status: Arc::new(Mutex::new(DaemonStatus::Stopped)), + process: Arc::new(Mutex::new(None)), + } + } + + /// Get current daemon status + pub async fn status(&self) -> DaemonStatus { + *self.status.lock().await + } + + /// Start the daemon process + pub async fn start(&self) -> Result<()> { + let mut status = self.status.lock().await; + *status = DaemonStatus::Starting; + + // TODO: Implement actual daemon startup + // This will spawn the wsrx daemon process and monitor it + + *status = DaemonStatus::Running; + Ok(()) + } + + /// Stop the daemon process + pub async fn stop(&self) -> Result<()> { + let mut status = self.status.lock().await; + *status = DaemonStatus::Stopping; + + // TODO: Implement actual daemon shutdown + // This will gracefully stop the daemon process + + *status = DaemonStatus::Stopped; + Ok(()) + } + + /// Restart the daemon process + pub async fn restart(&self) -> Result<()> { + self.stop().await?; + self.start().await?; + Ok(()) + } } -// Placeholder - Will be implemented in next phase +impl Default for DaemonBridge { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/wsrx-desktop-gpui/src/bridges/mod.rs b/crates/wsrx-desktop-gpui/src/bridges/mod.rs index 87e2b40..73b9dbb 100644 --- a/crates/wsrx-desktop-gpui/src/bridges/mod.rs +++ b/crates/wsrx-desktop-gpui/src/bridges/mod.rs @@ -1,10 +1,7 @@ // Bridges - Integration layer between UI and core functionality -// This module contains the bridges that connect the UI to the wsrx daemon and other services +// This module contains the bridges that connect the UI to the wsrx daemon and +// other services pub mod daemon; pub mod settings; pub mod system_info; - -pub use daemon::DaemonBridge; -pub use settings::SettingsBridge; -pub use system_info::SystemInfoBridge; diff --git a/crates/wsrx-desktop-gpui/src/bridges/settings.rs b/crates/wsrx-desktop-gpui/src/bridges/settings.rs index 1e59c42..ffb57e9 100644 --- a/crates/wsrx-desktop-gpui/src/bridges/settings.rs +++ b/crates/wsrx-desktop-gpui/src/bridges/settings.rs @@ -1,6 +1,56 @@ // Settings bridge - Application settings persistence +use std::path::PathBuf; + +use anyhow::Result; +use directories::ProjectDirs; + +use crate::models::Settings; + +/// Bridge for managing application settings persistence pub struct SettingsBridge { - // TODO: Define state + /// Path to settings file + settings_path: PathBuf, +} + +impl SettingsBridge { + /// Create a new settings bridge + pub fn new() -> Result { + let settings_path = Self::get_settings_path()?; + Ok(Self { settings_path }) + } + + /// Get the settings file path + fn get_settings_path() -> Result { + let proj_dirs = ProjectDirs::from("org", "xdsec", "wsrx-desktop-gpui") + .ok_or_else(|| anyhow::anyhow!("Could not determine settings directory"))?; + + let config_dir = proj_dirs.config_dir(); + std::fs::create_dir_all(config_dir)?; + + Ok(config_dir.join("settings.toml")) + } + + /// Load settings from file + pub fn load(&self) -> Result { + if !self.settings_path.exists() { + return Ok(Settings::default()); + } + + let content = std::fs::read_to_string(&self.settings_path)?; + let settings: Settings = toml::from_str(&content)?; + Ok(settings) + } + + /// Save settings to file + pub fn save(&self, settings: &Settings) -> Result<()> { + let content = toml::to_string_pretty(settings)?; + std::fs::write(&self.settings_path, content)?; + Ok(()) + } } -// Placeholder - Will be implemented in next phase +impl Default for SettingsBridge { + fn default() -> Self { + Self::new().expect("Failed to initialize settings bridge") + } +} diff --git a/crates/wsrx-desktop-gpui/src/bridges/system_info.rs b/crates/wsrx-desktop-gpui/src/bridges/system_info.rs index 58d7e01..072e987 100644 --- a/crates/wsrx-desktop-gpui/src/bridges/system_info.rs +++ b/crates/wsrx-desktop-gpui/src/bridges/system_info.rs @@ -1,6 +1,76 @@ // System info bridge - System resource monitoring +use std::sync::{Arc, Mutex}; + +use sysinfo::System; + +/// System information and monitoring pub struct SystemInfoBridge { - // TODO: Define state + /// System info handle + system: Arc>, +} + +impl SystemInfoBridge { + /// Create a new system info bridge + pub fn new() -> Self { + let mut system = System::new_all(); + system.refresh_all(); + + Self { + system: Arc::new(Mutex::new(system)), + } + } + + /// Get CPU usage percentage + pub fn cpu_usage(&self) -> f32 { + let mut system = self.system.lock().unwrap(); + system.refresh_cpu_all(); + + let cpus = system.cpus(); + if cpus.is_empty() { + return 0.0; + } + + cpus.iter().map(|cpu| cpu.cpu_usage()).sum::() / cpus.len() as f32 + } + + /// Get memory usage percentage + pub fn memory_usage(&self) -> f32 { + let mut system = self.system.lock().unwrap(); + system.refresh_memory(); + + let total = system.total_memory(); + let used = system.used_memory(); + + if total == 0 { + return 0.0; + } + + (used as f32 / total as f32) * 100.0 + } + + /// Get total memory in bytes + pub fn total_memory(&self) -> u64 { + let mut system = self.system.lock().unwrap(); + system.refresh_memory(); + system.total_memory() + } + + /// Get used memory in bytes + pub fn used_memory(&self) -> u64 { + let mut system = self.system.lock().unwrap(); + system.refresh_memory(); + system.used_memory() + } + + /// Refresh all system information + pub fn refresh(&self) { + let mut system = self.system.lock().unwrap(); + system.refresh_all(); + } } -// Placeholder - Will be implemented in next phase +impl Default for SystemInfoBridge { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/button.rs b/crates/wsrx-desktop-gpui/src/components/button.rs new file mode 100644 index 0000000..fb37af7 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/button.rs @@ -0,0 +1,114 @@ +// Button component - Reusable button with consistent styling +use gpui::{Context, Render, SharedString, Window, div, prelude::*}; + +use super::traits::{Clickable, Disableable, Styleable}; +use crate::styles::colors; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ButtonVariant { + Primary, + Secondary, + Danger, +} + +pub struct Button { + label: String, + variant: ButtonVariant, + disabled: bool, + on_click: Option) + Send + Sync>>, +} + +impl Button { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + variant: ButtonVariant::Primary, + disabled: false, + on_click: None, + } + } + + pub fn variant(mut self, variant: ButtonVariant) -> Self { + self.variant = variant; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + fn bg_color(&self) -> gpui::Rgba { + match self.variant { + ButtonVariant::Primary => colors::accent(), + ButtonVariant::Secondary => gpui::rgba(0x444444FF), + ButtonVariant::Danger => colors::error(), + } + } + + fn hover_color(&self) -> gpui::Rgba { + match self.variant { + ButtonVariant::Primary => gpui::rgba(0x0088DDFF), + ButtonVariant::Secondary => gpui::rgba(0x555555FF), + ButtonVariant::Danger => gpui::rgba(0xFF6655FF), + } + } +} + +impl Clickable for Button { + fn on_click(mut self, handler: F) -> Self + where + F: Fn(&mut Window, &mut Context) + Send + Sync + 'static, + { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl Disableable for Button { + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Styleable for Button { + type Style = ButtonVariant; + + fn style(self, style: Self::Style) -> Self { + Self { + variant: style, + ..self + } + } +} + +impl Render for Button { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let id = SharedString::from(format!("button-{}", self.label)); + let label = self.label.clone(); + let disabled = self.disabled; + + div() + .id(id) + .px_4() + .py_2() + .rounded_md() + .cursor_pointer() + .when(!disabled, |div| { + div.bg(self.bg_color()) + .hover(|div| div.bg(self.hover_color())) + .on_click(cx.listener(|this, _event, window, cx| { + if let Some(ref callback) = this.on_click { + callback(window, cx); + } + })) + }) + .when(disabled, |div| { + div.bg(gpui::rgba(0x333333FF)) + .text_color(gpui::rgba(0x666666FF)) + }) + .text_color(colors::foreground()) + .child(label) + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/checkbox.rs b/crates/wsrx-desktop-gpui/src/components/checkbox.rs new file mode 100644 index 0000000..2b1d6be --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/checkbox.rs @@ -0,0 +1,116 @@ +// Checkbox component +// Based on Zed's checkbox pattern + +use super::prelude::*; +use crate::styles::{border_radius, colors, sizes}; + +pub struct Checkbox { + id: SharedString, + label: SharedString, + checked: bool, + disabled: bool, + on_change: Option>, +} + +impl Checkbox { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + id: id.into(), + label: label.into(), + checked: false, + disabled: false, + on_change: None, + } + } + + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_change( + mut self, handler: impl Fn(bool, &mut Window, &mut App) + Send + Sync + 'static, + ) -> Self { + self.on_change = Some(Box::new(handler)); + self + } +} + +impl IntoElement for Checkbox { + type Element = gpui::AnyElement; + + fn into_element(self) -> Self::Element { + let checked = self.checked; + let disabled = self.disabled; + let id = self.id.clone(); + let label = self.label.to_string(); + + let mut root_div = div() + .id(id) + .flex() + .flex_row() + .items_center() + .gap(sizes::icon_sm()); + + if !disabled { + root_div = root_div.cursor_pointer(); + } + + if disabled { + root_div = root_div.cursor_not_allowed().opacity(0.5); + } + + if let Some(on_change) = self.on_change { + root_div = root_div.on_click(move |_event, window, cx| { + if !disabled { + on_change(!checked, window, cx); + } + }); + } + + let checkbox_box = { + let mut box_div = div() + .flex() + .items_center() + .justify_center() + .size(sizes::icon_md()) + .rounded(border_radius::r_xs()) + .border_1() + .border_color(if checked { + colors::primary_bg() + } else { + colors::element_border() + }) + .bg(if checked { + colors::primary_bg() + } else { + gpui::rgba(0x00000000) + }); + + if checked { + box_div = box_div.child( + // Checkmark icon + svg() + .path("icons/checkmark.svg") + .size(sizes::icon_xs()) + .text_color(colors::window_bg()), + ); + } + + box_div + }; + + root_div + .child(checkbox_box) + .child( + // Label + div().text_color(colors::window_fg()).child(label), + ) + .into_any_element() + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/icon_button.rs b/crates/wsrx-desktop-gpui/src/components/icon_button.rs new file mode 100644 index 0000000..5553846 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/icon_button.rs @@ -0,0 +1,123 @@ +// Icon-only button component +// Based on Zed's IconButton pattern + +use super::prelude::*; +use crate::styles::{border_radius, colors, heights, sizes}; + +#[derive(Clone, Copy, PartialEq)] +pub enum IconButtonStyle { + Subtle, // Default - transparent with hover + Filled, // Solid background + Danger, // Red-themed for destructive actions +} + +pub struct IconButton { + icon_path: &'static str, + style: IconButtonStyle, + disabled: bool, + on_click: Option>, +} + +impl IconButton { + pub fn new(icon_path: &'static str) -> Self { + Self { + icon_path, + style: IconButtonStyle::Subtle, + disabled: false, + on_click: None, + } + } + + pub fn style(mut self, style: IconButtonStyle) -> Self { + self.style = style; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click( + mut self, handler: impl Fn(&mut Window, &mut App) + Send + Sync + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } + + fn background_color(&self) -> gpui::Rgba { + match self.style { + IconButtonStyle::Subtle => gpui::rgba(0x00000000), + IconButtonStyle::Filled => colors::layer_2(), + IconButtonStyle::Danger => colors::layer_1(), + } + } + + fn hover_background_color(&self) -> gpui::Rgba { + match self.style { + IconButtonStyle::Subtle => colors::layer_1(), + IconButtonStyle::Filled => colors::layer_3(), + IconButtonStyle::Danger => colors::error_bg(), + } + } + + fn icon_color(&self) -> gpui::Hsla { + if self.disabled { + gpui::Hsla::from(colors::window_fg()).opacity(0.3) + } else { + match self.style { + IconButtonStyle::Danger => gpui::Hsla::from(colors::error_fg()), + _ => gpui::Hsla::from(colors::window_fg()), + } + } + } +} + +impl IntoElement for IconButton { + type Element = gpui::AnyElement; + + fn into_element(self) -> Self::Element { + let background_color = self.background_color(); + let icon_color = self.icon_color(); + + let IconButton { + icon_path, + style: _, + disabled, + on_click, + } = self; + + let id = SharedString::from(format!("icon-button-{}", icon_path)); + + let mut div = div() + .id(id) + .flex() + .items_center() + .justify_center() + .size(heights::h_md()) + .bg(background_color) + .rounded(border_radius::r_sm()); + + if !disabled { + div = div.cursor_pointer(); + } + + if disabled { + div = div.cursor_not_allowed().opacity(0.5); + } + + if let Some(on_click) = on_click { + div = div.on_click(move |_event, window, cx| { + on_click(window, cx); + }); + } + + div.child( + svg() + .path(icon_path) + .size(sizes::icon_sm()) + .text_color(icon_color), + ) + .into_any_element() + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/input.rs b/crates/wsrx-desktop-gpui/src/components/input.rs new file mode 100644 index 0000000..31ffbda --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/input.rs @@ -0,0 +1,85 @@ +// Input component - Text input field with consistent styling +use gpui::{Context, Render, SharedString, Window, div, prelude::*}; + +use crate::styles::colors; + +pub struct Input { + id: String, + placeholder: String, + value: String, + disabled: bool, + on_change: Option) + Send + Sync>>, +} + +impl Input { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + placeholder: String::new(), + value: String::new(), + disabled: false, + on_change: None, + } + } + + pub fn placeholder(mut self, placeholder: impl Into) -> Self { + self.placeholder = placeholder.into(); + self + } + + pub fn value(mut self, value: impl Into) -> Self { + self.value = value.into(); + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_change(mut self, callback: F) -> Self + where + F: Fn(String, &mut Window, &mut Context) + Send + Sync + 'static, + { + self.on_change = Some(Box::new(callback)); + self + } +} + +impl Render for Input { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let id = SharedString::from(format!("input-{}", self.id)); + let placeholder = if self.value.is_empty() { + self.placeholder.clone() + } else { + String::new() + }; + let value = self.value.clone(); + let disabled = self.disabled; + + div() + .id(id) + .flex() + .items_center() + .px_3() + .py_2() + .rounded_md() + .border_1() + .when(!disabled, |div| { + div.bg(gpui::rgba(0x2A2A2AFF)) + .border_color(gpui::rgba(0x444444FF)) + .hover(|div| div.border_color(colors::accent())) + }) + .when(disabled, |div| { + div.bg(gpui::rgba(0x1A1A1AFF)) + .border_color(gpui::rgba(0x333333FF)) + .text_color(gpui::rgba(0x666666FF)) + }) + .text_color(colors::foreground()) + .child(if value.is_empty() && !placeholder.is_empty() { + div().text_color(gpui::rgba(0x888888FF)).child(placeholder) + } else { + div().child(value) + }) + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/mod.rs b/crates/wsrx-desktop-gpui/src/components/mod.rs index e8b2264..a1cbc37 100644 --- a/crates/wsrx-desktop-gpui/src/components/mod.rs +++ b/crates/wsrx-desktop-gpui/src/components/mod.rs @@ -1,10 +1,18 @@ // Components - Reusable UI elements built with GPUI // These are lower-level components used across different views +pub mod button; +pub mod checkbox; +pub mod icon_button; +pub mod input; +pub mod modal; +pub mod prelude; +pub mod select; +pub mod status_indicator; +pub mod tab_navigation; pub mod title_bar; +pub mod traits; pub mod window_controls; -pub mod tab_navigation; -pub use title_bar::TitleBar; +// pub use title_bar::TitleBar; pub use window_controls::WindowControls; -pub use tab_navigation::TabNavigation; diff --git a/crates/wsrx-desktop-gpui/src/components/modal.rs b/crates/wsrx-desktop-gpui/src/components/modal.rs new file mode 100644 index 0000000..aecd918 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/modal.rs @@ -0,0 +1,106 @@ +// Modal component - Modal dialog overlay +use gpui::{AnyElement, Context, Render, Window, div, prelude::*}; + +use crate::styles::colors; + +pub struct Modal { + title: String, + content: Option, + show_close: bool, + on_close: Option) + Send + Sync>>, +} + +impl Modal { + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + content: None, + show_close: true, + on_close: None, + } + } + + pub fn content(mut self, content: impl IntoElement) -> Self { + self.content = Some(content.into_any_element()); + self + } + + pub fn show_close(mut self, show: bool) -> Self { + self.show_close = show; + self + } + + pub fn on_close(mut self, callback: F) -> Self + where + F: Fn(&mut Window, &mut Context) + Send + Sync + 'static, + { + self.on_close = Some(Box::new(callback)); + self + } +} + +impl Render for Modal { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let title = self.title.clone(); + let show_close = self.show_close; + + div() + .flex() + .absolute() + .top_0() + .left_0() + .w_full() + .h_full() + .items_center() + .justify_center() + .bg(gpui::rgba(0x00000099)) // Semi-transparent overlay + .child( + div() + .flex() + .flex_col() + .w(gpui::relative(0.8)) + .max_w(gpui::px(600.0)) + .bg(gpui::rgba(0x2A2A2AFF)) + .rounded_lg() + .shadow_lg() + .child( + // Header + div() + .flex() + .items_center() + .justify_between() + .px_6() + .py_4() + .border_b_1() + .border_color(gpui::rgba(0x444444FF)) + .child( + div() + .text_xl() + .text_color(colors::foreground()) + .child(title), + ) + .when(show_close, |container| { + container.child( + div() + .id("modal-close") + .px_2() + .py_1() + .cursor_pointer() + .hover(|div| div.bg(gpui::rgba(0x444444FF))) + .rounded_md() + .on_click(cx.listener(|this, _event, window, cx| { + if let Some(ref callback) = this.on_close { + callback(window, cx); + } + })) + .child("✕"), + ) + }), + ) + .child( + // Content + div().px_6().py_4().children(self.content.take()), + ), + ) + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/prelude.rs b/crates/wsrx-desktop-gpui/src/components/prelude.rs new file mode 100644 index 0000000..3b57440 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/prelude.rs @@ -0,0 +1,10 @@ +// Component prelude - Common imports for all components +// Following Zed's pattern from crates/ui/src/component_prelude.rs + +pub use gpui::{ + App, AppContext, InteractiveElement, IntoElement, ParentElement, SharedString, + StatefulInteractiveElement, Styled, Window, div, prelude::*, svg, +}; + +// Component traits +pub use super::traits::{Clickable, Disableable, Selectable, Styleable}; diff --git a/crates/wsrx-desktop-gpui/src/components/select.rs b/crates/wsrx-desktop-gpui/src/components/select.rs new file mode 100644 index 0000000..4ddf51a --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/select.rs @@ -0,0 +1,149 @@ +// Select component - Dropdown selection component +// Following Zed's pattern with traits for reusability + +use gpui::{Context, Render, SharedString, Window, div, prelude::*}; + +use super::traits::{Disableable, Selectable}; +use crate::styles::colors; + +pub struct Select { + id: SharedString, + placeholder: String, + options: Vec, + selected_index: Option, + disabled: bool, + on_select: Option, T) + Send + Sync>>, +} + +impl Select +where + T: Clone + ToString + 'static, +{ + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + placeholder: "Select an option...".to_string(), + options: Vec::new(), + selected_index: None, + disabled: false, + on_select: None, + } + } + + pub fn placeholder(mut self, placeholder: impl Into) -> Self { + self.placeholder = placeholder.into(); + self + } + + pub fn options(mut self, options: Vec) -> Self { + self.options = options; + self + } + + pub fn selected_index(mut self, index: Option) -> Self { + self.selected_index = index; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Selectable for Select +where + T: Clone + ToString + 'static, +{ + type Item = T; + + fn selected(mut self, item: Self::Item) -> Self { + if let Some(index) = self + .options + .iter() + .position(|opt| opt.to_string() == item.to_string()) + { + self.selected_index = Some(index); + } + self + } + + fn on_select(mut self, handler: F) -> Self + where + F: Fn(&mut Window, &mut Context, Self::Item) + Send + Sync + 'static, + { + self.on_select = Some(Box::new(handler)); + self + } +} + +impl Disableable for Select +where + T: Clone + ToString + 'static, +{ + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Render for Select +where + T: Clone + ToString + 'static, +{ + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let selected_text = self + .selected_index + .and_then(|idx| self.options.get(idx)) + .map(|item| item.to_string()) + .unwrap_or_else(|| self.placeholder.clone()); + + let disabled = self.disabled; + let options = self.options.clone(); + + div() + .id(self.id.clone()) + .relative() + .w_full() + .px_3() + .py_2() + .rounded_md() + .bg(colors::background()) + .text_color(colors::foreground()) + .cursor_pointer() + .when(!disabled, |div| { + div.hover(|div| div.bg(gpui::rgba(0x2A2A2AFF))) + .on_click(cx.listener(move |this, _event, window, cx| { + // For now, just cycle through options on click + // TODO: Implement proper dropdown with overlay + let next_index = this + .selected_index + .map(|idx| (idx + 1) % this.options.len()) + .unwrap_or(0); + this.selected_index = Some(next_index); + + if let (Some(index), Some(callback)) = + (this.selected_index, &this.on_select) + { + if let Some(item) = this.options.get(index).cloned() { + callback(window, cx, item); + } + } + + cx.notify(); + })) + }) + .when(disabled, |div| { + div.bg(gpui::rgba(0x1A1A1AFF)) + .text_color(gpui::rgba(0x666666FF)) + }) + .child( + div() + .flex() + .justify_between() + .items_center() + .child(selected_text) + .child("▼"), // Simple dropdown arrow + ) + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/status_indicator.rs b/crates/wsrx-desktop-gpui/src/components/status_indicator.rs new file mode 100644 index 0000000..5db7906 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/status_indicator.rs @@ -0,0 +1,71 @@ +// StatusIndicator component - Visual status indicator with color coding +use gpui::{Context, Render, Window, div, prelude::*}; + +use crate::styles::colors; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Status { + Success, + Warning, + Error, + Info, + Inactive, +} + +pub struct StatusIndicator { + status: Status, + label: Option, + size: f32, +} + +impl StatusIndicator { + pub fn new(status: Status) -> Self { + Self { + status, + label: None, + size: 8.0, + } + } + + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + fn status_color(&self) -> gpui::Rgba { + match self.status { + Status::Success => colors::success(), + Status::Warning => colors::warning(), + Status::Error => colors::error(), + Status::Info => colors::accent(), + Status::Inactive => gpui::rgba(0x666666FF), + } + } +} + +impl Render for StatusIndicator { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .items_center() + .gap_2() + .child( + div() + .w(gpui::px(self.size)) + .h(gpui::px(self.size)) + .rounded_full() + .bg(self.status_color()), + ) + .children(self.label.as_ref().map(|label| { + div() + .text_sm() + .text_color(colors::foreground()) + .child(label.clone()) + })) + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/title_bar.rs b/crates/wsrx-desktop-gpui/src/components/title_bar.rs index 4165a6a..31e67b3 100644 --- a/crates/wsrx-desktop-gpui/src/components/title_bar.rs +++ b/crates/wsrx-desktop-gpui/src/components/title_bar.rs @@ -1,6 +1,88 @@ // Title bar component +use gpui::{prelude::FluentBuilder, *}; + +use crate::{components::WindowControls, styles}; + pub struct TitleBar { - // TODO: Define state + window: AnyWindowHandle, + show_sidebar_callback: Option>, } -// Placeholder - Will be implemented in next phase +impl TitleBar { + pub fn new(window: AnyWindowHandle) -> Self { + Self { + window, + show_sidebar_callback: None, + } + } + + pub fn set_show_sidebar_callback(&mut self, callback: Box) { + self.show_sidebar_callback = Some(callback); + } +} + +impl Render for TitleBar { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let window = self.window.clone(); + let is_macos = cfg!(target_os = "macos"); + + div() + .id("title-bar") + .flex() + .flex_row() + .items_center() + .justify_between() + .h(styles::heights::h_md() + styles::padding::p_md() * 2.0) + .px(styles::padding::p_md()) + .py(styles::padding::p_md()) + .gap(styles::spacing::s_md()) + .bg(gpui::transparent_black()) + // Drag area + .on_mouse_down(MouseButton::Left, { + let window = window.clone(); + cx.listener(move |_this, _event: &MouseDownEvent, _window, cx| { + window + .update(cx, |_view, window, _cx| { + window.start_window_move(); + }) + .ok(); + }) + }) + .child( + div() + .flex() + .flex_row() + .gap(styles::spacing::s_md()) + .when(!is_macos, |this| { + this.child( + div() + .id("toggle-sidebar-btn") + .flex() + .items_center() + .justify_center() + .size(styles::heights::h_md()) + .bg(styles::colors::layer_1()) + .hover(|this| this.bg(styles::colors::layer_2())) + .rounded(styles::border_radius::r_sm()) + .cursor_pointer() + .on_click(cx.listener(|this, _event, _window, cx| { + if let Some(ref callback) = this.show_sidebar_callback { + callback(cx); + } + })) + .child( + svg() + .path("icons/navigation.svg") + .size(styles::sizes::icon_sm()) + .text_color(styles::colors::window_fg()), + ), + ) + }), + ) + .child( + // Center spacer + div().flex_1(), + ) + .child(cx.new(|_cx| WindowControls::new(window))) + } +} diff --git a/crates/wsrx-desktop-gpui/src/components/traits.rs b/crates/wsrx-desktop-gpui/src/components/traits.rs new file mode 100644 index 0000000..999a54c --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/components/traits.rs @@ -0,0 +1,31 @@ +// Component traits - Common interfaces for UI components +// Following Zed's pattern for reusable component behavior + +use gpui::{Context, Window}; + +/// Trait for components that can be clicked +pub trait Clickable { + fn on_click(self, handler: F) -> Self + where + F: Fn(&mut Window, &mut Context) + Send + Sync + 'static; +} + +/// Trait for components that can be disabled +pub trait Disableable { + fn disabled(self, disabled: bool) -> Self; +} + +/// Trait for components with styled variants +pub trait Styleable { + type Style; + fn style(self, style: Self::Style) -> Self; +} + +/// Trait for components that can be selected from a list +pub trait Selectable { + type Item; + fn selected(self, item: Self::Item) -> Self; + fn on_select(self, handler: F) -> Self + where + F: Fn(&mut Window, &mut Context, Self::Item) + Send + Sync + 'static; +} diff --git a/crates/wsrx-desktop-gpui/src/components/window_controls.rs b/crates/wsrx-desktop-gpui/src/components/window_controls.rs index 7dcc507..b33b586 100644 --- a/crates/wsrx-desktop-gpui/src/components/window_controls.rs +++ b/crates/wsrx-desktop-gpui/src/components/window_controls.rs @@ -1,6 +1,106 @@ // Window controls component (minimize, maximize, close buttons) +use gpui::{prelude::FluentBuilder, *}; + +use crate::styles; + pub struct WindowControls { - // TODO: Define state + window: AnyWindowHandle, +} + +impl WindowControls { + pub fn new(window: AnyWindowHandle) -> Self { + Self { window } + } } -// Placeholder - Will be implemented in next phase +impl Render for WindowControls { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let window = self.window.clone(); + let is_macos = cfg!(target_os = "macos"); + + div() + .flex() + .flex_row() + .gap(styles::spacing::s_md()) + .when(!is_macos, |this| { + this + // Minimize button + .child( + div() + .id("minimize-btn") + .flex() + .items_center() + .justify_center() + .size(styles::heights::h_md()) + .bg(styles::colors::layer_1()) + .hover(|this| this.bg(styles::colors::layer_2())) + .cursor_pointer() + .on_click({ + let window = window.clone(); + cx.listener(move |_this, _event, _window, cx| { + window + .update(cx, |_view, window, _cx| { + window.minimize_window(); + }) + .ok(); + }) + }) + .child( + svg() + .path("icons/subtract.svg") + .size(styles::sizes::icon_sm()) + .text_color(styles::colors::window_fg()), + ), + ) + // Maximize button + .child( + div() + .id("maximize-btn") + .flex() + .items_center() + .justify_center() + .size(styles::heights::h_md()) + .bg(styles::colors::layer_1()) + .hover(|this| this.bg(styles::colors::layer_2())) + .cursor_pointer() + .on_click({ + let window = window.clone(); + cx.listener(move |_this, _event, _window, cx| { + window + .update(cx, |_view, window, _cx| { + window.zoom_window(); + }) + .ok(); + }) + }) + .child( + svg() + .path("icons/maximize.svg") + .size(styles::sizes::icon_sm()) + .text_color(styles::colors::window_fg()), + ), + ) + // Close button + .child( + div() + .id("close-btn") + .flex() + .items_center() + .justify_center() + .size(styles::heights::h_md()) + .bg(styles::colors::layer_1()) + .hover(|this| this.bg(styles::colors::error_bg())) + .cursor_pointer() + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.quit(); + })) + .child( + svg() + .path("icons/dismiss.svg") + .size(styles::sizes::icon_sm()) + .text_color(styles::colors::window_fg()), + ), + ) + }) + } +} diff --git a/crates/wsrx-desktop-gpui/src/i18n.rs b/crates/wsrx-desktop-gpui/src/i18n.rs new file mode 100644 index 0000000..faf5ff2 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/i18n.rs @@ -0,0 +1,29 @@ +// i18n - Internationalization support using rust-i18n + +// Provides multi-language support with YAML locale files +// NOTE: The i18n! macro is initialized in lib.rs at crate root + +// Re-export functions for convenience +pub use rust_i18n::{locale, set_locale}; + +/// Set application language +pub fn set_language(locale_str: &str) { + set_locale(locale_str); +} + +/// Get current language +pub fn current_language() -> impl std::ops::Deref { + locale() +} + +/// Detect system locale and set it +pub fn init_locale() { + if let Some(locale) = sys_locale::get_locale() { + // Map system locale to supported locales + let locale = match locale.as_str() { + l if l.starts_with("zh") => "zh-CN", + _ => "en", + }; + set_language(locale); + } +} diff --git a/crates/wsrx-desktop-gpui/src/icons.rs b/crates/wsrx-desktop-gpui/src/icons.rs new file mode 100644 index 0000000..ae740c7 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/icons.rs @@ -0,0 +1,42 @@ +// Embedded SVG icons +// All SVG files are embedded directly into the binary at compile time + +pub const HOME: &str = include_str!("../icons/home.svg"); +pub const CODE: &str = include_str!("../icons/code.svg"); +pub const SETTINGS: &str = include_str!("../icons/settings.svg"); +pub const GLOBE_STAR: &str = include_str!("../icons/globe-star.svg"); +pub const NAVIGATION: &str = include_str!("../icons/navigation.svg"); +pub const LOGO: &str = include_str!("../icons/logo.svg"); +pub const LOGO_STROKED: &str = include_str!("../icons/logo-stroked.svg"); +pub const WARNING: &str = include_str!("../icons/warning.svg"); +pub const DISMISS: &str = include_str!("../icons/dismiss.svg"); +pub const MAXIMIZE: &str = include_str!("../icons/maximize.svg"); +pub const SUBTRACT: &str = include_str!("../icons/subtract.svg"); +pub const LOCK_CLOSED: &str = include_str!("../icons/lock-closed.svg"); +pub const CHECKMARK: &str = include_str!("../icons/checkmark.svg"); +pub const CHECKBOX_UNCHECKED: &str = include_str!("../icons/checkbox-unchecked.svg"); +pub const ARROW_UP_RIGHT: &str = include_str!("../icons/arrow-up-right.svg"); +pub const ARROW_SYNC_OFF: &str = include_str!("../icons/arrow-sync-off.svg"); + +/// Get icon SVG content by name +pub fn get_icon(name: &str) -> Option<&'static str> { + match name { + "home" => Some(HOME), + "code" => Some(CODE), + "settings" => Some(SETTINGS), + "globe-star" => Some(GLOBE_STAR), + "navigation" => Some(NAVIGATION), + "logo" => Some(LOGO), + "logo-stroked" => Some(LOGO_STROKED), + "warning" => Some(WARNING), + "dismiss" => Some(DISMISS), + "maximize" => Some(MAXIMIZE), + "subtract" => Some(SUBTRACT), + "lock-closed" => Some(LOCK_CLOSED), + "checkmark" => Some(CHECKMARK), + "checkbox-unchecked" => Some(CHECKBOX_UNCHECKED), + "arrow-up-right" => Some(ARROW_UP_RIGHT), + "arrow-sync-off" => Some(ARROW_SYNC_OFF), + _ => None, + } +} diff --git a/crates/wsrx-desktop-gpui/src/lib.rs b/crates/wsrx-desktop-gpui/src/lib.rs deleted file mode 100644 index 3820116..0000000 --- a/crates/wsrx-desktop-gpui/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -// wsrx-desktop-gpui lib root -pub mod bridges; -pub mod components; -pub mod logging; -pub mod models; -pub mod styles; -pub mod views; - -// Include generated constants from build.rs -include!(concat!(env!("OUT_DIR"), "/constants.rs")); - -// Re-export commonly used types -pub use models::*; -pub use views::*; -pub use components::*; -pub use styles::*; diff --git a/crates/wsrx-desktop-gpui/src/logging.rs b/crates/wsrx-desktop-gpui/src/logging.rs index 6e1ee1f..bfd2bdc 100644 --- a/crates/wsrx-desktop-gpui/src/logging.rs +++ b/crates/wsrx-desktop-gpui/src/logging.rs @@ -1,9 +1,10 @@ // Logging setup for wsrx-desktop-gpui +use std::fs; + use anyhow::Result; use directories::ProjectDirs; -use std::fs; use tracing_appender::non_blocking; -use tracing_subscriber::{fmt, prelude::*, EnvFilter}; +use tracing_subscriber::{EnvFilter, fmt, prelude::*}; pub fn setup() -> Result<( tracing_appender::non_blocking::WorkerGuard, diff --git a/crates/wsrx-desktop-gpui/src/main.rs b/crates/wsrx-desktop-gpui/src/main.rs index d012165..d1f583f 100644 --- a/crates/wsrx-desktop-gpui/src/main.rs +++ b/crates/wsrx-desktop-gpui/src/main.rs @@ -1,18 +1,95 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use anyhow::Result; -use gpui::{App, Application, WindowOptions, WindowBounds, Bounds, Size}; +use gpui::{ + App, AppContext, Application, AssetSource, Bounds, SharedString, TitlebarOptions, + WindowBounds, WindowDecorations, WindowKind, WindowOptions, point, px, size, +}; +mod bridges; +mod components; +mod i18n; +mod icons; mod logging; +mod models; +mod styles; +mod views; + +// Initialize i18n at crate root with TOML locale files +// The path is relative to CARGO_MANIFEST_DIR (crate root) + +#[macro_use] +extern crate rust_i18n; + +i18n!("locales", fallback = "en"); + +// Include generated constants from build.rs +include!(concat!(env!("OUT_DIR"), "/constants.rs")); + +use views::RootView; + +/// Asset source that loads embedded SVG icons from binary +struct EmbeddedAssets; + +impl AssetSource for EmbeddedAssets { + fn load(&self, path: &str) -> Result>> { + // Handle icon paths like "icons/home.svg" + if let Some(icon_name) = path.strip_prefix("icons/").and_then(|p| p.strip_suffix(".svg")) { + if let Some(svg_content) = icons::get_icon(icon_name) { + return Ok(Some(std::borrow::Cow::Borrowed(svg_content.as_bytes()))); + } + } + Ok(None) + } + + fn list(&self, _path: &str) -> Result> { + // Return empty list - we don't need directory listing for embedded assets + Ok(Vec::new()) + } +} fn main() -> Result<()> { // Initialize logging let (_console_guard, _file_guard) = logging::setup()?; - // Create and run the GPUI application - Application::new().run(|_cx: &mut App| { - // TODO: Initialize main window and root view - // This will be implemented in the next migration phase + // Initialize i18n with system locale + i18n::init_locale(); + + // Create and run the GPUI application with embedded assets + Application::new() + .with_assets(EmbeddedAssets) + .run(|cx: &mut App| { + // Create main window with centered bounds + let bounds = Bounds::centered(None, size(px(1200.0), px(800.0)), cx); + + // Platform-specific window configuration (following Zed's pattern) + let titlebar_config = Some(TitlebarOptions { + title: None, // Custom titlebar will show title + appears_transparent: true, + traffic_light_position: Some(point(px(9.0), px(9.0))), + ..Default::default() + }); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: titlebar_config, + window_decorations: Some(WindowDecorations::Client), // Client-side decorations + kind: WindowKind::Normal, + is_movable: true, + focus: true, + show: true, + window_min_size: Some(gpui::Size { + width: px(800.0), + height: px(600.0), + }), + ..Default::default() + }, + |window, cx| cx.new(|cx| RootView::new(window, cx)), + ) + .expect("Failed to open window"); + + cx.activate(true); }); Ok(()) diff --git a/crates/wsrx-desktop-gpui/src/models/app_state.rs b/crates/wsrx-desktop-gpui/src/models/app_state.rs new file mode 100644 index 0000000..6155e09 --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/models/app_state.rs @@ -0,0 +1,101 @@ +// Application State - Global application state management +use std::collections::VecDeque; + +use super::{Connection, LogEntry, Settings, Tunnel}; + +/// Current active page in the application +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Page { + GetStarted, + Connections, + NetworkLogs, + Settings, +} + +/// Daemon connection status +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DaemonStatus { + Stopped, + Starting, + Running, + Stopping, + Error, +} + +/// Global application state +/// This struct holds the main application data that is shared across views +pub struct AppState { + /// Currently active page + pub current_page: Page, + + /// List of configured tunnels + pub tunnels: Vec, + + /// Active connections + pub connections: Vec, + + /// Application settings + pub settings: Settings, + + /// Recent log entries (circular buffer) + pub recent_logs: VecDeque, + + /// Maximum number of logs to keep in memory + pub max_logs: usize, + + /// Current daemon status + pub daemon_status: DaemonStatus, +} + +impl AppState { + /// Create a new AppState with default values + pub fn new() -> Self { + Self { + current_page: Page::GetStarted, + tunnels: Vec::new(), + connections: Vec::new(), + settings: Settings::default(), + recent_logs: VecDeque::new(), + max_logs: 10000, + daemon_status: DaemonStatus::Stopped, + } + } + + /// Add a log entry, removing oldest if over capacity + pub fn add_log(&mut self, entry: LogEntry) { + if self.recent_logs.len() >= self.max_logs { + self.recent_logs.pop_front(); + } + self.recent_logs.push_back(entry); + } + + /// Clear all logs + pub fn clear_logs(&mut self) { + self.recent_logs.clear(); + } + + /// Add or update a tunnel + pub fn upsert_tunnel(&mut self, tunnel: Tunnel) { + if let Some(pos) = self.tunnels.iter().position(|t| t.id == tunnel.id) { + self.tunnels[pos] = tunnel; + } else { + self.tunnels.push(tunnel); + } + } + + /// Remove a tunnel by ID + pub fn remove_tunnel(&mut self, tunnel_id: &str) { + self.tunnels.retain(|t| t.id != tunnel_id); + } + + /// Get a tunnel by ID + pub fn get_tunnel(&self, tunnel_id: &str) -> Option<&Tunnel> { + self.tunnels.iter().find(|t| t.id == tunnel_id) + } +} + +impl Default for AppState { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/wsrx-desktop-gpui/src/models/events.rs b/crates/wsrx-desktop-gpui/src/models/events.rs new file mode 100644 index 0000000..eaad67b --- /dev/null +++ b/crates/wsrx-desktop-gpui/src/models/events.rs @@ -0,0 +1,45 @@ +// Events - Application event definitions for inter-component communication + +use super::{Connection, LogEntry, Tunnel}; + +/// Events that can occur in the application +#[derive(Clone, Debug)] +pub enum AppEvent { + /// Page navigation event + NavigateToPage(super::app_state::Page), + + /// Tunnel-related events + TunnelCreated(Tunnel), + TunnelUpdated(Tunnel), + TunnelDeleted(String), // tunnel_id + TunnelEnabled(String), + TunnelDisabled(String), + + /// Connection-related events + ConnectionEstablished(Connection), + ConnectionClosed(String), // connection_id + ConnectionError { + connection_id: String, + error: String, + }, + + /// Daemon-related events + DaemonStarted, + DaemonStopped, + DaemonError(String), + + /// Log events + LogReceived(LogEntry), + ClearLogs, + + /// Settings events + SettingsUpdated, + ThemeChanged, + + /// UI events + ShowNotification { + title: String, + message: String, + }, + ShowError(String), +} diff --git a/crates/wsrx-desktop-gpui/src/models/mod.rs b/crates/wsrx-desktop-gpui/src/models/mod.rs index c699c62..efc6d7a 100644 --- a/crates/wsrx-desktop-gpui/src/models/mod.rs +++ b/crates/wsrx-desktop-gpui/src/models/mod.rs @@ -1,9 +1,13 @@ // Models - Data structures for the application // This module contains all the data models used throughout the application -use serde::{Deserialize, Serialize}; use std::net::SocketAddr; +use serde::{Deserialize, Serialize}; + +pub mod app_state; +pub mod events; + /// Represents a WebSocket tunnel configuration #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Tunnel { diff --git a/crates/wsrx-desktop-gpui/src/styles/mod.rs b/crates/wsrx-desktop-gpui/src/styles/mod.rs index 0105926..b7fb707 100644 --- a/crates/wsrx-desktop-gpui/src/styles/mod.rs +++ b/crates/wsrx-desktop-gpui/src/styles/mod.rs @@ -2,84 +2,287 @@ // This module contains all the styling, colors, and theming configuration /// Color palette for the application +/// Aligned with Slint design system for consistency pub mod colors { use gpui::Rgba; + // Dark mode palette (matching Slint dark-palette) + pub fn window_fg() -> Rgba { + gpui::rgba(0xCDD6F4FF) // #cdd6f4 + } + + pub fn window_bg() -> Rgba { + gpui::rgba(0x151515FF) // #151515 + } + + pub fn window_alter_bg() -> Rgba { + gpui::rgba(0x1E1E1EFF) // #1e1e1e + } + + pub fn primary_bg() -> Rgba { + gpui::rgba(0x0078D6FF) // #0078D6 + } + + pub fn window_border() -> Rgba { + gpui::rgba(0x323232FF) // #323232 + } + + pub fn element_border() -> Rgba { + gpui::rgba(0x2D2D2DFF) // #2d2d2d + } + + // Legacy aliases for backward compatibility pub fn background() -> Rgba { - gpui::rgba(0x1e1e1eff) + window_bg() } pub fn foreground() -> Rgba { - gpui::rgba(0xe5e5e5ff) + window_fg() } pub fn accent() -> Rgba { - gpui::rgba(0x007accff) + primary_bg() } + // Semantic colors (matching Slint) pub fn error() -> Rgba { - gpui::rgba(0xf48771ff) + gpui::rgba(0xEF303FFF) // #ef303f + } + + pub fn error_bg() -> Rgba { + error() + } + + pub fn error_fg() -> Rgba { + error() } pub fn warning() -> Rgba { - gpui::rgba(0xddb76fff) + gpui::rgba(0xE85D03FF) // #e85d03 + } + + pub fn warning_bg() -> Rgba { + warning() } pub fn success() -> Rgba { - gpui::rgba(0x7ec699ff) + gpui::rgba(0x03A44EFF) // #03a44e + } + + pub fn success_bg() -> Rgba { + success() + } + + pub fn info() -> Rgba { + gpui::rgba(0x0078D6FF) // #0078D6 + } + + pub fn info_bg() -> Rgba { + info() + } + + pub fn debug() -> Rgba { + gpui::rgba(0x808080FF) // #808080 + } + + // Layer colors (for depth/elevation) + pub fn layer_1() -> Rgba { + gpui::rgba(0xFFFFFF10) // #ffffff10 + } + + pub fn layer_2() -> Rgba { + gpui::rgba(0xFFFFFF18) // #ffffff18 + } + + pub fn layer_3() -> Rgba { + gpui::rgba(0xFFFFFF20) // #ffffff20 + } + + pub fn layer_4() -> Rgba { + gpui::rgba(0xFFFFFF28) // #ffffff28 + } + + pub fn layer_5() -> Rgba { + gpui::rgba(0xFFFFFF30) // #ffffff30 } } -/// Typography settings +/// Typography settings (matching Slint font sizes) pub mod typography { - use gpui::Pixels; + use gpui::{Pixels, px}; pub fn font_size_xs() -> Pixels { - Pixels(11.0) + px(12.0) } pub fn font_size_sm() -> Pixels { - Pixels(12.0) + px(14.0) } pub fn font_size_base() -> Pixels { - Pixels(14.0) + px(16.0) // matches Slint font: 16px } pub fn font_size_lg() -> Pixels { - Pixels(16.0) + px(18.0) } pub fn font_size_xl() -> Pixels { - Pixels(20.0) + px(20.0) + } + + pub fn font_size_2xl() -> Pixels { + px(24.0) } } -/// Spacing constants +/// Spacing constants (matching Slint sizes) pub mod spacing { - use gpui::Pixels; + use gpui::{Pixels, px}; + + // Padding sizes (p-*) + pub fn p_xs() -> Pixels { + px(1.0) + } + + pub fn p_sm() -> Pixels { + px(2.0) + } + + pub fn p_md() -> Pixels { + px(4.0) + } + + pub fn p_lg() -> Pixels { + px(8.0) + } + + pub fn p_xl() -> Pixels { + px(12.0) + } + // Spacing sizes (s-*) + pub fn s_xs() -> Pixels { + px(1.0) + } + + pub fn s_sm() -> Pixels { + px(2.0) + } + + pub fn s_md() -> Pixels { + px(4.0) + } + + pub fn s_lg() -> Pixels { + px(8.0) + } + + pub fn s_xl() -> Pixels { + px(12.0) + } + + // Legacy aliases pub fn xs() -> Pixels { - Pixels(2.0) + px(2.0) } pub fn sm() -> Pixels { - Pixels(4.0) + px(4.0) } pub fn base() -> Pixels { - Pixels(8.0) + px(8.0) } pub fn md() -> Pixels { - Pixels(12.0) + px(12.0) } pub fn lg() -> Pixels { - Pixels(16.0) + px(16.0) } pub fn xl() -> Pixels { - Pixels(24.0) + px(24.0) + } +} + +/// Border radius constants (matching Slint r-*) +pub mod radius { + use gpui::{Pixels, px}; + + pub fn r_xs() -> Pixels { + px(2.0) + } + + pub fn r_sm() -> Pixels { + px(4.0) + } + + pub fn r_md() -> Pixels { + px(6.0) + } + + pub fn r_lg() -> Pixels { + px(8.0) + } + + pub fn r_xl() -> Pixels { + px(10.0) } } + +/// Height constants (matching Slint h-*) +pub mod heights { + use gpui::{Pixels, px}; + + pub fn h_xs() -> Pixels { + px(16.0) + } + + pub fn h_sm() -> Pixels { + px(24.0) + } + + pub fn h_md() -> Pixels { + px(32.0) + } + + pub fn h_lg() -> Pixels { + px(36.0) + } + + pub fn h_xl() -> Pixels { + px(40.0) + } +} + +/// Icon and misc sizes +pub mod sizes { + use gpui::{Pixels, px}; + + pub fn icon_xs() -> Pixels { + px(12.0) + } + + pub fn icon_sm() -> Pixels { + px(16.0) + } + + pub fn icon_md() -> Pixels { + px(20.0) + } + + pub fn icon_lg() -> Pixels { + px(24.0) + } + + pub fn icon_xl() -> Pixels { + px(32.0) + } +} + +// Module aliases for convenience +pub use radius as border_radius; +pub use spacing as padding; diff --git a/crates/wsrx-desktop-gpui/src/views/connections.rs b/crates/wsrx-desktop-gpui/src/views/connections.rs index 65c9ffe..c3a3c44 100644 --- a/crates/wsrx-desktop-gpui/src/views/connections.rs +++ b/crates/wsrx-desktop-gpui/src/views/connections.rs @@ -1,6 +1,146 @@ // Connections view - Manage tunnels and connections +use gpui::{Context, Render, SharedString, Window, div, prelude::*}; + +use crate::{models::Tunnel, styles::colors}; + pub struct ConnectionsView { - // TODO: Define state + tunnels: Vec, +} + +impl ConnectionsView { + pub fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self { + tunnels: Vec::new(), + } + } + + fn render_tunnel_item(&self, tunnel: &Tunnel, index: usize) -> impl IntoElement { + let id = SharedString::from(format!("tunnel-{}", index)); + let status_color = if tunnel.enabled { + colors::success() + } else { + gpui::rgba(0x888888FF) + }; + + div() + .id(id) + .flex() + .items_center() + .justify_between() + .px_4() + .py_3() + .mb_2() + .bg(gpui::rgba(0x2A2A2AFF)) + .rounded_md() + .hover(|div| div.bg(gpui::rgba(0x333333FF))) + .child( + div() + .flex() + .items_center() + .gap_3() + .child(div().w_3().h_3().rounded_full().bg(status_color)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_color(colors::foreground()) + .child(tunnel.name.clone()), + ) + .child( + div() + .text_sm() + .text_color(gpui::rgba(0xAAAAAAFF)) + .child(format!( + "{} → {}", + tunnel.local_addr, tunnel.remote_addr + )), + ), + ), + ) + .child( + div() + .text_sm() + .text_color(gpui::rgba(0xAAAAAAFF)) + .child(if tunnel.enabled { + "Enabled" + } else { + "Disabled" + }), + ) + } + + fn render_empty_state(&self) -> impl IntoElement { + div() + .flex() + .flex_col() + .items_center() + .justify_center() + .flex_1() + .gap_4() + .child( + div() + .text_xl() + .text_color(gpui::rgba(0xAAAAAAFF)) + .child("No tunnels configured"), + ) + .child( + div() + .text_color(gpui::rgba(0x888888FF)) + .child("Click the + button to create your first tunnel"), + ) + } } -// Placeholder - Will be implemented in next phase +impl Render for ConnectionsView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .w_full() + .h_full() + .p_4() + .child( + div() + .flex() + .items_center() + .justify_between() + .mb_4() + .child( + div() + .text_xl() + .text_color(colors::foreground()) + .child("Connections"), + ) + .child( + div() + .id("add-tunnel-button") + .px_4() + .py_2() + .bg(colors::accent()) + .rounded_md() + .cursor_pointer() + .hover(|div| div.bg(gpui::rgba(0x0088DDFF))) + .child("+ Add Tunnel"), + ), + ) + .child(if self.tunnels.is_empty() { + self.render_empty_state().into_any_element() + } else { + let elements: Vec<_> = self + .tunnels + .iter() + .enumerate() + .map(|(i, tunnel)| self.render_tunnel_item(tunnel, i)) + .collect(); + div() + .flex() + .flex_col() + .gap_2() + .children(elements) + .into_any_element() + }) + } +} diff --git a/crates/wsrx-desktop-gpui/src/views/get_started.rs b/crates/wsrx-desktop-gpui/src/views/get_started.rs index b6f920f..42bfb54 100644 --- a/crates/wsrx-desktop-gpui/src/views/get_started.rs +++ b/crates/wsrx-desktop-gpui/src/views/get_started.rs @@ -1,6 +1,37 @@ // Get Started view - Onboarding page -pub struct GetStartedView { - // TODO: Define state +use gpui::{Context, Render, Window, div, prelude::*}; + +use crate::styles::colors; + +pub struct GetStartedView {} + +impl GetStartedView { + pub fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self {} + } } -// Placeholder - Will be implemented in next phase +impl Render for GetStartedView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .items_center() + .justify_center() + .w_full() + .h_full() + .gap_4() + .child( + div() + .text_2xl() + .text_color(colors::foreground()) + .child("Welcome to WebSocket Reflector X"), + ) + .child( + div() + .text_base() + .text_color(colors::foreground()) + .child("Get started by creating your first tunnel"), + ) + } +} diff --git a/crates/wsrx-desktop-gpui/src/views/mod.rs b/crates/wsrx-desktop-gpui/src/views/mod.rs index 531a9c7..0a9ce4a 100644 --- a/crates/wsrx-desktop-gpui/src/views/mod.rs +++ b/crates/wsrx-desktop-gpui/src/views/mod.rs @@ -1,16 +1,16 @@ // Views - High-level UI components in GPUI // Each view corresponds to a page/screen in the application -pub mod root; -pub mod get_started; pub mod connections; +pub mod get_started; pub mod network_logs; +pub mod root; pub mod settings; pub mod sidebar; -pub use root::RootView; -pub use get_started::GetStartedView; pub use connections::ConnectionsView; +pub use get_started::GetStartedView; pub use network_logs::NetworkLogsView; +pub use root::RootView; pub use settings::SettingsView; pub use sidebar::SidebarView; diff --git a/crates/wsrx-desktop-gpui/src/views/network_logs.rs b/crates/wsrx-desktop-gpui/src/views/network_logs.rs index 49c2284..f2a7ceb 100644 --- a/crates/wsrx-desktop-gpui/src/views/network_logs.rs +++ b/crates/wsrx-desktop-gpui/src/views/network_logs.rs @@ -1,6 +1,165 @@ // Network Logs view - Display real-time logs +use std::collections::VecDeque; + +use gpui::{Context, Render, SharedString, Window, div, prelude::*}; + +use crate::{ + models::{LogEntry, LogLevel}, + styles::colors, +}; + pub struct NetworkLogsView { - // TODO: Define state + logs: VecDeque, } -// Placeholder - Will be implemented in next phase +impl NetworkLogsView { + pub fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self { + logs: VecDeque::new(), + } + } + + fn log_level_color(&self, level: LogLevel) -> gpui::Rgba { + match level { + LogLevel::Debug => gpui::rgba(0x888888FF), + LogLevel::Info => colors::foreground(), + LogLevel::Warn => colors::warning(), + LogLevel::Error => colors::error(), + } + } + + fn log_level_text(&self, level: LogLevel) -> &'static str { + match level { + LogLevel::Debug => "DEBUG", + LogLevel::Info => "INFO", + LogLevel::Warn => "WARN", + LogLevel::Error => "ERROR", + } + } + + fn render_log_entry(&self, entry: &LogEntry, index: usize) -> impl IntoElement { + let id = SharedString::from(format!("log-entry-{}", index)); + let level_color = self.log_level_color(entry.level); + let level_text = self.log_level_text(entry.level); + + div() + .id(id) + .flex() + .items_start() + .gap_3() + .px_4() + .py_2() + .border_b_1() + .border_color(gpui::rgba(0x2A2A2AFF)) + .hover(|div| div.bg(gpui::rgba(0x00000020))) + .child( + div().flex().items_center().gap_2().min_w_32().child( + div() + .text_sm() + .text_color(gpui::rgba(0xAAAAAAFF)) + .child(entry.timestamp.clone()), + ), + ) + .child( + div() + .text_sm() + .text_color(level_color) + .min_w_16() + .child(level_text), + ) + .child( + div() + .text_sm() + .text_color(gpui::rgba(0x888888FF)) + .min_w_32() + .child(entry.target.clone()), + ) + .child( + div() + .flex_1() + .text_sm() + .text_color(colors::foreground()) + .child(entry.message.clone()), + ) + } + + fn render_empty_state(&self) -> impl IntoElement { + div() + .flex() + .flex_col() + .items_center() + .justify_center() + .flex_1() + .gap_4() + .child( + div() + .text_xl() + .text_color(gpui::rgba(0xAAAAAAFF)) + .child("No logs yet"), + ) + .child( + div() + .text_color(gpui::rgba(0x888888FF)) + .child("Logs will appear here when connections are active"), + ) + } +} + +impl Render for NetworkLogsView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .w_full() + .h_full() + .child( + div() + .flex() + .items_center() + .justify_between() + .px_4() + .py_3() + .border_b_1() + .border_color(gpui::rgba(0x2A2A2AFF)) + .child( + div() + .text_xl() + .text_color(colors::foreground()) + .child("Network Logs"), + ) + .child( + div().flex().gap_2().child( + div() + .id("clear-logs-button") + .px_3() + .py_1() + .text_sm() + .bg(gpui::rgba(0x444444FF)) + .rounded_md() + .cursor_pointer() + .hover(|div| div.bg(gpui::rgba(0x555555FF))) + .on_click(cx.listener(|this, _event, _window, cx| { + this.logs.clear(); + cx.notify(); + })) + .child("Clear"), + ), + ), + ) + .child(if self.logs.is_empty() { + self.render_empty_state().into_any_element() + } else { + let elements: Vec<_> = self + .logs + .iter() + .enumerate() + .map(|(i, log)| self.render_log_entry(log, i)) + .collect(); + div() + .flex() + .flex_col() + .children(elements) + .into_any_element() + }) + } +} diff --git a/crates/wsrx-desktop-gpui/src/views/root.rs b/crates/wsrx-desktop-gpui/src/views/root.rs index 415ed2e..166a0e4 100644 --- a/crates/wsrx-desktop-gpui/src/views/root.rs +++ b/crates/wsrx-desktop-gpui/src/views/root.rs @@ -1,6 +1,130 @@ // Root view - Main application window +use gpui::{AnyWindowHandle, Context, Entity, Render, Window, div, prelude::*}; + +use super::{ConnectionsView, GetStartedView, NetworkLogsView, SettingsView, SidebarView}; +use crate::{components::title_bar::TitleBar, models::app_state::Page, styles::colors}; + pub struct RootView { - // TODO: Define state + /// Window handle + window: AnyWindowHandle, + + /// Current active page + current_page: Page, + + /// Title bar + title_bar: Entity, + + /// Sidebar entity + sidebar: Entity, + + /// Page views + get_started: Entity, + connections: Entity, + network_logs: Entity, + settings: Entity, + + /// Sidebar visibility + show_sidebar: bool, } -// Placeholder - Will be implemented in next phase +impl RootView { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let current_page = Page::GetStarted; + let window_handle = window.window_handle(); + + let root = Self { + window: window_handle.clone(), + current_page, + show_sidebar: true, + title_bar: cx.new(|_cx| TitleBar::new(window_handle.clone())), + sidebar: cx.new(|cx| SidebarView::new(window, cx, current_page)), + get_started: cx.new(|cx| GetStartedView::new(window, cx)), + connections: cx.new(|cx| ConnectionsView::new(window, cx)), + network_logs: cx.new(|cx| NetworkLogsView::new(window, cx)), + settings: cx.new(|cx| SettingsView::new(window, cx)), + }; + + // Set up the navigation callback for sidebar + let weak_self = cx.weak_entity(); + root.sidebar.update(cx, |sidebar, _| { + sidebar.set_on_page_change(Box::new(move |page, cx| { + if let Some(root) = weak_self.upgrade() { + root.update(cx, |root, cx| { + root.set_page(page, cx); + }); + } + })); + }); + + // Set up title bar sidebar toggle callback + let weak_self = cx.weak_entity(); + root.title_bar.update(cx, |title_bar, _| { + title_bar.set_show_sidebar_callback(Box::new(move |cx| { + if let Some(root) = weak_self.upgrade() { + root.update(cx, |root, cx| { + root.toggle_sidebar(cx); + }); + } + })); + }); + + root + } + + pub fn set_page(&mut self, page: Page, cx: &mut Context) { + self.current_page = page; + cx.notify(); // Trigger re-render + } + + pub fn toggle_sidebar(&mut self, cx: &mut Context) { + self.show_sidebar = !self.show_sidebar; + cx.notify(); + } + + fn render_sidebar(&self) -> impl IntoElement { + div() + .flex() + .flex_col() + .when(self.show_sidebar, |div| div.w_64()) + .when(!self.show_sidebar, |div| div.w_0()) + .h_full() // Full height of window + .overflow_hidden() + .child(self.sidebar.clone()) + } + + fn render_main_content(&self) -> impl IntoElement { + div() + .flex() + .flex_col() + .flex_1() + .h_full() // Full height of window + .bg(colors::window_alter_bg()) + .child(self.title_bar.clone()) + .child(self.render_page_content()) + } + + fn render_page_content(&self) -> impl IntoElement { + div() + .flex_1() + .overflow_hidden() // Use basic overflow hidden + .child(match self.current_page { + Page::GetStarted => div().h_full().child(self.get_started.clone()), + Page::Connections => div().h_full().child(self.connections.clone()), + Page::NetworkLogs => div().h_full().child(self.network_logs.clone()), + Page::Settings => div().h_full().child(self.settings.clone()), + }) + } +} + +impl Render for RootView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .w_full() + .h_full() + .bg(colors::window_bg()) + .text_color(colors::window_fg()) + .child(self.render_sidebar()) + .child(self.render_main_content()) + } +} diff --git a/crates/wsrx-desktop-gpui/src/views/settings.rs b/crates/wsrx-desktop-gpui/src/views/settings.rs index 6908360..9efeb43 100644 --- a/crates/wsrx-desktop-gpui/src/views/settings.rs +++ b/crates/wsrx-desktop-gpui/src/views/settings.rs @@ -1,6 +1,151 @@ // Settings view - Application configuration +use gpui::{Context, Render, Window, div, prelude::*}; + +use crate::{ + models::{Settings, Theme}, + styles::colors, +}; + pub struct SettingsView { - // TODO: Define state + settings: Settings, } -// Placeholder - Will be implemented in next phase +impl SettingsView { + pub fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self { + settings: Settings::default(), + } + } + + fn render_section_title(&self, title: &str) -> impl IntoElement { + let title = title.to_string(); + div() + .text_lg() + .text_color(colors::foreground()) + .mb_3() + .child(title) + } + + fn render_setting_row(&self, label: &str, value: &str) -> impl IntoElement { + let label = label.to_string(); + let value = value.to_string(); + div() + .flex() + .items_center() + .justify_between() + .px_4() + .py_3() + .mb_2() + .bg(gpui::rgba(0x2A2A2AFF)) + .rounded_md() + .child(div().text_color(colors::foreground()).child(label)) + .child(div().text_color(gpui::rgba(0xAAAAAAFF)).child(value)) + } +} + +impl Render for SettingsView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .w_full() + .h_full() + .p_4() + .child( + div() + .text_xl() + .text_color(colors::foreground()) + .mb_6() + .child("Settings"), + ) + .child( + div() + .flex() + .flex_col() + .gap_6() + .child( + div() + .flex() + .flex_col() + .child(self.render_section_title("Application")) + .child(self.render_setting_row( + "Auto-start Daemon", + if self.settings.daemon_auto_start { + "Enabled" + } else { + "Disabled" + }, + )) + .child(self.render_setting_row( + "Show Network Logs", + if self.settings.show_network_logs { + "Enabled" + } else { + "Disabled" + }, + )), + ) + .child( + div() + .flex() + .flex_col() + .child(self.render_section_title("Appearance")) + .child(self.render_setting_row( + "Theme", + match self.settings.theme { + Theme::Light => "Light", + Theme::Dark => "Dark", + Theme::Auto => "Auto", + }, + )), + ) + .child( + div() + .flex() + .flex_col() + .child(self.render_section_title("Logging")) + .child( + self.render_setting_row("Log Level", &self.settings.logging_level), + ), + ) + .child( + div() + .flex() + .flex_col() + .child(self.render_section_title("About")) + .child( + div() + .px_4() + .py_3() + .bg(gpui::rgba(0x2A2A2AFF)) + .rounded_md() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_color(colors::foreground()) + .child("WebSocket Reflector X"), + ) + .child( + div() + .text_sm() + .text_color(gpui::rgba(0xAAAAAAFF)) + .child("Version 0.5.14"), + ) + .child( + div() + .text_sm() + .text_color(gpui::rgba(0x888888FF)) + .child( + "Controlled TCP-over-WebSocket tunneling tool", + ), + ), + ), + ), + ), + ) + } +} diff --git a/crates/wsrx-desktop-gpui/src/views/sidebar.rs b/crates/wsrx-desktop-gpui/src/views/sidebar.rs index 43302ab..ce8ff35 100644 --- a/crates/wsrx-desktop-gpui/src/views/sidebar.rs +++ b/crates/wsrx-desktop-gpui/src/views/sidebar.rs @@ -1,6 +1,125 @@ -// Sidebar view - Navigation and status indicators +// Sidebar view - Navigation sidebar +use gpui::{App, Context, Render, SharedString, Window, div, prelude::*, svg}; + +use crate::{ + models::app_state::Page, + styles::{border_radius, colors, heights, padding, sizes, spacing}, +}; + +type PageChangeCallback = Box; + pub struct SidebarView { - // TODO: Define state + active_page: Page, + on_page_change: Option, +} + +impl SidebarView { + pub fn new(_window: &mut Window, _cx: &mut Context, active_page: Page) -> Self { + Self { + active_page, + on_page_change: None, + } + } + + pub fn set_on_page_change(&mut self, callback: PageChangeCallback) { + self.on_page_change = Some(callback); + } + + pub fn set_active_page(&mut self, page: Page) { + self.active_page = page; + } + + fn render_tab( + &self, page: Page, icon_path: &'static str, cx: &mut Context, + ) -> impl IntoElement { + let is_active = self.active_page == page; + let label_text = match page { + Page::GetStarted => t!("get_started"), + Page::Connections => t!("connections"), + Page::NetworkLogs => t!("network_logs"), + Page::Settings => t!("settings"), + }; + + let id = SharedString::from(format!("sidebar-tab-{:?}", page)); + + div() + .id(id) + .flex() + .flex_row() + .items_center() + .gap(spacing::s_md()) + .px(padding::p_lg()) // Consistent padding for all tabs + .py(padding::p_lg()) + .rounded(border_radius::r_sm()) + .cursor_pointer() + .when(is_active, |div| { + div.bg(colors::layer_3()) + .border_l_4() + .border_color(colors::primary_bg()) + .pl(padding::p_md()) // Adjust left padding to account for border + }) + .when(!is_active, |div| { + div.hover(|div| div.bg(colors::layer_2())) + .pl(padding::p_lg()) // Same left padding as active tab without border + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + // Update our own state first + this.active_page = page; + // Then notify parent + if let Some(ref callback) = this.on_page_change { + callback(page, cx); + } + })) + .child( + svg() + .path(icon_path) + .size(sizes::icon_md()) + .flex_none() // Prevent icon from shrinking + .text_color(if is_active { + colors::primary_bg() + } else { + colors::window_fg() + }), + ) + .child( + div() + .text_color(colors::window_fg()) + .font_weight(if is_active { + gpui::FontWeight::BOLD + } else { + gpui::FontWeight::NORMAL + }) + .child(label_text.to_string()), + ) + } } -// Placeholder - Will be implemented in next phase +impl Render for SidebarView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_macos = cfg!(target_os = "macos"); + + div() + .flex() + .flex_col() + .h_full() // Full height + .gap(spacing::s_sm()) + .px(padding::p_md()) + .pt(if is_macos { + heights::h_lg() + } else { + padding::p_md() + }) + .pb(padding::p_md()) + .bg(colors::layer_1()) + .border_r_1() + .border_color(colors::element_border()) + .child(self.render_tab(Page::GetStarted, "icons/home.svg", cx)) + .child(self.render_tab(Page::Connections, "icons/globe-star.svg", cx)) + .child(self.render_tab(Page::NetworkLogs, "icons/code.svg", cx)) + .child( + // Spacer + div().flex_1(), + ) + .child(self.render_tab(Page::Settings, "icons/settings.svg", cx)) + } +}