diff --git a/.mimocode/.cron-lock b/.mimocode/.cron-lock new file mode 100644 index 0000000..490162b --- /dev/null +++ b/.mimocode/.cron-lock @@ -0,0 +1 @@ +{"pid":373799,"startedAt":1783749172698} \ No newline at end of file diff --git a/.mimocode/plans/1783749335099-nimble-moon.md b/.mimocode/plans/1783749335099-nimble-moon.md new file mode 100644 index 0000000..30abb1f --- /dev/null +++ b/.mimocode/plans/1783749335099-nimble-moon.md @@ -0,0 +1,262 @@ +# Plan: Full egui → Iced Feature Port + +## Scope + +Port ALL egui GUI features to Iced. The egui GUI has: +- 11 dockable panels + document tabs +- 4 external panel crates (filters, AI chat, script, AI script) +- 14 modal dialogs +- 10 top-level menus +- 20 toolbox slots (30+ tools) +- Brush panel (30+ styles, 3 view modes) +- Color panel (HSL wheel, HSV square, palettes) +- Brush editor (4 tabs, live preview) +- Shell infrastructure (viewer, tablet, clipboard, themes) +- ~60 unique features total + +--- + +## Phase 1: Fix Critical Bugs + Working Canvas (MUST DO FIRST) + +**Files:** `app.rs`, `canvas/mod.rs` + +### 1.1 Fix `ButtonPressed` → dispatch `CanvasPointerPressed` +- Add `last_cursor_pos: Option<(f32, f32)>` to `HcieIcedApp` +- Track cursor position in `CursorMoved` handler +- `ButtonPressed` dispatches `CanvasPointerPressed` using tracked position + +### 1.2 Call `refresh_composite()` after every stroke +- After `CanvasPointerMoved` and `CanvasPointerReleased`, call `canvas::render::refresh_composite()` + +### 1.3 Fix coordinate mapping with `mouse_area` +- Wrap canvas with `iced::widget::mouse_area` +- `on_press`, `on_move`, `on_scroll` provide widget-relative coords +- Simplify `screen_to_canvas` to just divide by zoom + +### 1.4 Request repaint after state changes +- Return `iced::Task::perform(async {}, |_| Message::CompositeRefresh)` from drawing messages + +### 1.5 Remove global subscription +- Remove `iced::event::listen_with` +- All events come from `mouse_area` on canvas + +--- + +## Phase 2: Core Panels + +### 2.1 Layers Panel +**File:** New `panels/layers.rs` + +- Layer list with visibility toggle, opacity slider, blend mode selector +- Active layer highlighting +- Lock toggle +- Add/delete/merge down/flatten buttons +- Hierarchical group fold/collapse +- Reorder (move up/down buttons) +- Uses `engine.layer_infos()`, `engine.set_layer_visible()`, `engine.set_layer_opacity()`, `engine.set_layer_blend_mode()`, etc. + +### 2.2 History Panel +**File:** New `panels/history.rs` + +- Undo/redo timeline list +- Jump-to-history-index on click +- Shows `engine.history_description(idx)` for each entry +- Current position highlighting + +### 2.3 Properties Panel +**File:** New `panels/properties.rs` + +- Dynamic properties based on active tool +- Text tool: font, size, color, alignment +- Vector shapes: position, size, rotation, fill/stroke +- Brush: size, opacity, hardness + +### 2.4 Brushes & Tips Panel +**File:** New `panels/brushes.rs` + +- Brush preset browser with categories +- Grid/list/card view modes +- Brush style selection (30+ styles) +- Size/opacity/hardness sliders +- Rendered stroke preview +- Custom preset save/load +- Uses `engine.set_brush_tip()`, `engine.set_color()`, etc. + +### 2.5 Color Palette Panel +**File:** New `panels/color_panel.rs` + +- HSV saturation/value square +- Hue slider +- RGB sliders with numeric inputs +- Hex input +- Palette grid (saved swatches) +- Recent colors + +### 2.6 Toolbar (Top) +**File:** New `panels/toolbar.rs` + +- New/Open/Save buttons +- Undo/Redo buttons +- Zoom in/out/reset +- Active tool mode + size/opacity sliders +- Theme toggle + +### 2.7 Status Bar (Bottom) +**File:** New `panels/status_bar.rs` + +- Active tool name +- Canvas coordinates +- Zoom slider (logarithmic) +- Canvas dimensions + +--- + +## Phase 3: Tool-Specific Panels + +### 3.1 Text Editor Panel +**File:** New `panels/text_editor.rs` + +- Font selector, size, color +- Alignment (left/center/right/justify) +- Orientation (horizontal/vertical) +- Text effects (rotation, warp, extrude) +- Accept/Cancel buttons + +### 3.2 Vector Geometry Panel +**File:** New `panels/geometry.rs` + +- List vector shapes on active layer +- Per-shape: position, size, rotation, fill, stroke, line cap +- Boolean operations between shapes + +### 3.3 Layer Styles Panel +**File:** New `panels/layer_styles.rs` + +- 10 style types with toggles +- Drop shadow, inner shadow, outer/inner glow +- Bevel/emboss, satin, color/gradient/pattern overlay, stroke +- Per-style parameter sliders and color pickers + +### 3.4 Layer Details Panel +**File:** New `panels/layer_details.rs` + +- Layer ID, name, fill opacity +- Clipping mask toggle +- Adjustment type display +- Effects list + +--- + +## Phase 4: Filter System + +### 4.1 Filters Panel (ported from `egui-panel-filters`) +**File:** New `panels/filters.rs` + +- Category tree (Blur, Sharpen, Pixelate, Distort, Stylize, Color & Light, Restore, Noise & Pattern, Procedural Textures) +- Filter selection with bullet indicators +- Dynamic parameter panel via `iced-panel-adapter` +- Apply/Reset buttons +- Instant preview for applicable filters + +### 4.2 Filter Dialogs +**File:** New `dialogs/filters.rs` + +- Parameter dialogs for each filter category +- Uses `ModulePanel` + `render_module_panel()` from iced-panel-adapter + +--- + +## Phase 5: Menus + +### 5.1 Menu Bar +**File:** New `panels/menus.rs` + +- File: New, Open, Save, Save As, Import, Export, Close, Exit +- Edit: Undo, Redo, Cut, Copy, Paste, Fill, Free Transform, Preferences +- Tools: All tool shortcuts +- Image: Adjustments, Image Size, Canvas Size, Rotation, Flip +- Layer: New, Delete, Merge Down, Flatten, Styles, Align +- Filter: All filter categories with submenus +- Select: All, Deselect, Invert, Grow, Shrink, Feather +- View: Zoom, Reset Pan, Panels submenu, Theme submenu +- Window: Documents list, Dock Layout +- Help: About + +--- + +## Phase 6: Dialogs + +### 6.1 New Image Dialog +### 6.2 Adjustments Dialog (Brightness/Contrast, HSL) +### 6.3 Close Confirm Dialog +### 6.4 Selection Operation Dialog +### 6.5 Settings/Preferences Dialog +### 6.6 Expand Canvas Dialog + +--- + +## Phase 7: External Panel Crates + +### 7.1 iced-panel-filters (port from egui-panel-filters) +- All 35+ filter definitions +- Parameter panels via iced-panel-adapter +- Instant preview integration + +### 7.2 iced-panel-ai-chat (port from egui-panel-ai-chat) +- Streaming chat UI +- LLM connectivity (Ollama, OpenAI) +- Tool calling (execute on canvas) +- DSL script execution + +### 7.3 iced-panel-script (port from egui-panel-script) +- Script editor with line numbers +- DSL parser and executor +- Command reference + +### 7.4 iced-panel-ai-script (port from egui-panel-ai-script) +- AI-assisted script generation +- Multiple LLM providers + +--- + +## Phase 8: Infrastructure + +### 8.1 Tablet/Stylus Input +- Pen pressure via evdev or winit events +- Map pressure to stroke_to() + +### 8.2 Clipboard +- Copy/paste images via arboard +- PNG clipboard support + +### 8.3 File I/O +- Open/Save/Import/Export +- Recent files list +- Format support (PNG, JPG, WebP, PSD, KRA, HCIE native) + +### 8.4 Themes +- 5 theme presets +- Theme switching at runtime + +### 8.5 Multi-Document Tabs +- Tab bar for multiple open documents +- Tab switching + +--- + +## Phase 9: Dock Layout + +### 9.1 Pane Grid Layout +- Use iced's `PaneGrid` for dockable panels +- Panel drag-and-drop +- Panel resize +- Layout save/load + +--- + +## Verification + +After each phase: +1. `cargo build -p hcie-iced-gui` — compiles +2. `cargo run -p hcie-iced-gui` — visual verification +3. `cargo build -p hcie-gui-egui` — no regression diff --git a/.mimocode/plans/1783771837559-curious-river.md b/.mimocode/plans/1783771837559-curious-river.md new file mode 100644 index 0000000..5f1576c --- /dev/null +++ b/.mimocode/plans/1783771837559-curious-river.md @@ -0,0 +1,226 @@ +# Iced GUI — Photoshop-Style UI Overhaul Plan + +## Goal +Transform the iced GUI from a generic dark theme to a professional Photoshop-like appearance with proper toolbox, menus, panels, and window management. + +--- + +## 1. Custom Window Frame (No OS Title Bar) + +**Files:** `main.rs`, `app.rs` + +### Changes: +- Set `decorations: false` in iced window settings to hide OS title bar +- Build custom title bar with: app icon, "HCIE" text, menu bar, window controls (min/max/close) +- Add `Message::WindowDrag`, `Message::WindowMinimize`, `Message::WindowMaximize`, `Message::WindowClose` +- Use `iced::window::drag(id)` for title bar drag-to-move +- Use `iced::window::minimize(id, true)` for minimize +- Use `iced::window::toggle_maximize(id)` for maximize +- Title bar: 24px height, `#2a2a2a` background, subtle bottom border +- Window controls: minimize/maximize/close buttons on right side (like Photoshop) + +### Window Setup: +```rust +iced::application("HCIE", HcieIcedApp::update, HcieIcedApp::view) + .window_settings(window::Settings { + decorations: false, + ..Default::default() + }) +``` + +--- + +## 2. Photoshop-Style Theme + +**Files:** `panels/styles.rs`, `theme.rs` + +### Color Palette (matching Photoshop): +| Token | Value | Usage | +|-------|-------|-------| +| `bg_app` | `#1e1e1e` | Main app background | +| `bg_panel` | `#2d2d2d` | Panel backgrounds | +| `bg_panel_header` | `#333333` | Panel title bars | +| `bg_hover` | `#3a3a3a` | Hover states | +| `bg_active` | `#404040` | Active/selected states | +| `accent` | `#2d8ceb` | Blue accent (selection, active tool) | +| `border` | `#1a1a1a` | Panel borders (very subtle) | +| `border_high` | `#444444` | Emphasized borders | +| `text_primary` | `#cccccc` | Primary text | +| `text_secondary` | `#888888` | Secondary text | +| `text_disabled` | `#555555` | Disabled text | +| `canvas_bg` | `#262626` | Canvas area background | + +### Style Functions to Update: +- `panel_background()` — `#2d2d2d` with no visible border +- `panel_header()` — `#333333` background, no horizontal rule +- `active_item_bg()` — subtle `#404040` highlight (NOT blue tint) +- `inactive_item_bg()` — transparent +- `menubar_background()` — `#2a2a2a` +- `statusbar_background()` — `#2a2a2a` +- `sidebar_background()` — `#2d2d2d` +- `titlebar_background()` — `#1e1e1e` + +--- + +## 3. Toolbox with SVG Icons (Photoshop-Style) + +**Files:** `sidebar/mod.rs`, new: `sidebar/tool_slots.rs` + +### Layout: +- **Single column** (default): 36px wide, icons only, no text labels +- **Two-column mode**: Toggle button (◀/▶ arrow) at top expands to 72px +- Toggle button: small arrow at top of toolbox +- Tool buttons: 28x28px icons, 3px rounded corners +- Active tool: subtle `#404040` background + `#2d8ceb` border +- Hover: `#3a3a3a` background +- Submenu indicator: small triangle for tools with sub-tools + +### Tool Slots (matching egui's TOOL_SLOTS): +``` +Slot 0: Move +Slot 1: VectorSelect +Slot 2: Select (rect) +Slot 3: Lasso, PolygonSelect (submenu) +Slot 4: MagicWand +Slot 5: SmartSelect, VisionSelect (submenu) +Slot 6: Crop +Slot 7: Eyedropper +Slot 8: Brush +Slot 9: Pen +Slot 10: Spray +Slot 11: Eraser +Slot 12: FloodFill +Slot 13: Gradient +Slot 14: Text +Slot 15: VectorLine +Slot 16: VectorRect +Slot 17: VectorCircle +Slot 18: Vector shapes (submenu: Arrow, Star, Polygon, etc.) +Slot 19: Retouch (submenu: SpotRemoval, RedEye, SmartPatch) +``` + +### SVG Icons: +- Use existing SVG files from `hcie-egui-app/assets/icons/` +- Copy SVG files to `hcie-iced-app/assets/icons/` +- Use iced's `svg::Handle::from_path()` to load icons +- Tint icons: active=`#2d8ceb`, default=`#cccccc`, hover=`#ffffff` +- Icon size: 20x20px within 28x28 button + +### Color Swatches (bottom of toolbox): +- Overlapping fg/bg swatches (20x20 each, offset 2px) +- Swap button (⇅) between them +- Reset to B/W button + +--- + +## 4. Panel Titles (Modern, No Blue Tint) + +**Files:** `dock/view.rs` + +### Current (bad): +- Blue tinted background on active panel +- Horizontal rule under title +- Primitive appearance + +### New (Photoshop-style): +- Panel header: `#333333` background, 24px height +- Title text: 11px, `#cccccc` color, no bold +- No horizontal rule +- Close/maximize buttons: subtle, appear on hover only +- Active panel: slightly lighter header (`#3a3a3a`) +- Panel border: 1px `#1a1a1a` (barely visible) + +### Implementation: +- Remove `horizontal_rule` from panel titles +- Replace blue tint with neutral `#3a3a3a` for active +- Make close/maximize buttons hover-only (opacity transition) +- Add small panel icon next to title (from SVG assets) + +--- + +## 5. Menu Dropdown (Overlay, Not Separate Panel) + +**Files:** `panels/menus.rs` + +### Current (bad): +- Menu opens as a separate panel in the dock +- Takes up space, pushes other content + +### New (Photoshop-style): +- Menu dropdown renders as an **overlay** on top of the UI +- Dark background `#2d2d2d`, 1px border `#444444` +- Items: 11px text, 24px height, hover highlight `#3a3a3a` +- Keyboard shortcuts shown on right side in `#888888` +- Separators: 1px line `#3a3a3a` +- Click outside to close +- Escape to close + +### Implementation: +- Track `active_menu: Option` in app state +- When menu button clicked, set `active_menu = Some(idx)` +- Render dropdown as a `Stack` overlay on top of main content +- Use `iced::widget::mouse_area` wrapper to detect clicks outside +- Menu items dispatch `Message::MenuAction(menu_idx, item_idx)` + +--- + +## 6. Dock Manager (Draggable Panels) + +**Files:** `dock/state.rs`, `dock/view.rs` + +### Current: +- Dock exists with PaneGrid but drag/drop may not be fully functional +- Panel rearrangement limited + +### New: +- Ensure `on_drag(Message::PaneDragged)` is properly wired +- Handle `DragEvent::Picked`, `DragEvent::Dropped`, `DragEvent::Canceled` +- On `Dropped`: call `state.drop(pane, target)` for proper rearrangement +- Add right-click context menu on panel titles (Pin, Float, Close) +- Support panel undocking (float) and re-docking + +### Implementation: +- Wire `Message::PaneDragged` handler to call appropriate state methods +- Add `PaneDrop(pane, target)` message that calls `state.drop()` +- Add context menu overlay for panel title bars + +--- + +## 7. Menu Items (Full Photoshop-Style) + +**Files:** `panels/menus.rs`, `app.rs` + +### Menu Structure: +| Menu | Items | +|------|-------| +| File | New, Open, Open Recent, Save, Save As, Export PNG, Export JPEG, Close | +| Edit | Undo, Redo, Cut, Copy, Paste, Select All, Deselect, Fill | +| Image | Canvas Size, Image Size, Rotate CW, Rotate CCW, Flip H, Flip V | +| Layer | New Layer, Duplicate, Delete, Merge Down, Flatten, Clear | +| Filter | (all filter categories with submenus) | +| Select | All, Deselect, Invert, Grow, Shrink, Feather | +| View | Zoom In, Zoom Out, Fit Screen, 100%, Panel Toggles | +| Window | Dock Profile, Reset Layout | +| Help | About, Documentation | + +--- + +## Execution Order + +1. **Theme colors** — Update `styles.rs` with Photoshop palette +2. **Custom title bar** — Hide OS decorations, build custom title bar +3. **Toolbox SVG icons** — Copy SVGs, implement icon rendering +4. **Toolbox layout** — Single column + expand toggle +5. **Panel titles** — Remove blue tint, modernize +6. **Menu overlay** — Implement dropdown overlay system +7. **Dock drag/drop** — Wire pane rearrangement +8. **Menu items** — Wire all menu actions + +--- + +## Verification + +1. Build: `cargo build -p hcie-iced-gui` +2. Launch and compare with Photoshop screenshot +3. Test: tool selection, drawing, color picking, menu dropdowns, panel rearrangement +4. Screenshot comparison with Photoshop reference diff --git a/Cargo.lock b/Cargo.lock index 0d88c0f..39a3aca 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -96,7 +107,7 @@ dependencies = [ "log", "ndk", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "thiserror 2.0.18", ] @@ -107,6 +118,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -188,14 +208,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", - "image", + "image 0.25.10", "log", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", - "parking_lot", + "parking_lot 0.12.5", "percent-encoding", "windows-sys 0.60.2", "x11rb", @@ -209,7 +229,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -239,6 +259,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "ashpd" version = "0.11.1" @@ -258,7 +287,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus", + "zbus 5.16.0", ] [[package]] @@ -376,7 +405,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -411,7 +440,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -497,6 +526,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -515,6 +553,12 @@ dependencies = [ "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -578,6 +622,12 @@ dependencies = [ "wyz", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -630,6 +680,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "byte-slice-cast" version = "1.2.3" @@ -653,7 +709,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -753,6 +809,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -777,13 +839,53 @@ dependencies = [ "error-code", ] +[[package]] +name = "clipboard_macos" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" +dependencies = [ + "thiserror 2.0.18", + "x11rb", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + [[package]] name = "codespan-reporting" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -807,6 +909,37 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "combine" version = "4.6.7" @@ -885,6 +1018,29 @@ dependencies = [ "libm", ] +[[package]] +name = "cosmic-text" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +dependencies = [ + "bitflags 2.13.0", + "fontdb 0.16.2", + "log", + "rangemap", + "rayon", + "rustc-hash 1.1.0", + "rustybuzz 0.14.1", + "self_cell", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -944,12 +1100,48 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" +dependencies = [ + "dtor", +] + [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "d3d12" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +dependencies = [ + "bitflags 2.13.0", + "libloading 0.8.9", + "winapi", +] + +[[package]] +name = "dark-light" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs 4.0.0", + "objc", + "rust-ini", + "web-sys", + "winreg", + "zbus 4.4.0", +] + [[package]] name = "data-url" version = "0.3.2" @@ -978,6 +1170,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "defmt" version = "1.1.0" @@ -998,7 +1196,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1018,9 +1216,15 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "digest" version = "0.10.7" @@ -1031,13 +1235,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", ] [[package]] @@ -1078,7 +1302,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1087,9 +1311,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading", + "libloading 0.8.9", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "document-features" version = "0.2.12" @@ -1111,6 +1341,52 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "dtor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" + [[package]] name = "duplicate" version = "2.0.1" @@ -1139,24 +1415,24 @@ version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98fe83b2589105b69dd25ca1e0fa2135a6e864d502fd8e08978f937e128cfef" dependencies = [ - "ahash", + "ahash 0.8.12", "bytemuck", "document-features", "egui", "egui-wgpu", "egui-winit", "egui_glow", - "glow", + "glow 0.17.0", "glutin", "glutin-winit", "home", - "image", + "image 0.25.10", "js-sys", "log", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", - "parking_lot", + "parking_lot 0.12.5", "percent-encoding", "profiling", "raw-window-handle", @@ -1178,7 +1454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3" dependencies = [ "accesskit", - "ahash", + "ahash 0.8.12", "bitflags 2.13.0", "emath", "epaint", @@ -1272,7 +1548,7 @@ version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f0c0559ac5598a1b887a6206dccbab7e3e6246c57cb00ae287262bd44776c9c" dependencies = [ - "ahash", + "ahash 0.8.12", "bytemuck", "document-features", "egui", @@ -1282,7 +1558,7 @@ dependencies = [ "thiserror 2.0.18", "type-map", "web-time", - "wgpu", + "wgpu 29.0.3", "winit", ] @@ -1325,10 +1601,10 @@ version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598d8675f6fd9088db8a93d8c7aacda936b2f3d0c2b0660ad1744a45b5caf922" dependencies = [ - "ahash", + "ahash 0.8.12", "egui", "enum-map", - "image", + "image 0.25.10", "log", "mime_guess2", "profiling", @@ -1343,7 +1619,7 @@ checksum = "62b652957fa7e1ab01e8fecbfbf4e35f6e43a53fa98af8a562b50d5403cd44b9" dependencies = [ "bytemuck", "egui", - "glow", + "glow 0.17.0", "log", "memoffset 0.9.1", "profiling", @@ -1400,7 +1676,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1421,7 +1697,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1432,7 +1708,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1464,19 +1740,19 @@ version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79" dependencies = [ - "ahash", + "ahash 0.8.12", "bytemuck", "ecolor", "emath", "epaint_default_fonts", - "font-types", + "font-types 0.11.3", "log", "nohash-hasher", - "parking_lot", + "parking_lot 0.12.5", "profiling", "self_cell", "serde", - "skrifa", + "skrifa 0.40.0", "smallvec", "vello_cpu", ] @@ -1504,7 +1780,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1529,6 +1805,16 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "euclid" version = "0.22.14" @@ -1547,7 +1833,7 @@ dependencies = [ "bitvec", "cfg-if", "libc", - "nix", + "nix 0.23.2", "thiserror 1.0.69", ] @@ -1582,6 +1868,7 @@ dependencies = [ "half", "lebe", "miniz_oxide", + "rayon-core", "smallvec", "zune-inflate", ] @@ -1595,6 +1882,12 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastnoise-lite" version = "1.1.1" @@ -1674,6 +1967,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +dependencies = [ + "bytemuck", +] + [[package]] name = "font-types" version = "0.11.3" @@ -1693,6 +1995,34 @@ dependencies = [ "roxmltree", ] +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.21.1", +] + [[package]] name = "fontdb" version = "0.21.0" @@ -1744,7 +2074,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1843,7 +2173,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1966,12 +2296,30 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.17.0" @@ -1991,13 +2339,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ "bitflags 2.13.0", - "cfg_aliases", + "cfg_aliases 0.2.1", "cgl", "dispatch2", "glutin_egl_sys", "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", + "glutin_wgl_sys 0.6.1", + "libloading 0.8.9", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", @@ -2015,7 +2363,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "glutin", "raw-window-handle", "winit", @@ -2041,6 +2389,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + [[package]] name = "glutin_wgl_sys" version = "0.6.1" @@ -2050,6 +2407,68 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +dependencies = [ + "bitflags 2.13.0", + "gpu-descriptor-types", + "hashbrown 0.14.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "h2" version = "0.4.15" @@ -2081,13 +2500,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", ] [[package]] @@ -2116,6 +2554,21 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.13.0", + "com", + "libc", + "libloading 0.8.9", + "thiserror 1.0.69", + "widestring", + "winapi", +] + [[package]] name = "hcie-ai" version = "0.1.0" @@ -2146,7 +2599,7 @@ dependencies = [ "eframe", "egui", "hcie-color", - "image", + "image 0.25.10", "proptest", "rand 0.8.6", "rayon", @@ -2241,8 +2694,8 @@ dependencies = [ "hcie-text", "hcie-tile", "hcie-vector", - "image", - "libloading", + "image 0.25.10", + "libloading 0.8.9", "log", "proptest", "psd", @@ -2265,7 +2718,7 @@ dependencies = [ "hcie-blend", "hcie-color", "hcie-protocol", - "image", + "image 0.25.10", "log", "proptest", "rand 0.8.6", @@ -2290,7 +2743,7 @@ dependencies = [ "approx", "arboard", "base64", - "dirs", + "dirs 5.0.1", "eframe", "egui", "egui-panel-adapter", @@ -2303,7 +2756,7 @@ dependencies = [ "env_logger", "evdev", "hcie-engine-api", - "image", + "image 0.25.10", "log", "proptest", "quick-xml 0.36.2", @@ -2328,6 +2781,22 @@ dependencies = [ "rstest", ] +[[package]] +name = "hcie-iced-gui" +version = "0.1.0" +dependencies = [ + "arboard", + "env_logger", + "evdev", + "hcie-engine-api", + "iced", + "iced-panel-adapter", + "image 0.25.10", + "log", + "rfd", + "serde_json", +] + [[package]] name = "hcie-io" version = "0.1.0" @@ -2342,7 +2811,7 @@ dependencies = [ "hcie-kra", "hcie-protocol", "hcie-psd-saver", - "image", + "image 0.25.10", "log", "proptest", "psd", @@ -2364,7 +2833,7 @@ dependencies = [ "hcie-blend", "hcie-fx", "hcie-protocol", - "image", + "image 0.25.10", "log", "quick-xml 0.36.2", "rand 0.8.6", @@ -2466,7 +2935,7 @@ dependencies = [ "bincode", "hcie-engine-api", "hcie-protocol", - "image", + "image 0.25.10", "log", "proptest", "rand 0.8.6", @@ -2627,6 +3096,221 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iced" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f" +dependencies = [ + "iced_core", + "iced_futures", + "iced_renderer", + "iced_widget", + "iced_winit", + "image 0.24.9", + "thiserror 1.0.69", +] + +[[package]] +name = "iced-panel-adapter" +version = "0.1.0" +dependencies = [ + "hcie-engine-api", + "iced", + "serde_json", +] + +[[package]] +name = "iced-panel-ai-chat" +version = "0.1.0" +dependencies = [ + "hcie-engine-api", + "iced", + "log", + "reqwest", + "serde_json", + "tokio", +] + +[[package]] +name = "iced-panel-script" +version = "0.1.0" +dependencies = [ + "hcie-engine-api", + "iced", + "log", + "serde_json", +] + +[[package]] +name = "iced_core" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "dark-light", + "glam", + "log", + "num-traits", + "once_cell", + "palette", + "rustc-hash 2.1.2", + "smol_str", + "thiserror 1.0.69", + "web-time", +] + +[[package]] +name = "iced_futures" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c04a6745ba2e80f32cf01e034fd00d853aa4f4cd8b91888099cb7aaee0d5d7c" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash 2.1.2", + "tokio", + "wasm-bindgen-futures", + "wasm-timer", +] + +[[package]] +name = "iced_glyphon" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c3bb56f1820ca252bc1d0994ece33d233a55657c0c263ea7cb16895adbde82" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.1.2", + "wgpu 0.19.4", +] + +[[package]] +name = "iced_graphics" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "image 0.24.9", + "kamadak-exif", + "log", + "once_cell", + "raw-window-handle", + "rustc-hash 2.1.2", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_renderer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73558208059f9e622df2bf434e044ee2f838ce75201a023cf0ca3e1244f46c2a" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_runtime" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348b5b2c61c934d88ca3b0ed1ed913291e923d086a66fa288ce9669da9ef62b5" +dependencies = [ + "bytes", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c625d368284fcc43b0b36b176f76eff1abebe7959dd58bd8ce6897d641962a50" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_graphics", + "kurbo 0.10.4", + "log", + "resvg 0.42.0", + "rustc-hash 2.1.2", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "futures", + "glam", + "guillotiere", + "iced_glyphon", + "iced_graphics", + "log", + "once_cell", + "resvg 0.42.0", + "rustc-hash 2.1.2", + "thiserror 1.0.69", + "wgpu 0.19.4", +] + +[[package]] +name = "iced_widget" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d" +dependencies = [ + "iced_renderer", + "iced_runtime", + "num-traits", + "once_cell", + "rustc-hash 2.1.2", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_winit" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44cd4e1c594b6334f409282937bf972ba14d31fedf03c23aa595d982a2fda28" +dependencies = [ + "iced_futures", + "iced_graphics", + "iced_runtime", + "log", + "rustc-hash 2.1.2", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "window_clipboard", + "winit", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -2730,6 +3414,24 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif 0.13.3", + "jpeg-decoder", + "num-traits", + "png 0.17.16", + "qoi", + "tiff 0.9.1", +] + [[package]] name = "image" version = "0.25.10" @@ -2750,7 +3452,7 @@ dependencies = [ "qoi", "ravif", "rgb", - "tiff", + "tiff 0.11.3", "zune-core 0.5.1", "zune-jpeg 0.5.15", ] @@ -2775,6 +3477,12 @@ dependencies = [ "quick-error 2.0.1", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "imagesize" version = "0.13.0" @@ -2797,6 +3505,15 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -2805,7 +3522,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2857,7 +3574,7 @@ checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2887,7 +3604,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.118", ] [[package]] @@ -2915,7 +3632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2928,6 +3645,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.103" @@ -2939,12 +3665,42 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.9", + "pkg-config", +] + [[package]] name = "khronos_api" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "kurbo" version = "0.11.3" @@ -2990,6 +3746,16 @@ dependencies = [ "cc", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -3030,6 +3796,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3072,6 +3844,21 @@ dependencies = [ "imgref", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -3114,6 +3901,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +dependencies = [ + "bitflags 2.13.0", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "mime" version = "0.3.17" @@ -3187,6 +3989,32 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "naga" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +dependencies = [ + "bit-set 0.5.3", + "bitflags 2.13.0", + "codespan-reporting 0.11.1", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + [[package]] name = "naga" version = "29.0.3" @@ -3197,8 +4025,8 @@ dependencies = [ "bit-set 0.9.1", "bitflags 2.13.0", "cfg-if", - "cfg_aliases", - "codespan-reporting", + "cfg_aliases 0.2.1", + "codespan-reporting 0.13.1", "half", "hashbrown 0.16.1", "hexf-parse", @@ -3238,7 +4066,7 @@ dependencies = [ "bitflags 2.13.0", "jni-sys 0.3.1", "log", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -3250,6 +4078,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -3278,6 +4115,19 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -3326,7 +4176,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -3378,7 +4228,17 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", ] [[package]] @@ -3419,7 +4279,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -3599,6 +4459,18 @@ dependencies = [ "objc2-metal", ] +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-symbols" version = "0.2.2" @@ -3624,7 +4496,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -3666,6 +4538,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3700,7 +4581,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -3737,6 +4618,16 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3756,6 +4647,30 @@ dependencies = [ "ttf-parser 0.25.1", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "panel-tuner" version = "0.1.0" @@ -3770,6 +4685,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -3777,7 +4703,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -3854,7 +4794,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.118", "unicase", ] @@ -3891,7 +4831,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -3900,6 +4840,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.5" @@ -4011,6 +4957,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -4039,7 +4991,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -4059,7 +5011,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "version_check", ] @@ -4079,7 +5031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -4111,7 +5063,7 @@ dependencies = [ "hcie-fx", "hcie-protocol", "hcie-psd-saver", - "image", + "image 0.25.10", "log", "rand 0.8.6", "serde", @@ -4268,6 +5220,18 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + [[package]] name = "rav1e" version = "0.8.1" @@ -4343,6 +5307,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" +dependencies = [ + "bytemuck", + "font-types 0.7.3", +] + [[package]] name = "read-fonts" version = "0.37.0" @@ -4350,7 +5324,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" dependencies = [ "bytemuck", - "font-types", + "font-types 0.11.3", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -4477,6 +5460,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" +dependencies = [ + "gif 0.13.3", + "jpeg-decoder", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg 0.42.0", +] + [[package]] name = "resvg" version = "0.43.0" @@ -4601,10 +5600,20 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.118", "unicode-ident", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4703,6 +5712,23 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring 0.2.0", + "unicode-ccc 0.2.0", + "unicode-properties", + "unicode-script", +] + [[package]] name = "rustybuzz" version = "0.18.0" @@ -4715,8 +5741,8 @@ dependencies = [ "log", "smallvec", "ttf-parser 0.24.1", - "unicode-bidi-mirroring", - "unicode-ccc", + "unicode-bidi-mirroring 0.3.0", + "unicode-ccc 0.3.0", "unicode-properties", "unicode-script", ] @@ -4761,7 +5787,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" name = "screenshot-diff" version = "0.1.0" dependencies = [ - "image", + "image 0.25.10", ] [[package]] @@ -4839,7 +5865,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -4863,7 +5889,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -4887,6 +5913,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4960,6 +5997,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts 0.22.7", +] + [[package]] name = "skrifa" version = "0.40.0" @@ -4967,7 +6014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.37.0", ] [[package]] @@ -5076,6 +6123,47 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "drm", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5103,6 +6191,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + [[package]] name = "svgtypes" version = "0.15.3" @@ -5113,6 +6207,28 @@ dependencies = [ "siphasher", ] +[[package]] +name = "swash" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" +dependencies = [ + "skrifa 0.22.3", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.118" @@ -5141,7 +6257,16 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", ] [[package]] @@ -5203,6 +6328,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5229,7 +6363,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -5240,7 +6374,18 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", ] [[package]] @@ -5283,6 +6428,19 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-xlib" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading 0.8.9", + "pkg-config", + "tracing", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -5317,7 +6475,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5333,7 +6491,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -5485,7 +6643,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -5503,6 +6661,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + [[package]] name = "ttf-parser" version = "0.21.1" @@ -5574,12 +6738,24 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + [[package]] name = "unicode-ccc" version = "0.3.0" @@ -5592,6 +6768,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-properties" version = "0.1.4" @@ -5616,12 +6798,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -5647,6 +6841,33 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "usvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb 0.18.0", + "imagesize 0.12.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree", + "rustybuzz 0.14.1", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "usvg" version = "0.43.0" @@ -5656,13 +6877,13 @@ dependencies = [ "base64", "data-url", "flate2", - "fontdb", - "imagesize", + "fontdb 0.21.0", + "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", "roxmltree", - "rustybuzz", + "rustybuzz 0.18.0", "simplecss", "siphasher", "strict-num", @@ -5683,7 +6904,7 @@ dependencies = [ "base64", "data-url", "flate2", - "imagesize", + "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", @@ -5747,7 +6968,7 @@ dependencies = [ "hashbrown 0.16.1", "log", "peniko", - "skrifa", + "skrifa 0.40.0", "smallvec", ] @@ -5859,7 +7080,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -5885,6 +7106,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.15" @@ -6062,6 +7298,31 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +dependencies = [ + "arrayvec", + "cfg-if", + "cfg_aliases 0.1.1", + "js-sys", + "log", + "naga 0.19.2", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core 0.19.4", + "wgpu-hal 0.19.5", + "wgpu-types 0.19.2", +] + [[package]] name = "wgpu" version = "29.0.3" @@ -6072,7 +7333,7 @@ dependencies = [ "bitflags 2.13.0", "bytemuck", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "hashbrown 0.16.1", "js-sys", @@ -6085,9 +7346,35 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core 29.0.3", + "wgpu-hal 29.0.3", + "wgpu-types 29.0.3", +] + +[[package]] +name = "wgpu-core" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +dependencies = [ + "arrayvec", + "bit-vec 0.6.3", + "bitflags 2.13.0", + "cfg_aliases 0.1.1", + "codespan-reporting 0.11.1", + "indexmap", + "log", + "naga 0.19.2", + "once_cell", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "web-sys", + "wgpu-hal 0.19.5", + "wgpu-types 0.19.2", ] [[package]] @@ -6101,14 +7388,14 @@ dependencies = [ "bit-vec 0.9.1", "bitflags 2.13.0", "bytemuck", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "hashbrown 0.16.1", "indexmap", "log", - "naga", + "naga 29.0.3", "once_cell", - "parking_lot", + "parking_lot 0.12.5", "portable-atomic", "profiling", "raw-window-handle", @@ -6116,9 +7403,9 @@ dependencies = [ "smallvec", "thiserror 2.0.18", "wgpu-core-deps-windows-linux-android", - "wgpu-hal", + "wgpu-hal 29.0.3", "wgpu-naga-bridge", - "wgpu-types", + "wgpu-types 29.0.3", ] [[package]] @@ -6127,7 +7414,52 @@ version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916" dependencies = [ - "wgpu-hal", + "wgpu-hal 29.0.3", +] + +[[package]] +name = "wgpu-hal" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.5.3", + "bitflags 2.13.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow 0.13.1", + "glutin_wgl_sys 0.5.0", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.9", + "log", + "metal", + "naga 0.19.2", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot 0.12.5", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types 0.19.2", + "winapi", ] [[package]] @@ -6138,17 +7470,17 @@ checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f" dependencies = [ "bitflags 2.13.0", "cfg-if", - "cfg_aliases", - "libloading", + "cfg_aliases 0.2.1", + "libloading 0.8.9", "log", - "naga", + "naga 29.0.3", "portable-atomic", "portable-atomic-util", "raw-window-handle", "renderdoc-sys", "thiserror 2.0.18", "wgpu-naga-bridge", - "wgpu-types", + "wgpu-types 29.0.3", ] [[package]] @@ -6157,8 +7489,19 @@ version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02" dependencies = [ - "naga", - "wgpu-types", + "naga 29.0.3", + "wgpu-types 29.0.3", +] + +[[package]] +name = "wgpu-types" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +dependencies = [ + "bitflags 2.13.0", + "js-sys", + "web-sys", ] [[package]] @@ -6175,6 +7518,28 @@ dependencies = [ "web-sys", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -6184,6 +7549,45 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window_clipboard" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -6456,14 +7860,14 @@ version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ - "ahash", + "ahash 0.8.12", "android-activity", "atomic-waker", "bitflags 2.13.0", "block2 0.5.1", "bytemuck", "calloop 0.13.0", - "cfg_aliases", + "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation 0.9.4", "core-graphics", @@ -6511,6 +7915,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -6552,7 +7965,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading", + "libloading 0.8.9", "once_cell", "rustix 1.1.4", "x11rb-protocol", @@ -6570,6 +7983,16 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -6607,6 +8030,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + [[package]] name = "yoke" version = "0.8.3" @@ -6626,10 +8055,48 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "synstructure", ] +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.6", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + [[package]] name = "zbus" version = "5.16.0" @@ -6660,9 +8127,22 @@ dependencies = [ "uuid", "windows-sys 0.61.2", "winnow", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 5.16.0", + "zbus_names 4.3.2", + "zvariant 5.12.0", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils 2.1.0", ] [[package]] @@ -6674,10 +8154,21 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", - "zbus_names", - "zvariant", - "zvariant_utils", + "syn 2.0.118", + "zbus_names 4.3.2", + "zvariant 5.12.0", + "zvariant_utils 3.4.0", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", ] [[package]] @@ -6688,9 +8179,15 @@ checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", "winnow", - "zvariant", + "zvariant 5.12.0", ] +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + [[package]] name = "zerocopy" version = "0.8.52" @@ -6708,7 +8205,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -6728,7 +8225,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "synstructure", ] @@ -6768,7 +8265,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -6845,6 +8342,19 @@ dependencies = [ "zune-core 0.5.1", ] +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + [[package]] name = "zvariant" version = "5.12.0" @@ -6856,8 +8366,21 @@ dependencies = [ "serde", "url", "winnow", - "zvariant_derive", - "zvariant_utils", + "zvariant_derive 5.12.0", + "zvariant_utils 3.4.0", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils 2.1.0", ] [[package]] @@ -6869,8 +8392,19 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", - "zvariant_utils", + "syn 2.0.118", + "zvariant_utils 3.4.0", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -6882,6 +8416,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.118", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 130e0e2..98a1a05 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,10 @@ members = [ "hcie-egui-app/crates/egui-panel-ai-chat", "hcie-egui-app/crates/egui-panel-script", "hcie-egui-app/crates/egui-panel-ai-script", + "hcie-iced-app/crates/hcie-iced-gui", + "hcie-iced-app/crates/iced-panel-adapter", + "hcie-iced-app/crates/iced-panel-script", + "hcie-iced-app/crates/iced-panel-ai-chat", "tools/screenshot-diff", "tools/panel-tuner", ] @@ -58,6 +62,7 @@ eframe = { version = "0.34", default-features = false, features = ["default egui_extras = { version = "0.34", features = ["svg", "image"] } egui_dock = { version = "0.19", features = ["serde"] } rfd = "0.15" +iced = { version = "0.13", features = ["tokio", "image", "svg"] } # Utilities base64 = "0.22" diff --git a/hcie-iced-app/Cargo.toml b/hcie-iced-app/Cargo.toml new file mode 100644 index 0000000..7bcdd8e --- /dev/null +++ b/hcie-iced-app/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hcie-iced-app" +version = "0.1.0" +edition = "2021" + +[dependencies] +hcie-iced-gui = { path = "crates/hcie-iced-gui" } +hcie-engine-api = { path = "../hcie-engine-api" } diff --git a/hcie-iced-app/assets/icons/brush.svg b/hcie-iced-app/assets/icons/brush.svg new file mode 100644 index 0000000..dfaa527 --- /dev/null +++ b/hcie-iced-app/assets/icons/brush.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/bucket.svg b/hcie-iced-app/assets/icons/bucket.svg new file mode 100644 index 0000000..d028f85 --- /dev/null +++ b/hcie-iced-app/assets/icons/bucket.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/circle.svg b/hcie-iced-app/assets/icons/circle.svg new file mode 100644 index 0000000..60a03ab --- /dev/null +++ b/hcie-iced-app/assets/icons/circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/hcie-iced-app/assets/icons/close.svg b/hcie-iced-app/assets/icons/close.svg new file mode 100644 index 0000000..455a23c --- /dev/null +++ b/hcie-iced-app/assets/icons/close.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/close_x.svg b/hcie-iced-app/assets/icons/close_x.svg new file mode 100644 index 0000000..51ca5de --- /dev/null +++ b/hcie-iced-app/assets/icons/close_x.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/crop.svg b/hcie-iced-app/assets/icons/crop.svg new file mode 100644 index 0000000..1e4608b --- /dev/null +++ b/hcie-iced-app/assets/icons/crop.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/eraser.svg b/hcie-iced-app/assets/icons/eraser.svg new file mode 100644 index 0000000..5f75768 --- /dev/null +++ b/hcie-iced-app/assets/icons/eraser.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/eyedropper.svg b/hcie-iced-app/assets/icons/eyedropper.svg new file mode 100644 index 0000000..e7f12d2 --- /dev/null +++ b/hcie-iced-app/assets/icons/eyedropper.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/glow.svg b/hcie-iced-app/assets/icons/glow.svg new file mode 100644 index 0000000..86294ee --- /dev/null +++ b/hcie-iced-app/assets/icons/glow.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/gradient.svg b/hcie-iced-app/assets/icons/gradient.svg new file mode 100644 index 0000000..569246b --- /dev/null +++ b/hcie-iced-app/assets/icons/gradient.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/hatch.svg b/hcie-iced-app/assets/icons/hatch.svg new file mode 100644 index 0000000..879b1d1 --- /dev/null +++ b/hcie-iced-app/assets/icons/hatch.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/import_abr.svg b/hcie-iced-app/assets/icons/import_abr.svg new file mode 100644 index 0000000..5eb226b --- /dev/null +++ b/hcie-iced-app/assets/icons/import_abr.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/import_kpp.svg b/hcie-iced-app/assets/icons/import_kpp.svg new file mode 100644 index 0000000..58d6d5c --- /dev/null +++ b/hcie-iced-app/assets/icons/import_kpp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/lasso.svg b/hcie-iced-app/assets/icons/lasso.svg new file mode 100644 index 0000000..6e56a7d --- /dev/null +++ b/hcie-iced-app/assets/icons/lasso.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/line.svg b/hcie-iced-app/assets/icons/line.svg new file mode 100644 index 0000000..acc35d6 --- /dev/null +++ b/hcie-iced-app/assets/icons/line.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/lock.svg b/hcie-iced-app/assets/icons/lock.svg new file mode 100644 index 0000000..c2f8370 --- /dev/null +++ b/hcie-iced-app/assets/icons/lock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/magic_wand.svg b/hcie-iced-app/assets/icons/magic_wand.svg new file mode 100644 index 0000000..050d961 --- /dev/null +++ b/hcie-iced-app/assets/icons/magic_wand.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/move.svg b/hcie-iced-app/assets/icons/move.svg new file mode 100644 index 0000000..8385076 --- /dev/null +++ b/hcie-iced-app/assets/icons/move.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/new_file.svg b/hcie-iced-app/assets/icons/new_file.svg new file mode 100644 index 0000000..ccd6de5 --- /dev/null +++ b/hcie-iced-app/assets/icons/new_file.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/open_file.svg b/hcie-iced-app/assets/icons/open_file.svg new file mode 100644 index 0000000..167ff35 --- /dev/null +++ b/hcie-iced-app/assets/icons/open_file.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/pan.svg b/hcie-iced-app/assets/icons/pan.svg new file mode 100644 index 0000000..3011bc8 --- /dev/null +++ b/hcie-iced-app/assets/icons/pan.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-ai.svg b/hcie-iced-app/assets/icons/panel-ai.svg new file mode 100644 index 0000000..109c08e --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-ai.svg @@ -0,0 +1,20 @@ + + + + + + + Icon_24px_AIHub_Color + + + + + + + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-brush.svg b/hcie-iced-app/assets/icons/panel-brush.svg new file mode 100644 index 0000000..ce8ba98 --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-brush.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-colorbox.svg b/hcie-iced-app/assets/icons/panel-colorbox.svg new file mode 100644 index 0000000..e2fe29c --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-colorbox.svg @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-filters.svg b/hcie-iced-app/assets/icons/panel-filters.svg new file mode 100644 index 0000000..c5a36c5 --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-filters.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-history.svg b/hcie-iced-app/assets/icons/panel-history.svg new file mode 100644 index 0000000..fd0e502 --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-history.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-layers.svg b/hcie-iced-app/assets/icons/panel-layers.svg new file mode 100644 index 0000000..0ac4abc --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-layers.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-pen.svg b/hcie-iced-app/assets/icons/panel-pen.svg new file mode 100644 index 0000000..28b5718 --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-pen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-plugin.svg b/hcie-iced-app/assets/icons/panel-plugin.svg new file mode 100644 index 0000000..f45c146 --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-plugin.svg @@ -0,0 +1,12 @@ + + + plugin_2_line + + + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/panel-settings.svg b/hcie-iced-app/assets/icons/panel-settings.svg new file mode 100644 index 0000000..b24d1c5 --- /dev/null +++ b/hcie-iced-app/assets/icons/panel-settings.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/patch.svg b/hcie-iced-app/assets/icons/patch.svg new file mode 100644 index 0000000..23450e4 --- /dev/null +++ b/hcie-iced-app/assets/icons/patch.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/pen.svg b/hcie-iced-app/assets/icons/pen.svg new file mode 100644 index 0000000..58e0a81 --- /dev/null +++ b/hcie-iced-app/assets/icons/pen.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/rect_select.svg b/hcie-iced-app/assets/icons/rect_select.svg new file mode 100644 index 0000000..53f6d4d --- /dev/null +++ b/hcie-iced-app/assets/icons/rect_select.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/redeye.svg b/hcie-iced-app/assets/icons/redeye.svg new file mode 100644 index 0000000..8e28bc7 --- /dev/null +++ b/hcie-iced-app/assets/icons/redeye.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/redo.svg b/hcie-iced-app/assets/icons/redo.svg new file mode 100644 index 0000000..a48f9ad --- /dev/null +++ b/hcie-iced-app/assets/icons/redo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/save_file.svg b/hcie-iced-app/assets/icons/save_file.svg new file mode 100644 index 0000000..1436169 --- /dev/null +++ b/hcie-iced-app/assets/icons/save_file.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/sketch.svg b/hcie-iced-app/assets/icons/sketch.svg new file mode 100644 index 0000000..aba874e --- /dev/null +++ b/hcie-iced-app/assets/icons/sketch.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/smart_select.svg b/hcie-iced-app/assets/icons/smart_select.svg new file mode 100644 index 0000000..124f5f7 --- /dev/null +++ b/hcie-iced-app/assets/icons/smart_select.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/spot.svg b/hcie-iced-app/assets/icons/spot.svg new file mode 100644 index 0000000..d24d216 --- /dev/null +++ b/hcie-iced-app/assets/icons/spot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/spray.svg b/hcie-iced-app/assets/icons/spray.svg new file mode 100644 index 0000000..a12912c --- /dev/null +++ b/hcie-iced-app/assets/icons/spray.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/star.svg b/hcie-iced-app/assets/icons/star.svg new file mode 100644 index 0000000..ba3b24a --- /dev/null +++ b/hcie-iced-app/assets/icons/star.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/text.svg b/hcie-iced-app/assets/icons/text.svg new file mode 100644 index 0000000..0f65766 --- /dev/null +++ b/hcie-iced-app/assets/icons/text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/theme_dark.svg b/hcie-iced-app/assets/icons/theme_dark.svg new file mode 100644 index 0000000..17d920c --- /dev/null +++ b/hcie-iced-app/assets/icons/theme_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/theme_light.svg b/hcie-iced-app/assets/icons/theme_light.svg new file mode 100644 index 0000000..84b6d33 --- /dev/null +++ b/hcie-iced-app/assets/icons/theme_light.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/undo.svg b/hcie-iced-app/assets/icons/undo.svg new file mode 100644 index 0000000..3a83736 --- /dev/null +++ b/hcie-iced-app/assets/icons/undo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_arrow.svg b/hcie-iced-app/assets/icons/vector_arrow.svg new file mode 100644 index 0000000..d92cbd6 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_arrow.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_arrow4.svg b/hcie-iced-app/assets/icons/vector_arrow4.svg new file mode 100644 index 0000000..e328ce2 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_arrow4.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_bolt.svg b/hcie-iced-app/assets/icons/vector_bolt.svg new file mode 100644 index 0000000..9cdcc56 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_bolt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_bubble.svg b/hcie-iced-app/assets/icons/vector_bubble.svg new file mode 100644 index 0000000..5823f8d --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_bubble.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/hcie-iced-app/assets/icons/vector_crescent.svg b/hcie-iced-app/assets/icons/vector_crescent.svg new file mode 100644 index 0000000..2371e43 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_crescent.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_cross.svg b/hcie-iced-app/assets/icons/vector_cross.svg new file mode 100644 index 0000000..a989b3c --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_cross.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_cylinder.svg b/hcie-iced-app/assets/icons/vector_cylinder.svg new file mode 100644 index 0000000..ccf4949 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_cylinder.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_gear.svg b/hcie-iced-app/assets/icons/vector_gear.svg new file mode 100644 index 0000000..177dcfb --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_gear.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_heart.svg b/hcie-iced-app/assets/icons/vector_heart.svg new file mode 100644 index 0000000..5751af3 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_heart.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_polygon.svg b/hcie-iced-app/assets/icons/vector_polygon.svg new file mode 100644 index 0000000..a51ba87 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_polygon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_rect.svg b/hcie-iced-app/assets/icons/vector_rect.svg new file mode 100644 index 0000000..0e554f6 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_rect.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_rhombus.svg b/hcie-iced-app/assets/icons/vector_rhombus.svg new file mode 100644 index 0000000..3ee6971 --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_rhombus.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/vector_select.svg b/hcie-iced-app/assets/icons/vector_select.svg new file mode 100644 index 0000000..3bba83d --- /dev/null +++ b/hcie-iced-app/assets/icons/vector_select.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/visible.svg b/hcie-iced-app/assets/icons/visible.svg new file mode 100644 index 0000000..ad22359 --- /dev/null +++ b/hcie-iced-app/assets/icons/visible.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/window_max.svg b/hcie-iced-app/assets/icons/window_max.svg new file mode 100644 index 0000000..b0a454e --- /dev/null +++ b/hcie-iced-app/assets/icons/window_max.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/window_min.svg b/hcie-iced-app/assets/icons/window_min.svg new file mode 100644 index 0000000..6f20be1 --- /dev/null +++ b/hcie-iced-app/assets/icons/window_min.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/hcie-iced-app/assets/icons/window_restore.svg b/hcie-iced-app/assets/icons/window_restore.svg new file mode 100644 index 0000000..aed9374 --- /dev/null +++ b/hcie-iced-app/assets/icons/window_restore.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/hcie-iced-app/assets/icons/zoomin.svg b/hcie-iced-app/assets/icons/zoomin.svg new file mode 100644 index 0000000..0222119 --- /dev/null +++ b/hcie-iced-app/assets/icons/zoomin.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/hcie-iced-app/assets/icons/zoomout.svg b/hcie-iced-app/assets/icons/zoomout.svg new file mode 100644 index 0000000..81bfd11 --- /dev/null +++ b/hcie-iced-app/assets/icons/zoomout.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/hcie-iced-app/crates/hcie-iced-gui/Cargo.toml b/hcie-iced-app/crates/hcie-iced-gui/Cargo.toml new file mode 100644 index 0000000..258933a --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "hcie-iced-gui" +version = "0.1.0" +edition = "2021" + +[features] +default = ["tablet-evdev"] +tablet-evdev = ["dep:evdev"] + +[[bin]] +name = "hcie-iced" +path = "src/main.rs" + +[dependencies] +hcie-engine-api = { path = "../../../hcie-engine-api" } +iced-panel-adapter = { path = "../iced-panel-adapter" } +iced = { workspace = true } +env_logger = { workspace = true } +log = { workspace = true } +image = { workspace = true } +serde_json = { workspace = true } +rfd = { workspace = true } +arboard = { workspace = true } +evdev = { version = "0.12", optional = true } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs new file mode 100644 index 0000000..fa89de4 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -0,0 +1,1581 @@ +//! Core Iced application state and update logic. +//! +//! Implements the Elm-architecture pattern for the HCIE image editor. +//! Manages per-document engine instances, tool state, compositing, +//! dock layout, keyboard shortcuts, and all UI interactions. + +use crate::dialogs; +use crate::dock::state::{DockState, PaneType}; +use crate::io::tablet::TabletState; +use crate::panels; +use crate::theme::ThemeState; +use hcie_engine_api::{BrushTip, BrushStyle, Engine, FilterType, Tool, ZOOM_MAX, ZOOM_MIN}; +use iced::widget::{column, container, text}; +use iced::{Element, Length, Task, Theme, Vector}; +use std::sync::{Arc, Mutex}; + +/// Active dialog type. +#[derive(Debug, Clone, PartialEq)] +pub enum ActiveDialog { + None, + NewImage, + BrightnessContrast, + HueSaturation, + CloseConfirm, + SelectionOp(&'static str), +} + +/// Top-level application state. +pub struct HcieIcedApp { + /// All open documents. + pub documents: Vec, + /// Index of the currently active document. + pub active_doc: usize, + /// Current tool state. + pub tool_state: ToolState, + /// Foreground drawing color (RGBA). + pub fg_color: [u8; 4], + /// Background drawing color (RGBA). + pub bg_color: [u8; 4], + /// Last known cursor position from mouse_area events. + pub last_cursor_pos: Option<(f32, f32)>, + /// Cursor position in canvas-space for status bar display. + pub canvas_cursor_pos: Option<(u32, u32)>, + /// Currently active dialog (if any). + pub active_dialog: ActiveDialog, + /// New Image dialog state. + pub dialog_new_name: String, + pub dialog_new_width: u32, + pub dialog_new_height: u32, + pub dialog_new_transparent: bool, + /// Adjustment dialog state. + pub dialog_brightness: f32, + pub dialog_contrast: f32, + pub dialog_hue: f32, + pub dialog_saturation: f32, + pub dialog_lightness: f32, + /// Selection operation dialog state. + pub dialog_selection_value: f32, + /// Selected filter. + pub selected_filter: Option, + /// Filter parameters (JSON) for the currently selected filter. + pub filter_params: serde_json::Value, + /// Filter preview state. + pub filter_preview_active: bool, + /// Dock layout state. + pub dock: DockState, + /// Shared tablet state for pressure input. + pub tablet_state: Arc>, + /// Whether the app has been initialized (load_path processed). + pub initialized: bool, + /// Theme state for consistent styling. + pub theme_state: ThemeState, + /// Currently open menu index (None = no menu open). + pub active_menu: Option, + /// Whether the toolbox is in two-column mode. + pub toolbox_two_column: bool, +} + +/// Per-document state wrapping an engine instance. +pub struct IcedDocument { + pub engine: Engine, + /// Cached composite RGBA pixel data from the engine. + pub composite_buffer: Vec, + /// Whether the composite needs re-rendering. + pub composite_dirty: bool, + /// Document display name. + pub name: String, + /// Original file path, if loaded from disk. + pub source_path: Option, + /// Whether the document has unsaved changes. + pub modified: bool, + /// Per-document zoom level. + pub zoom: f32, + /// Per-document pan offset in screen pixels. + pub pan_offset: Vector, + /// Cached layer infos for the layers panel. + pub cached_layers: Vec, + /// Cached history items for the history panel. + pub cached_history: Vec<(usize, String)>, + /// Current history index. + pub history_current: i32, + /// Selection rectangle in canvas coordinates (for select tool). + pub selection_rect: Option<(f32, f32, f32, f32)>, + /// Vector shape being drawn (start pos, current pos). + pub vector_draw: Option<((f32, f32), (f32, f32))>, +} + +/// Drawing tool state. +pub struct ToolState { + /// Currently selected tool. + pub active_tool: Tool, + /// Whether the user is currently drawing (mouse button held). + pub is_drawing: bool, + /// Canvas-space coordinates of the last pointer event during a stroke. + pub last_stroke_pos: Option<(f32, f32)>, + /// Accumulated distance for the current stroke (for pressure interpolation). + pub brush_accumulated_dist: f32, + /// Current brush size. + pub brush_size: f32, + /// Current brush opacity. + pub brush_opacity: f32, + /// Current brush hardness. + pub brush_hardness: f32, + /// Current brush style. + pub brush_style: BrushStyle, + /// Current brush spacing. + pub brush_spacing: f32, +} + +impl Default for ToolState { + fn default() -> Self { + Self { + active_tool: Tool::Brush, + is_drawing: false, + last_stroke_pos: None, + brush_accumulated_dist: 0.0, + brush_size: 20.0, + brush_opacity: 1.0, + brush_hardness: 0.8, + brush_style: BrushStyle::Round, + brush_spacing: 1.0, + } + } +} + +/// Messages the application handles. +#[derive(Debug, Clone)] +pub enum Message { + // ── Tool selection ────────────────────────────────── + ToolSelected(Tool), + + // ── Color ─────────────────────────────────────────── + FgColorChanged([u8; 4]), + BgColorChanged([u8; 4]), + SwapColors, + + // ── Canvas interaction ────────────────────────────── + CanvasPointerPressed { x: f32, y: f32 }, + CanvasPointerMoved { x: f32, y: f32 }, + CanvasPointerReleased, + CanvasZoom { delta: f32 }, + CanvasZoomSet(f32), + + // ── Engine operations ─────────────────────────────── + CompositeRefresh, + Undo, + Redo, + + // ── Layer operations ──────────────────────────────── + LayerSelect(u64), + LayerAdd, + LayerDelete(u64), + LayerToggleVisibility(u64), + LayerToggleLock(u64), + LayerSetOpacity(u64, f32), + LayerMergeDown, + LayerFlatten, + LayerMoveUp(u64), + LayerMoveDown(u64), + + // ── History ───────────────────────────────────────── + HistoryJumpTo(i32), + + // ── Text tool ─────────────────────────────────────── + TextSizeChanged(f32), + TextColorChanged([u8; 4]), + TextAlignChanged(String), + TextAccept, + TextCancel, + + // ── Layer styles ──────────────────────────────────── + LayerStyleToggle(u32), + LayerStyleAdd, + + // ── Brushes ───────────────────────────────────────── + BrushStyleSelected(BrushStyle), + BrushSizeChanged(f32), + BrushOpacityChanged(f32), + BrushHardnessChanged(f32), + + // ── Filters ───────────────────────────────────────── + FilterSelect(FilterType), + FilterApply, + FilterReset, + FilterParamChanged(String, serde_json::Value), + + // ── Dialogs ───────────────────────────────────────── + DialogOpen(ActiveDialog), + DialogClose, + DialogCloseSave, + DialogCloseDiscard, + + // ── New Image Dialog ──────────────────────────────── + DialogNewImageName(String), + DialogNewImageWidth(u32), + DialogNewImageHeight(u32), + DialogNewImageTransparent(bool), + DialogNewImagePreset(u32, u32), + DialogNewImageCreate, + + // ── Adjustments Dialog ────────────────────────────── + AdjBrightness(f32), + AdjContrast(f32), + AdjHue(f32), + AdjSaturation(f32), + AdjLightness(f32), + AdjApply, + + // ── Selection Dialog ──────────────────────────────── + SelectionOpValue(f32), + SelectionOpApply, + + // ── Selection tools ───────────────────────────────── + SelectionDragStart(f32, f32), + SelectionDragMove(f32, f32), + SelectionDragEnd, + + // ── Vector tools ──────────────────────────────────── + VectorDrawStart(f32, f32), + VectorDrawMove(f32, f32), + VectorDrawEnd, + + // ── AI Chat ───────────────────────────────────────── + AiChatInput(String), + AiChatSend, + AiChatClear, + + // ── Clipboard ─────────────────────────────────────── + CopyImage, + PasteImage, + CopyText(String), + PasteText, + ClipboardResult(Result, String>), + ImageCopied(Result<(), String>), + ImagePasted(Result, u32, u32)>, String>), + + // ── File I/O (with rfd) ───────────────────────────── + OpenFileRfd, + SaveFileAs, + FileDialogClosed(Option), + + // ── Dock Layout ───────────────────────────────────── + PaneClicked(iced::widget::pane_grid::Pane), + PaneDragged(iced::widget::pane_grid::DragEvent), + PaneResized(iced::widget::pane_grid::ResizeEvent), + PaneClose(iced::widget::pane_grid::Pane), + PaneMaximize(iced::widget::pane_grid::Pane), + + // ── File I/O ──────────────────────────────────────── + NewDocument(u32, u32), + DocSwitch(usize), + OpenFile, + FileOpened(Result), + SaveFile, + + // ── Menu ───────────────────────────────────────────── + MenuOpen(usize), + MenuClose, + MenuAction(usize, usize), // (menu_index, item_index) + + // ── Toolbox ───────────────────────────────────────── + ToggleToolbox, + + // ── Misc ──────────────────────────────────────────── + NoOp, +} + +/// Convert a FilterType enum to the engine's string filter ID. +fn filter_type_to_id(filter: FilterType) -> &'static str { + match filter { + FilterType::BoxBlur => "box_blur", + FilterType::GaussianBlur => "gaussian_blur", + FilterType::MotionBlur => "motion_blur", + FilterType::UnsharpMask => "sharpen", + FilterType::Mosaic => "mosaic", + FilterType::Pinch => "pinch", + FilterType::Twirl => "twirl", + FilterType::OilPaint => "oil_paint", + FilterType::Crystallize => "crystallize", + FilterType::Levels => "levels", + FilterType::Vibrance => "vibrance", + FilterType::Exposure => "exposure", + FilterType::Posterize => "posterize", + FilterType::Threshold => "threshold", + FilterType::ChannelMixer => "channel_mixer", + FilterType::BlackAndWhite => "black_and_white", + FilterType::SelectiveColor => "selective_color", + FilterType::GammaCorrection => "gamma_correction", + FilterType::ExtractChannel => "extract_channel", + FilterType::GradientMap => "gradient_map", + FilterType::GaussianBlurGamma => "gaussian_blur_gamma", + FilterType::BoxBlurGamma => "box_blur_gamma", + FilterType::MedianFilter => "median_filter", + FilterType::Dehaze => "dehaze", + FilterType::NoisePattern => "noise_pattern", + } +} + +/// Build a BrushTip from the current tool state. +/// +/// Converts the tool state's brush parameters into an engine BrushTip +/// that can be passed to `engine.set_brush_tip()` before starting a stroke. +fn build_brush_tip(state: &ToolState) -> BrushTip { + BrushTip { + style: state.brush_style, + size: state.brush_size, + opacity: state.brush_opacity, + hardness: state.brush_hardness, + spacing: state.brush_spacing, + ..BrushTip::default() + } +} + +impl HcieIcedApp { + /// Create the initial application state. + /// + /// If `load_path` is provided, the file will be opened after initialization. + pub fn new(load_path: Option) -> Self { + let mut engine = Engine::new(800, 600); + engine.pre_tile_all_layers(); + let composite_buffer = engine.get_composite_pixels(); + + let cached_layers = engine.layer_infos(); + let history_len = engine.history_len(); + let cached_history: Vec<(usize, String)> = (0..history_len) + .filter_map(|i| engine.history_description(i).map(|d| (i, d))) + .collect(); + let history_current = engine.history_current(); + + let doc = IcedDocument { + engine, + composite_buffer, + composite_dirty: false, + name: "Untitled".to_string(), + source_path: None, + modified: false, + zoom: 1.0, + pan_offset: Vector::ZERO, + cached_layers, + cached_history, + history_current, + selection_rect: None, + vector_draw: None, + }; + + let mut app = Self { + documents: vec![doc], + active_doc: 0, + tool_state: ToolState::default(), + fg_color: [220, 50, 50, 255], + bg_color: [255, 255, 255, 255], + last_cursor_pos: None, + canvas_cursor_pos: None, + active_dialog: ActiveDialog::None, + dialog_new_name: "Untitled".to_string(), + dialog_new_width: 800, + dialog_new_height: 600, + dialog_new_transparent: true, + dialog_brightness: 0.0, + dialog_contrast: 0.0, + dialog_hue: 0.0, + dialog_saturation: 0.0, + dialog_lightness: 0.0, + dialog_selection_value: 5.0, + selected_filter: None, + filter_params: serde_json::json!({}), + filter_preview_active: false, + dock: DockState::new(), + tablet_state: Arc::new(Mutex::new(TabletState::new())), + initialized: false, + theme_state: ThemeState::new(), + active_menu: None, + toolbox_two_column: false, + }; + + // If a file path was provided, open it + if let Some(path) = load_path { + let path_str = path.to_string_lossy().to_string(); + match app.documents[0].engine.open_image(&path_str) { + Ok(()) => { + app.documents[0].name = path.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "Untitled".to_string()); + app.documents[0].source_path = Some(path); + app.documents[0].composite_dirty = true; + app.refresh_composite_if_needed(); + } + Err(e) => { + log::error!("Failed to open file on startup: {}", e); + } + } + } + + app.initialized = true; + app + } + + /// Return a mutable reference to the active document. + fn active_document_mut(&mut self) -> &mut IcedDocument { + self.documents.get_mut(self.active_doc).expect("no active document") + } + + /// Convert widget-relative coordinates to canvas-space coordinates. + fn screen_to_canvas(&self, sx: f32, sy: f32) -> Option<(f32, f32)> { + let doc = &self.documents[self.active_doc]; + let zoom = doc.zoom; + let pan = doc.pan_offset; + + let canvas_x = (sx - pan.x) / zoom; + let canvas_y = (sy - pan.y) / zoom; + + if canvas_x >= 0.0 && canvas_x < doc.engine.canvas_width() as f32 + && canvas_y >= 0.0 && canvas_y < doc.engine.canvas_height() as f32 + { + Some((canvas_x, canvas_y)) + } else { + None + } + } + + /// Refresh the composite buffer and cached panel data from the engine if dirty. + fn refresh_composite_if_needed(&mut self) { + let doc = &mut self.documents[self.active_doc]; + if doc.composite_dirty { + doc.composite_buffer = doc.engine.get_composite_pixels(); + doc.composite_dirty = false; + } + // Always refresh cached panel data (cheap operation) + doc.cached_layers = doc.engine.layer_infos(); + let history_len = doc.engine.history_len(); + doc.cached_history = (0..history_len) + .filter_map(|i| doc.engine.history_description(i).map(|d| (i, d))) + .collect(); + doc.history_current = doc.engine.history_current(); + } + + /// Apply brush parameters from tool state to the engine. + /// + /// Called before starting a stroke to ensure the engine uses + /// the current brush size, opacity, hardness, and style. + fn apply_brush_params(&mut self) { + let tip = build_brush_tip(&self.tool_state); + self.documents[self.active_doc].engine.set_brush_tip(tip); + } + + // ── Update ────────────────────────────────────────── + + /// Handle a message and return an optional command. + pub fn update(&mut self, message: Message) -> Task { + match message { + Message::ToolSelected(tool) => { + log::info!("Tool selected: {:?}", tool); + self.tool_state.active_tool = tool; + } + + Message::FgColorChanged(color) => { + self.fg_color = color; + self.active_document_mut().engine.set_color(color); + } + + Message::BgColorChanged(color) => { + self.bg_color = color; + } + + Message::SwapColors => { + std::mem::swap(&mut self.fg_color, &mut self.bg_color); + let color = self.fg_color; + self.active_document_mut().engine.set_color(color); + } + + Message::CanvasPointerPressed { x, y } => { + let (px, py) = if x == 0.0 && y == 0.0 { + self.last_cursor_pos.unwrap_or((0.0, 0.0)) + } else { + (x, y) + }; + self.last_cursor_pos = Some((px, py)); + + if let Some((canvas_x, canvas_y)) = self.screen_to_canvas(px, py) { + self.canvas_cursor_pos = Some((canvas_x as u32, canvas_y as u32)); + let tool = self.tool_state.active_tool; + + match tool { + Tool::Pen | Tool::Brush | Tool::Eraser | Tool::Spray => { + // Apply brush parameters before starting stroke + self.apply_brush_params(); + + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + self.documents[self.active_doc].engine.set_tool(tool); + self.documents[self.active_doc].engine.set_eraser(tool == Tool::Eraser); + self.documents[self.active_doc].engine.begin_stroke(layer_id, canvas_x, canvas_y); + self.documents[self.active_doc].engine.stroke_to(layer_id, canvas_x, canvas_y, 1.0); + self.tool_state.is_drawing = true; + self.tool_state.last_stroke_pos = Some((canvas_x, canvas_y)); + self.tool_state.brush_accumulated_dist = 0.0; + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Tool::Eyedropper => { + let cx = canvas_x as u32; + let cy = canvas_y as u32; + let w = self.documents[self.active_doc].engine.canvas_width(); + let h = self.documents[self.active_doc].engine.canvas_height(); + if cx < w && cy < h { + let pixels = &self.documents[self.active_doc].composite_buffer; + let idx = ((cy * w + cx) * 4) as usize; + if idx + 3 < pixels.len() { + let color = [pixels[idx], pixels[idx + 1], pixels[idx + 2], pixels[idx + 3]]; + self.fg_color = color; + self.documents[self.active_doc].engine.set_color(color); + } + } + } + Tool::FloodFill => { + let cx = canvas_x as u32; + let cy = canvas_y as u32; + let color = self.fg_color; + self.documents[self.active_doc].engine.flood_fill(cx, cy, color, 32); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Tool::Select => { + // Start selection rectangle drag + let sx = canvas_x; + let sy = canvas_y; + return Task::perform(async move {}, move |_| { + Message::SelectionDragStart(sx, sy) + }); + } + Tool::VectorRect | Tool::VectorCircle | Tool::VectorLine => { + // Start vector shape drawing + let sx = canvas_x; + let sy = canvas_y; + return Task::perform(async move {}, move |_| { + Message::VectorDrawStart(sx, sy) + }); + } + _ => {} + } + } + } + + Message::CanvasPointerMoved { x, y } => { + self.last_cursor_pos = Some((x, y)); + + // Update canvas-space cursor for status bar + if let Some((cx, cy)) = self.screen_to_canvas(x, y) { + self.canvas_cursor_pos = Some((cx as u32, cy as u32)); + } + + if self.tool_state.is_drawing { + if let Some((canvas_x, canvas_y)) = self.screen_to_canvas(x, y) { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + + let spacing: f32 = match self.tool_state.active_tool { + Tool::Spray => 2.0_f32, + _ => 1.0_f32, + }.max(1.0); + + if let Some((last_x, last_y)) = self.tool_state.last_stroke_pos { + let dx = canvas_x - last_x; + let dy = canvas_y - last_y; + let dist = (dx * dx + dy * dy).sqrt(); + + // Use constant pressure for mouse input + // Real tablet pressure comes from tablet_state + let pressure = self.tablet_state.lock().map(|s| s.current_pressure()).unwrap_or(1.0); + + if dist >= spacing { + self.documents[self.active_doc].engine.stroke_to(layer_id, canvas_x, canvas_y, pressure); + self.tool_state.last_stroke_pos = Some((canvas_x, canvas_y)); + self.documents[self.active_doc].composite_dirty = true; + } + } else { + self.documents[self.active_doc].engine.stroke_to(layer_id, canvas_x, canvas_y, 1.0); + self.tool_state.last_stroke_pos = Some((canvas_x, canvas_y)); + self.documents[self.active_doc].composite_dirty = true; + } + + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + + // Handle selection drag + if self.tool_state.active_tool == Tool::Select { + if let Some((canvas_x, canvas_y)) = self.screen_to_canvas(x, y) { + let mx = canvas_x; + let my = canvas_y; + return Task::perform(async move {}, move |_| { + Message::SelectionDragMove(mx, my) + }); + } + } + + // Handle vector draw drag + if matches!(self.tool_state.active_tool, Tool::VectorRect | Tool::VectorCircle | Tool::VectorLine) { + if let Some((canvas_x, canvas_y)) = self.screen_to_canvas(x, y) { + let mx = canvas_x; + let my = canvas_y; + return Task::perform(async move {}, move |_| { + Message::VectorDrawMove(mx, my) + }); + } + } + } + + Message::CanvasPointerReleased => { + if self.tool_state.is_drawing { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + self.documents[self.active_doc].engine.end_stroke(layer_id); + self.documents[self.active_doc].engine.commit_pending_history(); + self.documents[self.active_doc].composite_dirty = true; + + self.tool_state.is_drawing = false; + self.tool_state.last_stroke_pos = None; + + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + // End selection drag + if self.tool_state.active_tool == Tool::Select { + return Task::perform(async {}, |_| Message::SelectionDragEnd); + } + + // End vector draw + if matches!(self.tool_state.active_tool, Tool::VectorRect | Tool::VectorCircle | Tool::VectorLine) { + return Task::perform(async {}, |_| Message::VectorDrawEnd); + } + } + + Message::CanvasZoom { delta } => { + let doc = &mut self.documents[self.active_doc]; + let factor = if delta > 0.0 { 1.1 } else { 1.0 / 1.1 }; + doc.zoom = (doc.zoom * factor).clamp(ZOOM_MIN, ZOOM_MAX); + } + + Message::CanvasZoomSet(z) => { + self.documents[self.active_doc].zoom = z.clamp(ZOOM_MIN, ZOOM_MAX); + } + + Message::CompositeRefresh | Message::Undo | Message::Redo => { + match message { + Message::Undo => { self.documents[self.active_doc].engine.undo(); } + Message::Redo => { self.documents[self.active_doc].engine.redo(); } + _ => {} + } + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + } + + // ── Layer operations ────────────────────────── + Message::LayerSelect(id) => { + self.documents[self.active_doc].engine.set_active_layer(id); + } + + Message::LayerAdd => { + let count = self.documents[self.active_doc].engine.get_layer_count(); + self.documents[self.active_doc].engine.add_layer(&format!("Layer {}", count + 1)); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + Message::LayerDelete(id) => { + self.documents[self.active_doc].engine.delete_layer(id); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + Message::LayerToggleVisibility(id) => { + let visible = self.documents[self.active_doc].engine.get_layer_info(id) + .map(|l| !l.visible) + .unwrap_or(true); + self.documents[self.active_doc].engine.set_layer_visible(id, visible); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + Message::LayerToggleLock(id) => { + let locked = self.documents[self.active_doc].engine.get_layer_info(id) + .map(|l| !l.locked) + .unwrap_or(false); + self.documents[self.active_doc].engine.set_layer_locked(id, locked); + } + + Message::LayerSetOpacity(id, opacity) => { + self.documents[self.active_doc].engine.set_layer_opacity(id, opacity); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + Message::LayerMergeDown => { + self.documents[self.active_doc].engine.merge_down(); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + Message::LayerFlatten => { + self.documents[self.active_doc].engine.flatten_image(); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + Message::LayerMoveUp(id) => { + let infos = self.documents[self.active_doc].engine.layer_infos(); + if let Some(pos) = infos.iter().position(|l| l.id == id) { + if pos > 0 { + let target_idx = pos - 1; + self.documents[self.active_doc].engine.move_layer(id, target_idx); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + } + + Message::LayerMoveDown(id) => { + let infos = self.documents[self.active_doc].engine.layer_infos(); + if let Some(pos) = infos.iter().position(|l| l.id == id) { + if pos + 1 < infos.len() { + let target_idx = pos + 1; + self.documents[self.active_doc].engine.move_layer(id, target_idx); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + } + + // ── History ─────────────────────────────────── + Message::HistoryJumpTo(idx) => { + self.documents[self.active_doc].engine.jump_to_history(idx); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + // ── Text tool ───────────────────────────────── + Message::TextSizeChanged(_size) => { + // TODO: apply to active text tool config + } + Message::TextColorChanged(_color) => { + // TODO: apply to active text tool config + } + Message::TextAlignChanged(_alignment) => { + // TODO: apply to active text tool config + } + Message::TextAccept => { + // TODO: commit text changes + } + Message::TextCancel => { + // TODO: cancel text changes + } + + // ── Layer styles ────────────────────────────── + Message::LayerStyleToggle(_index) => { + // TODO: toggle layer style enabled state + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::LayerStyleAdd => { + // TODO: open layer style add dialog + } + + // ── Brushes ─────────────────────────────────── + Message::BrushStyleSelected(style) => { + log::info!("Brush style selected: {:?}", style); + self.tool_state.brush_style = style; + } + Message::BrushSizeChanged(size) => { + self.tool_state.brush_size = size; + } + Message::BrushOpacityChanged(opacity) => { + self.tool_state.brush_opacity = opacity; + } + Message::BrushHardnessChanged(hardness) => { + self.tool_state.brush_hardness = hardness; + } + + // ── Filters ─────────────────────────────────── + Message::FilterSelect(filter) => { + self.selected_filter = Some(filter); + self.filter_preview_active = true; + self.filter_params = serde_json::json!({}); + self.documents[self.active_doc].engine.filter_preview_begin(); + } + Message::FilterParamChanged(key, value) => { + // Update the specific parameter in filter_params + if let serde_json::Value::Object(ref mut map) = self.filter_params { + map.insert(key, value); + } + // Apply preview with updated params + if let Some(filter) = self.selected_filter { + let filter_id = filter_type_to_id(filter); + self.documents[self.active_doc].engine.apply_filter_preview(filter_id, self.filter_params.clone()); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + Message::FilterApply => { + if let Some(filter) = self.selected_filter { + let filter_id = filter_type_to_id(filter); + self.documents[self.active_doc].engine.apply_filter(filter_id, self.filter_params.clone()); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + self.filter_preview_active = false; + self.selected_filter = None; + self.filter_params = serde_json::json!({}); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + Message::FilterReset => { + if self.filter_preview_active { + self.documents[self.active_doc].engine.filter_preview_restore(); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + self.filter_preview_active = false; + } + self.selected_filter = None; + self.filter_params = serde_json::json!({}); + } + + // ── Dialogs ─────────────────────────────────── + Message::DialogOpen(dialog) => { + self.active_dialog = dialog; + } + Message::DialogClose => { + self.active_dialog = ActiveDialog::None; + } + Message::DialogCloseSave => { + // TODO: save then close + self.active_dialog = ActiveDialog::None; + } + Message::DialogCloseDiscard => { + self.active_dialog = ActiveDialog::None; + } + + // ── New Image Dialog ────────────────────────── + Message::DialogNewImageName(name) => { + self.dialog_new_name = name; + } + Message::DialogNewImageWidth(w) => { + self.dialog_new_width = w; + } + Message::DialogNewImageHeight(h) => { + self.dialog_new_height = h; + } + Message::DialogNewImageTransparent(t) => { + self.dialog_new_transparent = t; + } + Message::DialogNewImagePreset(w, h) => { + self.dialog_new_width = w; + self.dialog_new_height = h; + } + Message::DialogNewImageCreate => { + let w = self.dialog_new_width; + let h = self.dialog_new_height; + let name = self.dialog_new_name.clone(); + let transparent = self.dialog_new_transparent; + let mut engine = Engine::new_with_options(&name, w, h, transparent); + engine.pre_tile_all_layers(); + let composite_buffer = engine.get_composite_pixels(); + let cached_layers = engine.layer_infos(); + let history_len = engine.history_len(); + let cached_history: Vec<(usize, String)> = (0..history_len) + .filter_map(|i| engine.history_description(i).map(|d| (i, d))) + .collect(); + let history_current = engine.history_current(); + self.documents.push(IcedDocument { + engine, + composite_buffer, + composite_dirty: false, + name, + source_path: None, + modified: false, + zoom: 1.0, + pan_offset: Vector::ZERO, + cached_layers, + cached_history, + history_current, + selection_rect: None, + vector_draw: None, + }); + self.active_doc = self.documents.len() - 1; + self.active_dialog = ActiveDialog::None; + } + + // ── Adjustments Dialog ──────────────────────── + Message::AdjBrightness(v) => { + self.dialog_brightness = v; + let params = serde_json::json!({"brightness": v, "contrast": self.dialog_contrast}); + self.documents[self.active_doc].engine.apply_filter_preview("brightness_contrast", params); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::AdjContrast(v) => { + self.dialog_contrast = v; + let params = serde_json::json!({"brightness": self.dialog_brightness, "contrast": v}); + self.documents[self.active_doc].engine.apply_filter_preview("brightness_contrast", params); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::AdjHue(v) => { + self.dialog_hue = v; + let params = serde_json::json!({"hue": v, "saturation": self.dialog_saturation, "lightness": self.dialog_lightness}); + self.documents[self.active_doc].engine.apply_filter_preview("hue_saturation", params); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::AdjSaturation(v) => { + self.dialog_saturation = v; + let params = serde_json::json!({"hue": self.dialog_hue, "saturation": v, "lightness": self.dialog_lightness}); + self.documents[self.active_doc].engine.apply_filter_preview("hue_saturation", params); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::AdjLightness(v) => { + self.dialog_lightness = v; + let params = serde_json::json!({"hue": self.dialog_hue, "saturation": self.dialog_saturation, "lightness": v}); + self.documents[self.active_doc].engine.apply_filter_preview("hue_saturation", params); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::AdjApply => { + // Apply the adjustment permanently (not just preview) + let params = match &self.active_dialog { + ActiveDialog::BrightnessContrast => { + serde_json::json!({"brightness": self.dialog_brightness, "contrast": self.dialog_contrast}) + } + ActiveDialog::HueSaturation => { + serde_json::json!({"hue": self.dialog_hue, "saturation": self.dialog_saturation, "lightness": self.dialog_lightness}) + } + _ => serde_json::json!({}), + }; + + // First restore the preview, then apply permanently + self.documents[self.active_doc].engine.filter_preview_restore(); + + let filter_id = match &self.active_dialog { + ActiveDialog::BrightnessContrast => "brightness_contrast", + ActiveDialog::HueSaturation => "hue_saturation", + _ => "", + }; + + if !filter_id.is_empty() { + self.documents[self.active_doc].engine.apply_filter(filter_id, params); + } + + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + self.active_dialog = ActiveDialog::None; + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + // ── Selection Dialog ────────────────────────── + Message::SelectionOpValue(v) => { + self.dialog_selection_value = v; + } + Message::SelectionOpApply => { + let v = self.dialog_selection_value as u32; + match &self.active_dialog { + ActiveDialog::SelectionOp(op) => { + match *op { + "Grow" => self.documents[self.active_doc].engine.selection_grow(v), + "Shrink" => self.documents[self.active_doc].engine.selection_shrink(v), + "Feather" => self.documents[self.active_doc].engine.selection_feather(v as f32), + _ => {} + } + } + _ => {} + } + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + self.active_dialog = ActiveDialog::None; + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + // ── Selection tools ─────────────────────────── + Message::SelectionDragStart(x, y) => { + self.documents[self.active_doc].selection_rect = Some((x, y, x, y)); + } + Message::SelectionDragMove(x, y) => { + if let Some((x0, y0, _, _)) = self.documents[self.active_doc].selection_rect { + self.documents[self.active_doc].selection_rect = Some((x0, y0, x, y)); + } + } + Message::SelectionDragEnd => { + if let Some((x0, y0, x1, y1)) = self.documents[self.active_doc].selection_rect.take() { + let sx = x0.min(x1) as u32; + let sy = y0.min(y1) as u32; + let ex = x0.max(x1) as u32; + let ey = y0.max(y1) as u32; + if ex > sx && ey > sy { + self.documents[self.active_doc].engine.create_selection_rect(sx, sy, ex, ey); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + } + + // ── Vector tools ────────────────────────────── + Message::VectorDrawStart(x, y) => { + self.documents[self.active_doc].vector_draw = Some(((x, y), (x, y))); + } + Message::VectorDrawMove(x, y) => { + if let Some((start, _)) = self.documents[self.active_doc].vector_draw { + self.documents[self.active_doc].vector_draw = Some((start, (x, y))); + } + } + Message::VectorDrawEnd => { + if let Some(((x0, y0), (x1, y1))) = self.documents[self.active_doc].vector_draw.take() { + let engine = &mut self.documents[self.active_doc].engine; + let color = self.fg_color; + + match self.tool_state.active_tool { + Tool::VectorRect => { + let shape = hcie_engine_api::VectorShape::Rect { + x1: x0.min(x1), + y1: y0.min(y1), + x2: x0.max(x1), + y2: y0.max(y1), + fill: true, + fill_color: color, + stroke: 2.0, + color, + radius: 0.0, + angle: 0.0, + opacity: 1.0, + hardness: 0.5, + }; + engine.add_vector_shape(shape); + } + Tool::VectorCircle => { + let shape = hcie_engine_api::VectorShape::Circle { + x1: x0.min(x1), + y1: y0.min(y1), + x2: x0.max(x1), + y2: y0.max(y1), + fill: true, + fill_color: color, + stroke: 2.0, + color, + angle: 0.0, + opacity: 1.0, + hardness: 0.5, + }; + engine.add_vector_shape(shape); + } + Tool::VectorLine => { + let shape = hcie_engine_api::VectorShape::Line { + x1: x0, + y1: y0, + x2: x1, + y2: y1, + stroke: 2.0, + color, + angle: 0.0, + cap_start: hcie_engine_api::LineCap::Round, + cap_end: hcie_engine_api::LineCap::Round, + opacity: 1.0, + hardness: 0.5, + }; + engine.add_vector_shape(shape); + } + _ => {} + } + + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + + // ── AI Chat ─────────────────────────────────── + Message::AiChatInput(_text) => { + // Handled by the chat panel directly + } + Message::AiChatSend => { + // Placeholder: would send to LLM + log::info!("AI chat send not yet implemented"); + } + Message::AiChatClear => { + // Handled by the chat panel directly + } + + // ── Clipboard ───────────────────────────────── + Message::CopyImage => { + let doc = &self.documents[self.active_doc]; + let w = doc.engine.canvas_width(); + let h = doc.engine.canvas_height(); + let pixels = doc.composite_buffer.clone(); + return Task::perform( + async move { crate::io::clipboard::copy_image_to_clipboard(&pixels, w, h) }, + Message::ImageCopied, + ); + } + Message::PasteImage => { + return Task::perform( + async { crate::io::clipboard::paste_image_from_clipboard() }, + Message::ImagePasted, + ); + } + Message::CopyText(text) => { + let text = text.clone(); + return Task::perform( + async move { crate::io::clipboard::copy_text_to_clipboard(&text) }, + |_| Message::NoOp, + ); + } + Message::PasteText => { + return Task::perform( + async { crate::io::clipboard::paste_text_from_clipboard() }, + Message::ClipboardResult, + ); + } + Message::ClipboardResult(Ok(Some(text))) => { + log::info!("Pasted text: {}", &text[..text.len().min(50)]); + } + Message::ClipboardResult(Ok(None)) => { + log::info!("Clipboard is empty"); + } + Message::ClipboardResult(Err(e)) => { + log::error!("Clipboard error: {}", e); + } + Message::ImageCopied(Ok(())) => { + log::info!("Image copied to clipboard"); + } + Message::ImageCopied(Err(e)) => { + log::error!("Failed to copy image: {}", e); + } + Message::ImagePasted(Ok(Some((pixels, w, h)))) => { + log::info!("Pasted image: {}x{}", w, h); + let engine = &mut self.documents[self.active_doc].engine; + engine.create_layer_from_data("Pasted", pixels, w, h); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::ImagePasted(Ok(None)) => { + log::info!("No image in clipboard"); + } + Message::ImagePasted(Err(e)) => { + log::error!("Failed to paste image: {}", e); + } + + // ── File I/O (with rfd) ─────────────────────── + Message::OpenFileRfd => { + return Task::perform( + async { + rfd::AsyncFileDialog::new() + .set_title("Open Image") + .add_filter("Images", &["png", "jpg", "jpeg", "webp", "psd", "kra", "hcie"]) + .pick_file() + .await + .map(|handle| handle.path().to_path_buf()) + }, + Message::FileDialogClosed, + ); + } + Message::SaveFileAs => { + return Task::perform( + async { + rfd::AsyncFileDialog::new() + .set_title("Save As") + .add_filter("PNG", &["png"]) + .add_filter("JPEG", &["jpg", "jpeg"]) + .add_filter("WebP", &["webp"]) + .save_file() + .await + .map(|handle| handle.path().to_path_buf()) + }, + Message::FileDialogClosed, + ); + } + Message::FileDialogClosed(Some(path)) => { + let path_str = path.to_string_lossy().to_string(); + let ext = path.extension() + .and_then(|e| e.to_str()) + .unwrap_or("png") + .to_lowercase(); + + if self.active_dialog == ActiveDialog::None { + // Open file + let engine = &mut self.documents[self.active_doc].engine; + match engine.open_image(&path_str) { + Ok(()) => { + self.documents[self.active_doc].name = path.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "Untitled".to_string()); + self.documents[self.active_doc].source_path = Some(path); + self.documents[self.active_doc].composite_dirty = true; + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Err(e) => log::error!("Failed to open: {}", e), + } + } else { + // Save file + let engine = &self.documents[self.active_doc].engine; + match engine.save_as(&path_str, &ext) { + Ok(()) => log::info!("Saved to {}", path_str), + Err(e) => log::error!("Failed to save: {}", e), + } + self.active_dialog = ActiveDialog::None; + } + } + Message::FileDialogClosed(None) => { + // User cancelled + } + + // ── Dock Layout ─────────────────────────────── + Message::PaneClicked(pane) => { + self.dock.focus(pane); + } + Message::PaneDragged(drag_event) => { + if let iced::widget::pane_grid::DragEvent::Dropped { pane, target } = drag_event { + if let iced::widget::pane_grid::Target::Pane(target_pane, _) = target { + self.dock.pane_grid.swap(pane, target_pane); + } + } + } + Message::PaneResized(resize_event) => { + self.dock.pane_grid.resize(resize_event.split, resize_event.ratio); + } + Message::PaneClose(pane) => { + // Don't close canvas panes + if let Some(PaneType::Canvas) = self.dock.pane_type(pane) { + return Task::none(); + } + self.dock.close_pane(pane); + } + Message::PaneMaximize(pane) => { + self.dock.toggle_maximize(pane); + } + + // ── File I/O ───────────────────────────────── + Message::NewDocument(w, h) => { + let mut engine = Engine::new(w, h); + engine.pre_tile_all_layers(); + let composite_buffer = engine.get_composite_pixels(); + let cached_layers = engine.layer_infos(); + let history_len = engine.history_len(); + let cached_history: Vec<(usize, String)> = (0..history_len) + .filter_map(|i| engine.history_description(i).map(|d| (i, d))) + .collect(); + let history_current = engine.history_current(); + self.documents.push(IcedDocument { + engine, + composite_buffer, + composite_dirty: false, + name: "Untitled".to_string(), + source_path: None, + modified: false, + zoom: 1.0, + pan_offset: Vector::ZERO, + cached_layers, + cached_history, + history_current, + selection_rect: None, + vector_draw: None, + }); + self.active_doc = self.documents.len() - 1; + } + + Message::DocSwitch(idx) => { + if idx < self.documents.len() { + self.active_doc = idx; + } + } + + Message::OpenFile => { + return Task::perform( + async { + rfd::AsyncFileDialog::new() + .set_title("Open Image") + .add_filter("Images", &["png", "jpg", "jpeg", "webp", "psd", "kra", "hcie"]) + .pick_file() + .await + .map(|handle| handle.path().to_path_buf()) + }, + Message::FileDialogClosed, + ); + } + + Message::FileOpened(Ok(path)) => { + log::info!("File opened: {:?}", path); + } + + Message::FileOpened(Err(e)) => { + log::error!("Failed to open file: {}", e); + } + + Message::SaveFile => { + return Task::perform( + async { + rfd::AsyncFileDialog::new() + .set_title("Save As") + .add_filter("PNG", &["png"]) + .add_filter("JPEG", &["jpg", "jpeg"]) + .add_filter("WebP", &["webp"]) + .save_file() + .await + .map(|handle| handle.path().to_path_buf()) + }, + Message::FileDialogClosed, + ); + } + + // ── Menu ───────────────────────────────────── + Message::MenuOpen(idx) => { + if self.active_menu == Some(idx) { + self.active_menu = None; + } else { + self.active_menu = Some(idx); + } + } + Message::MenuClose => { + if self.active_menu.is_some() { + self.active_menu = None; + } else if self.active_dialog != ActiveDialog::None { + self.active_dialog = ActiveDialog::None; + } + } + Message::MenuAction(menu_idx, item_idx) => { + self.active_menu = None; + // Dispatch the appropriate action based on menu and item indices + match (menu_idx, item_idx) { + // File menu + (0, 0) => return Task::perform(async {}, |_| Message::DialogOpen(ActiveDialog::NewImage)), + (0, 1) => return Task::perform(async {}, |_| Message::OpenFileRfd), + (0, 2) => return Task::perform(async {}, |_| Message::SaveFileAs), + (0, 3) => return Task::perform(async {}, |_| Message::SaveFileAs), + // Edit menu + (1, 0) => return Task::perform(async {}, |_| Message::Undo), + (1, 1) => return Task::perform(async {}, |_| Message::Redo), + (1, 4) => return Task::perform(async {}, |_| Message::PasteImage), + // Tools menu + (2, idx) => { + let tools = [ + Tool::Move, Tool::VectorSelect, + Tool::Select, Tool::Lasso, Tool::PolygonSelect, Tool::MagicWand, + Tool::Crop, Tool::Eyedropper, + Tool::Brush, Tool::Pen, Tool::Spray, Tool::Eraser, + Tool::FloodFill, Tool::Gradient, Tool::Text, + Tool::VectorLine, Tool::VectorRect, Tool::VectorCircle, + ]; + if idx < tools.len() { + self.tool_state.active_tool = tools[idx]; + } + } + // Layer menu + (4, 0) => return Task::perform(async {}, |_| Message::LayerAdd), + (4, 3) => return Task::perform(async {}, |_| Message::LayerMergeDown), + (4, 4) => return Task::perform(async {}, |_| Message::LayerFlatten), + // Filter menu + (5, 1) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::BoxBlur)), + (5, 2) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::GaussianBlur)), + (5, 3) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::MotionBlur)), + (5, 5) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::UnsharpMask)), + (5, 7) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Mosaic)), + (5, 8) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Crystallize)), + (5, 10) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Pinch)), + (5, 11) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Twirl)), + (5, 13) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::OilPaint)), + // View menu + (7, 0) => return Task::perform(async {}, |_| Message::CanvasZoom { delta: 1.0 }), + (7, 1) => return Task::perform(async {}, |_| Message::CanvasZoom { delta: -1.0 }), + (7, 2) => return Task::perform(async {}, |_| Message::CanvasZoomSet(1.0)), + (7, 4) => return Task::perform(async {}, |_| Message::CanvasZoomSet(1.0)), + // Escape closes menu + _ => {} + } + } + + // ── Toolbox ────────────────────────────────── + Message::ToggleToolbox => { + self.toolbox_two_column = !self.toolbox_two_column; + } + + Message::NoOp => {} + } + + Task::none() + } + + // ── View ──────────────────────────────────────────── + + /// Build the UI element tree. + /// + /// Uses the dock system for the main layout. The title bar, menu bar, + /// dock grid, and status bar surround the content. Dialogs overlay everything. + pub fn view(&self) -> Element<'_, Message> { + let doc = &self.documents[self.active_doc]; + let colors = self.theme_state.colors(); + + // Title bar + let title_bar = panels::title_bar::view( + &doc.name, + doc.zoom, + doc.engine.canvas_width(), + doc.engine.canvas_height(), + colors, + ); + + // Menu bar + let menu_bar = panels::menus::view(&self.tool_state.active_tool, self.active_menu); + + // Dock grid (replaces hardcoded layout) + let dock_content = crate::dock::view::dock_view(&self.dock, self); + + // Status bar + let status_bar = panels::status_bar::view( + &self.tool_state.active_tool, + self.canvas_cursor_pos, + doc.zoom, + doc.engine.canvas_width(), + doc.engine.canvas_height(), + colors, + ); + + let content = column![ + title_bar, + menu_bar, + iced::widget::horizontal_rule(1), + dock_content, + iced::widget::horizontal_rule(1), + status_bar, + ] + .width(Length::Fill) + .height(Length::Fill); + + // Dialog overlay + let dialog_overlay: Element<'_, Message> = match &self.active_dialog { + ActiveDialog::None => text("").into(), + ActiveDialog::NewImage => dialogs::new_image::view( + &self.dialog_new_name, + self.dialog_new_width, + self.dialog_new_height, + self.dialog_new_transparent, + ), + ActiveDialog::BrightnessContrast => dialogs::adjustments::brightness_contrast_view( + self.dialog_brightness, + self.dialog_contrast, + ), + ActiveDialog::HueSaturation => dialogs::adjustments::hsl_view( + self.dialog_hue, + self.dialog_saturation, + self.dialog_lightness, + ), + ActiveDialog::CloseConfirm => dialogs::confirm::close_confirm_view(&doc.name), + ActiveDialog::SelectionOp(op) => dialogs::confirm::selection_op_view( + op, + self.dialog_selection_value, + 1.0, + 100.0, + ), + }; + + // Stack main content with dialog overlay + let stacked = container(content) + .width(Length::Fill) + .height(Length::Fill) + .padding(0); + + if self.active_dialog != ActiveDialog::None { + iced::widget::Stack::new() + .push(stacked) + .push(dialog_overlay) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + stacked.into() + } + } + + // ── Theme & Subscription ──────────────────────────── + + /// Return the application theme. + pub fn theme(&self) -> Theme { + Theme::Dark + } + + /// Subscriptions — keyboard events for shortcuts. + /// + /// Handles common keyboard shortcuts: + /// - Ctrl+Z: Undo + /// - Ctrl+Y / Ctrl+Shift+Z: Redo + /// - Ctrl+S: Save + /// - Ctrl+N: New + /// - Ctrl+O: Open + /// - Ctrl+C: Copy + /// - Ctrl+V: Paste + /// - Escape: Close dialog + pub fn subscription(&self) -> iced::Subscription { + iced::keyboard::on_key_press(|key, modifiers| { + let ctrl = modifiers.control() || modifiers.logo(); + let shift = modifiers.shift(); + + match key { + // Ctrl+Z = Undo + iced::keyboard::Key::Character(ref c) if c.as_str() == "z" && ctrl && !shift => { + Some(Message::Undo) + } + // Ctrl+Y or Ctrl+Shift+Z = Redo + iced::keyboard::Key::Character(ref c) if c.as_str() == "y" && ctrl => { + Some(Message::Redo) + } + iced::keyboard::Key::Character(ref c) if c.as_str() == "z" && ctrl && shift => { + Some(Message::Redo) + } + // Ctrl+S = Save + iced::keyboard::Key::Character(ref c) if c.as_str() == "s" && ctrl && !shift => { + Some(Message::SaveFileAs) + } + // Ctrl+N = New + iced::keyboard::Key::Character(ref c) if c.as_str() == "n" && ctrl => { + Some(Message::DialogOpen(ActiveDialog::NewImage)) + } + // Ctrl+O = Open + iced::keyboard::Key::Character(ref c) if c.as_str() == "o" && ctrl => { + Some(Message::OpenFileRfd) + } + // Ctrl+C = Copy + iced::keyboard::Key::Character(ref c) if c.as_str() == "c" && ctrl && !shift => { + Some(Message::CopyImage) + } + // Ctrl+V = Paste + iced::keyboard::Key::Character(ref c) if c.as_str() == "v" && ctrl && !shift => { + Some(Message::PasteImage) + } + // Ctrl+X = Cut + iced::keyboard::Key::Character(ref c) if c.as_str() == "x" && ctrl && !shift => { + Some(Message::CopyImage) // TODO: cut + } + // Escape = close menu, cancel dialog, or deselect + iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape) => { + Some(Message::MenuClose) + } + _ => None, + } + }) + } +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs new file mode 100644 index 0000000..fbdc96d --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs @@ -0,0 +1,138 @@ +//! Canvas viewport — renders the composite texture with zoom/pan. +//! +//! Displays the engine's RGBA composite as an Iced image widget. +//! Mouse interaction is handled via `mouse_area` wrapper. +//! Includes checkerboard pattern for transparency visualization. + +pub mod render; + +use crate::app::Message; +use iced::widget::{column, container, image, mouse_area, row, text}; +use iced::{Element, Length}; + +/// Create a checkerboard pattern image for transparency visualization. +/// +/// Returns a checkerboard texture as RGBA bytes with the given dimensions. +/// The checker uses two shades of gray in an 8x8 pixel grid pattern. +fn checkerboard_rgba(w: u32, h: u32) -> Vec { + let mut pixels = vec![0u8; (w * h * 4) as usize]; + let size = 8u32; + for y in 0..h { + for x in 0..w { + let idx = ((y * w + x) * 4) as usize; + let is_light = ((x / size) + (y / size)) % 2 == 0; + let v: u8 = if is_light { 180 } else { 140 }; + pixels[idx] = v; + pixels[idx + 1] = v; + pixels[idx + 2] = v; + pixels[idx + 3] = 255; + } + } + pixels +} + +/// Build the canvas viewport element. +/// +/// The composite texture is displayed over a checkerboard background +/// that indicates transparency. Mouse interaction is handled via +/// `mouse_area` wrapper for press, move, scroll, and release events. +pub fn view<'a>( + doc: &'a crate::app::IcedDocument, + tool_state: &'a crate::app::ToolState, +) -> Element<'a, Message> { + let engine_w = doc.engine.canvas_width(); + let engine_h = doc.engine.canvas_height(); + let zoom = doc.zoom; + + let display_w = engine_w as f32 * zoom; + let display_h = engine_h as f32 * zoom; + + // Checkerboard background for transparency + let checker = checkerboard_rgba(engine_w, engine_h); + let checker_handle = image::Handle::from_rgba(engine_w, engine_h, checker); + let checker_img = image(checker_handle) + .width(display_w) + .height(display_h); + + // Composite texture on top + let pixels = doc.composite_buffer.clone(); + let handle = image::Handle::from_rgba(engine_w, engine_h, pixels); + let composite_img = image(handle) + .width(display_w) + .height(display_h); + + // Stack checkerboard behind composite + let canvas_stack = iced::widget::Stack::new() + .push(checker_img) + .push(composite_img); + + // Canvas container + let canvas_inner = container(canvas_stack) + .center_y(display_h) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.25, 0.25, 0.25))), + border: iced::Border::default().color(iced::Color::from_rgb(0.35, 0.35, 0.35)).width(1), + ..Default::default() + }); + + // Wrap with mouse_area for per-widget mouse events + let interactive_canvas = mouse_area(canvas_inner) + .on_press(Message::CanvasPointerPressed { x: 0.0, y: 0.0 }) + .on_move(|point| Message::CanvasPointerMoved { x: point.x, y: point.y }) + .on_release(Message::CanvasPointerReleased) + .on_scroll(|delta| { + match delta { + iced::mouse::ScrollDelta::Lines { y, .. } => Message::CanvasZoom { delta: y }, + iced::mouse::ScrollDelta::Pixels { y, .. } => Message::CanvasZoom { delta: y }, + } + }) + .interaction(iced::mouse::Interaction::Crosshair); + + // Selection overlay info + let overlay_info = if let Some((x0, y0, x1, y1)) = doc.selection_rect { + let w = (x1 - x0).abs() as u32; + let h = (y1 - y0).abs() as u32; + Some(format!("Selection: {}×{}", w, h)) + } else { + None + }; + + let vector_info = if let Some(((x0, y0), (x1, y1))) = doc.vector_draw { + let w = (x1 - x0).abs() as u32; + let h = (y1 - y0).abs() as u32; + Some(format!("Shape: {}×{}", w, h)) + } else { + None + }; + + let mut overlay_lines = vec![]; + if let Some(sel) = overlay_info { overlay_lines.push(sel); } + if let Some(vec) = vector_info { overlay_lines.push(vec); } + + let overlay_text = if overlay_lines.is_empty() { + text("").size(10) + } else { + text(overlay_lines.join(" | ")).size(10) + }; + + let tool_info = container( + row![ + text(format!("{:?}", tool_state.active_tool)).size(11), + iced::widget::horizontal_rule(1), + overlay_text, + ] + .spacing(4) + .align_y(iced::Alignment::Center) + ) + .padding([2, 8]); + + column![ + container(interactive_canvas) + .width(Length::Fill) + .height(Length::Fill), + tool_info, + ] + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/render.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/render.rs new file mode 100644 index 0000000..c56a227 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/render.rs @@ -0,0 +1,4 @@ +//! Composite texture rendering — dirty-region compositing and texture upload. +//! +//! Handles getting RGBA pixels from the engine and converting them +//! to Iced image handles for display. diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/color_picker.rs b/hcie-iced-app/crates/hcie-iced-gui/src/color_picker.rs new file mode 100644 index 0000000..77ce8c9 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/color_picker.rs @@ -0,0 +1,119 @@ +//! Basic color picker — RGB sliders and palette grid. +//! +//! Provides a color selection interface with RGB sliders +//! and a predefined palette grid. Palette swatches are clickable. + +use crate::app::Message; +use iced::widget::{column, container, horizontal_rule, row, slider, text}; +use iced::{Element, Length, Padding}; + +/// A predefined color palette. +const PALETTE: &[[u8; 3]] = &[ + [0, 0, 0], // Black + [255, 255, 255], // White + [255, 0, 0], // Red + [0, 255, 0], // Green + [0, 0, 255], // Blue + [255, 255, 0], // Yellow + [255, 0, 255], // Magenta + [0, 255, 255], // Cyan + [128, 0, 0], // Dark Red + [128, 128, 0], // Olive + [0, 128, 0], // Dark Green + [0, 128, 128], // Teal + [0, 0, 128], // Navy + [128, 0, 128], // Purple + [192, 192, 192], // Silver + [128, 128, 128], // Gray + [255, 128, 128], // Light Red + [255, 192, 128], // Orange + [255, 255, 128], // Light Yellow + [128, 255, 128], // Light Green + [128, 255, 255], // Light Cyan + [128, 128, 255], // Light Blue + [192, 128, 255], // Light Purple + [255, 128, 192], // Pink +]; + +/// Build the color picker element. +pub fn view<'a>(fg_color: &'a [u8; 4], _bg_color: &'a [u8; 4]) -> Element<'a, Message> { + let r = fg_color[0]; + let g = fg_color[1]; + let b = fg_color[2]; + + let r_slider = slider(0..=255, r, move |v| Message::FgColorChanged([v, g, b, 255])); + let g_slider = slider(0..=255, g, move |v| Message::FgColorChanged([r, v, b, 255])); + let b_slider = slider(0..=255, b, move |v| Message::FgColorChanged([r, g, v, 255])); + + let sliders = column![ + row![text("R").size(10), r_slider].spacing(4), + row![text("G").size(10), g_slider].spacing(4), + row![text("B").size(10), b_slider].spacing(4), + ] + .spacing(2) + .padding(Padding::from([4, 4])); + + // Color swatch preview + let fg = fg_color; + let preview = container(text("")) + .width(Length::Fill) + .height(32) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + fg[0] as f32 / 255.0, + fg[1] as f32 / 255.0, + fg[2] as f32 / 255.0, + fg[3] as f32 / 255.0, + ))), + border: iced::Border::default().rounded(4).color(iced::Color::from_rgb(0.4, 0.4, 0.4)).width(1), + ..Default::default() + }); + + // Palette grid — each swatch is clickable + let mut palette_grid = column![].spacing(1); + for row_colors in PALETTE.chunks(8) { + let mut row_widgets = row![].spacing(1); + for &color in row_colors { + let c = color; + let swatch = container(text("")) + .width(16) + .height(16) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb( + c[0] as f32 / 255.0, + c[1] as f32 / 255.0, + c[2] as f32 / 255.0, + ))), + border: iced::Border::default().rounded(2), + ..Default::default() + }); + + // Make swatch clickable + let clickable = iced::widget::mouse_area(swatch) + .on_press(Message::FgColorChanged([c[0], c[1], c[2], 255])) + .interaction(iced::mouse::Interaction::Pointer); + + row_widgets = row_widgets.push(clickable); + } + palette_grid = palette_grid.push(row_widgets); + } + + let palette = column![ + text("Palette").size(10), + palette_grid, + ] + .spacing(4) + .padding(Padding::from([4, 4])); + + column![ + text("Color").size(12).font(iced::Font::MONOSPACE), + preview, + sliders, + horizontal_rule(1), + palette, + ] + .width(Length::Fill) + .spacing(4) + .padding(Padding::from([4, 4])) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/adjustments.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/adjustments.rs new file mode 100644 index 0000000..c771d03 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/adjustments.rs @@ -0,0 +1,106 @@ +//! Adjustments dialog — brightness/contrast and HSL adjustments. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, row, slider, text}; +use iced::{Element, Length}; + +/// Build the Brightness/Contrast adjustment dialog. +pub fn brightness_contrast_view( + brightness: f32, + contrast: f32, +) -> Element<'static, Message> { + let brightness_slider = slider(-100.0..=100.0, brightness, |v| Message::AdjBrightness(v)) + .step(1.0) + .width(Length::Fill); + + let contrast_slider = slider(-100.0..=100.0, contrast, |v| Message::AdjContrast(v)) + .step(1.0) + .width(Length::Fill); + + let apply_btn = button(text("Apply").size(11)) + .on_press(Message::AdjApply) + .padding([6, 12]); + + let cancel_btn = button(text("Cancel").size(11)) + .on_press(Message::DialogClose) + .padding([6, 12]); + + let dialog = column![ + text("Brightness / Contrast").size(14).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text(format!("Brightness: {:.0}", brightness)).size(11), + brightness_slider, + text(format!("Contrast: {:.0}", contrast)).size(11), + contrast_slider, + horizontal_rule(1), + row![apply_btn, cancel_btn].spacing(12), + ] + .spacing(8) + .padding(16) + .width(350); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +/// Build the Hue/Saturation/Lightness adjustment dialog. +pub fn hsl_view( + hue: f32, + saturation: f32, + lightness: f32, +) -> Element<'static, Message> { + let hue_slider = slider(-180.0..=180.0, hue, |v| Message::AdjHue(v)) + .step(1.0) + .width(Length::Fill); + + let sat_slider = slider(-100.0..=100.0, saturation, |v| Message::AdjSaturation(v)) + .step(1.0) + .width(Length::Fill); + + let light_slider = slider(-100.0..=100.0, lightness, |v| Message::AdjLightness(v)) + .step(1.0) + .width(Length::Fill); + + let apply_btn = button(text("Apply").size(11)) + .on_press(Message::AdjApply) + .padding([6, 12]); + + let cancel_btn = button(text("Cancel").size(11)) + .on_press(Message::DialogClose) + .padding([6, 12]); + + let dialog = column![ + text("Hue / Saturation / Lightness").size(14).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text(format!("Hue: {:.0}°", hue)).size(11), + hue_slider, + text(format!("Saturation: {:.0}", saturation)).size(11), + sat_slider, + text(format!("Lightness: {:.0}", lightness)).size(11), + light_slider, + horizontal_rule(1), + row![apply_btn, cancel_btn].spacing(12), + ] + .spacing(8) + .padding(16) + .width(350); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/confirm.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/confirm.rs new file mode 100644 index 0000000..f1baed2 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/confirm.rs @@ -0,0 +1,88 @@ +//! Confirm dialog — save/discard/cancel confirmation. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, row, slider, text}; +use iced::{Element, Length}; + +/// Build a confirmation dialog with save/discard/cancel. +pub fn close_confirm_view(doc_name: &str) -> Element<'static, Message> { + let msg = format!("\"{}\" has unsaved changes.", doc_name); + + let save_btn = button(text("Save").size(12)) + .on_press(Message::DialogCloseSave) + .padding([8, 16]); + + let discard_btn = button(text("Discard").size(12)) + .on_press(Message::DialogCloseDiscard) + .padding([8, 16]); + + let cancel_btn = button(text("Cancel").size(12)) + .on_press(Message::DialogClose) + .padding([8, 16]); + + let dialog = column![ + text("Unsaved Changes").size(16).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text(msg).size(12), + horizontal_rule(1), + row![save_btn, discard_btn, cancel_btn].spacing(12), + ] + .spacing(8) + .padding(16) + .width(400); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +/// Build a selection operation dialog (grow/shrink/feather). +pub fn selection_op_view( + op_name: &str, + value: f32, + min: f32, + max: f32, +) -> Element<'static, Message> { + let op_owned = op_name.to_string(); + let value_slider = slider(min..=max, value, |v| Message::SelectionOpValue(v)) + .step(1.0) + .width(Length::Fill); + + let apply_btn = button(text("Apply").size(11)) + .on_press(Message::SelectionOpApply) + .padding([6, 12]); + + let cancel_btn = button(text("Cancel").size(11)) + .on_press(Message::DialogClose) + .padding([6, 12]); + + let dialog = column![ + text(op_owned).size(14).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text(format!("Value: {:.0}", value)).size(11), + value_slider, + horizontal_rule(1), + row![apply_btn, cancel_btn].spacing(12), + ] + .spacing(8) + .padding(16) + .width(350); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs new file mode 100644 index 0000000..65bc9cd --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs @@ -0,0 +1,5 @@ +//! Dialog modules — modal dialogs for the Iced GUI. + +pub mod new_image; +pub mod adjustments; +pub mod confirm; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/new_image.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/new_image.rs new file mode 100644 index 0000000..8e9c02c --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/new_image.rs @@ -0,0 +1,102 @@ +//! New Image dialog — create a new document with preset sizes. + +use crate::app::Message; +use iced::widget::{button, checkbox, column, container, horizontal_rule, row, scrollable, slider, text, text_input}; +use iced::{Element, Length}; + +/// A canvas preset size. +struct CanvasPreset { + name: &'static str, + width: u32, + height: u32, +} + +const PRESETS: &[CanvasPreset] = &[ + CanvasPreset { name: "Custom", width: 0, height: 0 }, + CanvasPreset { name: "HD (1280×720)", width: 1280, height: 720 }, + CanvasPreset { name: "Full HD (1920×1080)", width: 1920, height: 1080 }, + CanvasPreset { name: "4K (3840×2160)", width: 3840, height: 2160 }, + CanvasPreset { name: "Square (1024×1024)", width: 1024, height: 1024 }, + CanvasPreset { name: "A4 (2480×3508)", width: 2480, height: 3508 }, + CanvasPreset { name: "800×600", width: 800, height: 600 }, + CanvasPreset { name: "640×480", width: 640, height: 480 }, +]; + +/// Build the New Image dialog. +pub fn view( + name: &str, + width: u32, + height: u32, + transparent: bool, +) -> Element<'static, Message> { + let name_owned = name.to_string(); + let name_input = text_input("Document name", &name_owned) + .on_input(|s| Message::DialogNewImageName(s)) + .width(Length::Fill); + + let width_text = text(format!("Width: {}", width)).size(11); + let height_text = text(format!("Height: {}", height)).size(11); + + let width_slider = slider(1_u32..=4096, width, |v| Message::DialogNewImageWidth(v)) + .step(1_u32) + .width(Length::Fill); + + let height_slider = slider(1_u32..=4096, height, |v| Message::DialogNewImageHeight(v)) + .step(1_u32) + .width(Length::Fill); + + let transparent_check = checkbox("Transparent background", transparent) + .on_toggle(|v| Message::DialogNewImageTransparent(v)); + + let mut presets_col = column![].spacing(2); + for preset in PRESETS { + let label = text(preset.name).size(11); + let w = preset.width; + let h = preset.height; + let preset_btn = button(label) + .on_press(Message::DialogNewImagePreset(w, h)) + .width(Length::Fill) + .padding([4, 8]); + presets_col = presets_col.push(preset_btn); + } + + let create_btn = button(text("Create").size(12)) + .on_press(Message::DialogNewImageCreate) + .padding([8, 16]); + + let cancel_btn = button(text("Cancel").size(12)) + .on_press(Message::DialogClose) + .padding([8, 16]); + + let dialog = column![ + text("New Image").size(16).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text("Name:").size(11), + name_input, + width_text, + width_slider, + height_text, + height_slider, + transparent_check, + horizontal_rule(1), + text("Presets:").size(11), + scrollable(presets_col).height(200), + horizontal_rule(1), + row![create_btn, cancel_btn].spacing(12), + ] + .spacing(6) + .padding(16) + .width(400); + + // Center the dialog on screen + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/mod.rs new file mode 100644 index 0000000..a5acc38 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/mod.rs @@ -0,0 +1,7 @@ +//! Dock layout — PaneGrid-based dockable panel system. +//! +//! Provides a resizable, draggable pane grid where each pane +//! can host a different panel (layers, history, brushes, etc.). + +pub mod state; +pub mod view; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs new file mode 100644 index 0000000..b93777f --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs @@ -0,0 +1,196 @@ +//! Dock state — manages pane grid layout and panel assignments. +//! +//! Provides a resizable, draggable pane grid where each pane +//! can host a different panel (layers, history, brushes, etc.). +//! The default layout mirrors the egui GUI arrangement with Canvas centered: +//! +//! ```text +//! +--------+------------+-----------+---------+ +//! | | Brushes | | Layers | +//! | Tools | & Tips | |---------| +//! | (36px) | | Canvas | History | +//! | |------------| |---------| +//! | | Filters | | Color | +//! +--------+-----+------+-----------+---------+ +//! ``` + +use iced::widget::pane_grid::{Configuration, Pane, State}; + +/// Identifies which panel content a pane displays. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaneType { + /// The main canvas viewport. + Canvas, + /// Layers panel. + Layers, + /// History panel. + History, + /// Brushes panel. + Brushes, + /// Filters panel. + Filters, + /// Color picker panel. + ColorPicker, + /// Properties panel. + Properties, + /// Script panel. + Script, + /// AI Chat panel. + AiChat, + /// Layer Styles panel. + LayerStyles, + /// Toolbox (vertical tool strip). + Tools, + /// Layer Details panel. + LayerDetails, + /// Geometry panel (vector shapes). + Geometry, +} + +impl PaneType { + /// Display name for the pane type. + pub fn label(self) -> &'static str { + match self { + PaneType::Canvas => "Canvas", + PaneType::Layers => "Layers", + PaneType::History => "History", + PaneType::Brushes => "Brushes & Tips", + PaneType::Filters => "Filters", + PaneType::ColorPicker => "Color Palette", + PaneType::Properties => "Properties", + PaneType::Script => "Script", + PaneType::AiChat => "AI Assistant", + PaneType::LayerStyles => "Layer Styles", + PaneType::Tools => "Tools", + PaneType::LayerDetails => "Layer Details", + PaneType::Geometry => "Geometry", + } + } +} + +/// The dock layout state, wrapping iced's PaneGrid state. +pub struct DockState { + /// The underlying iced PaneGrid state. + pub pane_grid: State, + /// The root pane (always the canvas). + pub root_pane: Pane, + /// Currently focused pane. + pub focused_pane: Option, +} + +impl DockState { + /// Create a default dock layout with Canvas centered. + /// + /// Uses Configuration::Split to build a proper 3-column layout: + /// - Left column: Tools strip + (Brushes / Filters) + /// - Center: Canvas (fills available space) + /// - Right column: (Layers / History) + ColorPicker + pub fn new() -> Self { + // Build the layout using Configuration for proper centering: + // + // Root = HorizontalSplit(0.20, left_column, HorizontalSplit(0.70, canvas, right_column)) + // + // Left column = VerticalSplit(0.5, Tools, VerticalSplit(0.5, Brushes, Filters)) + // Right column = VerticalSplit(0.6, VerticalSplit(0.6, Layers, History), ColorPicker) + + let left_column = Configuration::Split { + axis: iced::widget::pane_grid::Axis::Horizontal, + ratio: 0.25, + a: Box::new(Configuration::Pane(PaneType::Tools)), + b: Box::new(Configuration::Split { + axis: iced::widget::pane_grid::Axis::Horizontal, + ratio: 0.5, + a: Box::new(Configuration::Pane(PaneType::Brushes)), + b: Box::new(Configuration::Pane(PaneType::Filters)), + }), + }; + + let right_column = Configuration::Split { + axis: iced::widget::pane_grid::Axis::Horizontal, + ratio: 0.6, + a: Box::new(Configuration::Split { + axis: iced::widget::pane_grid::Axis::Horizontal, + ratio: 0.5, + a: Box::new(Configuration::Pane(PaneType::Layers)), + b: Box::new(Configuration::Pane(PaneType::History)), + }), + b: Box::new(Configuration::Pane(PaneType::ColorPicker)), + }; + + // Root: 3-column layout with Canvas in center + let root_config = Configuration::Split { + axis: iced::widget::pane_grid::Axis::Vertical, + ratio: 0.20, + a: Box::new(left_column), + b: Box::new(Configuration::Split { + axis: iced::widget::pane_grid::Axis::Vertical, + ratio: 0.70, + a: Box::new(Configuration::Pane(PaneType::Canvas)), + b: Box::new(right_column), + }), + }; + + let pane_grid = State::with_configuration(root_config); + + // Find the Canvas pane (it's the center pane we want as root) + let root_pane = pane_grid.iter() + .find(|(_, pane_type)| **pane_type == PaneType::Canvas) + .map(|(pane, _)| *pane) + .unwrap_or_else(|| pane_grid.iter().next().map(|(p, _)| *p).unwrap()); + + Self { + pane_grid, + root_pane, + focused_pane: Some(root_pane), + } + } + + /// Split a pane horizontally with a new panel type. + pub fn split_horizontal(&mut self, target: Pane, pane_type: PaneType) -> Option { + let (new_pane, _) = self.pane_grid.split( + iced::widget::pane_grid::Axis::Horizontal, + target, + pane_type, + )?; + Some(new_pane) + } + + /// Split a pane vertically with a new panel type. + pub fn split_vertical(&mut self, target: Pane, pane_type: PaneType) -> Option { + let (new_pane, _) = self.pane_grid.split( + iced::widget::pane_grid::Axis::Vertical, + target, + pane_type, + )?; + Some(new_pane) + } + + /// Close a pane, redistributing its content to neighbors. + pub fn close_pane(&mut self, pane: Pane) -> bool { + self.pane_grid.close(pane).is_some() + } + + /// Toggle maximize on a pane. + pub fn toggle_maximize(&mut self, pane: Pane) { + if self.pane_grid.maximized().is_some() { + self.pane_grid.restore(); + } else { + self.pane_grid.maximize(pane); + } + } + + /// Focus a pane. + pub fn focus(&mut self, pane: Pane) { + self.focused_pane = Some(pane); + } + + /// Get the pane type at a given pane ID. + pub fn pane_type(&self, pane: Pane) -> Option { + self.pane_grid.get(pane).copied() + } + + /// Get the currently focused pane type. + pub fn focused_type(&self) -> Option { + self.focused_pane.and_then(|p| self.pane_type(p)) + } +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs new file mode 100644 index 0000000..4768635 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs @@ -0,0 +1,204 @@ +//! Dock view — renders the PaneGrid with panel content. +//! +//! Each pane type maps to its corresponding panel view function. +//! The canvas pane renders the main document viewport with a document tab bar. +//! Utility panes render their respective panel content. +//! All panels receive ThemeColors for consistent styling. + +use crate::app::Message; +use crate::dock::state::{DockState, PaneType}; +use crate::panels::styles; +use crate::theme::ThemeColors; +use iced::widget::pane_grid::{Content, TitleBar}; +use iced::widget::{button, column, container, pane_grid, row, text}; +use iced::{Element, Length}; + +/// Build the dock view element. +/// +/// Renders a PaneGrid where each pane displays the appropriate panel content +/// based on its PaneType. Title bars show the pane name with close/maximize buttons. +/// The canvas pane includes a document tab bar for multi-document support. +pub fn dock_view<'a>( + dock: &'a DockState, + app: &'a crate::app::HcieIcedApp, +) -> Element<'a, Message> { + let colors = app.theme_state.colors(); + + pane_grid::PaneGrid::new(&dock.pane_grid, |pane, pane_type, _is_maximized| { + let title = make_title_bar(pane, *pane_type, colors); + + let body: Element<'a, Message> = match pane_type { + PaneType::Canvas => { + // Canvas pane includes document tab bar + let doc_tab_bar = document_tab_bar(app, colors); + let canvas = { + let doc = &app.documents[app.active_doc]; + crate::canvas::view(doc, &app.tool_state) + }; + column![doc_tab_bar, canvas] + .width(Length::Fill) + .height(Length::Fill) + .into() + } + PaneType::Tools => { + crate::sidebar::view(&app.tool_state, &app.fg_color, &app.bg_color, colors, app.toolbox_two_column) + } + PaneType::Layers => { + let doc = &app.documents[app.active_doc]; + crate::panels::layers::view(&doc.cached_layers, doc.engine.active_layer_id(), colors) + } + PaneType::History => { + let doc = &app.documents[app.active_doc]; + crate::panels::history::view(&doc.cached_history, doc.history_current, colors) + } + PaneType::Brushes => { + crate::panels::brushes::view( + app.tool_state.brush_size, + app.tool_state.brush_opacity, + app.tool_state.brush_hardness, + colors, + ) + } + PaneType::Filters => { + crate::panels::filters::view(app.selected_filter, colors) + } + PaneType::ColorPicker => { + crate::color_picker::view(&app.fg_color, &app.bg_color) + } + PaneType::Properties => { + crate::panels::properties::view( + &app.tool_state.active_tool, + app.tool_state.brush_size, + app.tool_state.brush_opacity, + app.tool_state.brush_hardness, + app.documents[app.active_doc].selection_rect, + app.documents[app.active_doc].vector_draw, + colors, + ) + } + PaneType::Script => { + crate::panels::script_panel::view(colors) + } + PaneType::AiChat => { + crate::panels::ai_chat_panel::view("", &[], colors) + } + PaneType::LayerStyles => { + crate::panels::layer_styles::view(&[], colors) + } + PaneType::LayerDetails => { + let doc = &app.documents[app.active_doc]; + crate::panels::layer_details::view(&doc.cached_layers, doc.engine.active_layer_id(), colors) + } + PaneType::Geometry => { + crate::panels::geometry::view(&[], colors) + } + }; + + Content::new(body).title_bar(title) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(4) + .on_click(Message::PaneClicked) + .on_drag(Message::PaneDragged) + .on_resize(4, Message::PaneResized) + .into() +} + +/// Build the document tab bar. +/// +/// Shows a horizontal tab for each open document. The active document tab +/// is highlighted. Each tab shows the document name and a close button. +/// Modified documents show an asterisk (*). +fn document_tab_bar<'a>(app: &'a crate::app::HcieIcedApp, colors: ThemeColors) -> Element<'a, Message> { + let mut tabs = row![].spacing(0); + + for (idx, doc) in app.documents.iter().enumerate() { + let is_active = idx == app.active_doc; + let modified_indicator = if doc.modified { " *" } else { "" }; + let label = format!("{}{}", doc.name, modified_indicator); + + let tab_text = text(label).size(11); + let tab_text = if is_active { + tab_text.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.accent), + }) + } else { + tab_text + }; + + let tab_style = if is_active { + styles::active_tab_background(colors) + } else { + styles::inactive_tab_background(colors) + }; + + let tab = container( + row![ + tab_text, + button(text("×").size(10)) + .on_press(Message::NoOp) + .padding([0, 4]), + ] + .spacing(4) + .align_y(iced::Alignment::Center) + ) + .padding([4, 8]) + .style(move |_theme| tab_style.clone()); + + let doc_idx = idx; + let clickable = iced::widget::mouse_area(tab) + .on_press(Message::DocSwitch(doc_idx)); + + tabs = tabs.push(clickable); + } + + // Add new document button + let new_btn = button(text("+").size(12)) + .on_press(Message::DialogOpen(crate::app::ActiveDialog::NewImage)) + .padding([4, 8]); + + tabs = tabs.push(new_btn); + + container(tabs) + .width(Length::Fill) + .style(move |_theme| styles::tabbar_background(colors)) + .into() +} + +/// Create a Photoshop-style title bar for a pane. +/// +/// Shows the pane type label with close and maximize buttons. +/// No horizontal rule, no blue tint - clean modern look. +fn make_title_bar<'a>(pane: pane_grid::Pane, pane_type: PaneType, colors: ThemeColors) -> TitleBar<'a, Message> { + let title = text(pane_type.label()) + .size(11) + .color(colors.text_secondary); + + // Subtle close/maximize buttons + let close_btn = button(text("×").size(11).color(colors.text_secondary)) + .on_press(Message::PaneClose(pane)) + .padding([2, 6]) + .style(move |_theme, _status| iced::widget::button::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), + text_color: colors.text_secondary, + border: iced::Border::default(), + ..Default::default() + }); + + let maximize_btn = button(text("□").size(11).color(colors.text_secondary)) + .on_press(Message::PaneMaximize(pane)) + .padding([2, 6]) + .style(move |_theme, _status| iced::widget::button::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), + text_color: colors.text_secondary, + border: iced::Border::default(), + ..Default::default() + }); + + TitleBar::new( + row![title, iced::widget::Space::with_width(Length::Fill), maximize_btn, close_btn] + .spacing(2) + .align_y(iced::Alignment::Center) + ) +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/io/clipboard.rs b/hcie-iced-app/crates/hcie-iced-gui/src/io/clipboard.rs new file mode 100644 index 0000000..e97a246 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/io/clipboard.rs @@ -0,0 +1,176 @@ +//! Clipboard operations — copy/paste images via arboard. +//! +//! Provides cross-platform clipboard support for RGBA image data. +//! Falls back to shell tools (wl-paste/xclip) when arboard fails. + +/// Copy RGBA pixel data to the system clipboard. +pub fn copy_image_to_clipboard(pixels: &[u8], width: u32, height: u32) -> Result<(), String> { + #[cfg(not(target_arch = "wasm32"))] + { + use arboard::Clipboard; + use std::borrow::Cow; + + let mut clipboard = Clipboard::new().map_err(|e| format!("Clipboard init: {}", e))?; + let img_data = arboard::ImageData { + width: width as usize, + height: height as usize, + bytes: Cow::Borrowed(pixels), + }; + clipboard + .set_image(img_data) + .map_err(|e| format!("Clipboard set: {}", e))?; + Ok(()) + } + #[cfg(target_arch = "wasm32")] + { + let _ = (pixels, width, height); + log::warn!("Clipboard copy not implemented for Wasm"); + Ok(()) + } +} + +/// Paste an RGBA image from the system clipboard. +/// Returns (pixels, width, height) if successful. +pub fn paste_image_from_clipboard() -> Result, u32, u32)>, String> { + #[cfg(not(target_arch = "wasm32"))] + { + // Try arboard first + if let Some(result) = try_arboard_image() { + return Ok(Some(result)); + } + + // Try shell clipboard tools + if let Some(result) = try_shell_clipboard_image() { + return Ok(Some(result)); + } + + Ok(None) + } + #[cfg(target_arch = "wasm32")] + { + log::warn!("Clipboard paste not implemented for Wasm"); + Ok(None) + } +} + +/// Copy text to the system clipboard. +pub fn copy_text_to_clipboard(text: &str) -> Result<(), String> { + #[cfg(not(target_arch = "wasm32"))] + { + use arboard::Clipboard; + let mut clipboard = Clipboard::new().map_err(|e| format!("Clipboard init: {}", e))?; + clipboard + .set_text(text.to_string()) + .map_err(|e| format!("Clipboard set text: {}", e))?; + Ok(()) + } + #[cfg(target_arch = "wasm32")] + { + let _ = text; + Ok(()) + } +} + +/// Paste text from the system clipboard. +pub fn paste_text_from_clipboard() -> Result, String> { + #[cfg(not(target_arch = "wasm32"))] + { + use arboard::Clipboard; + let mut clipboard = Clipboard::new().map_err(|e| format!("Clipboard init: {}", e))?; + match clipboard.get_text() { + Ok(text) => Ok(Some(text.to_string())), + Err(_) => Ok(None), + } + } + #[cfg(target_arch = "wasm32")] + { + Ok(None) + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn try_arboard_image() -> Option<(Vec, u32, u32)> { + use arboard::Clipboard; + + let mut clipboard = match Clipboard::new() { + Ok(c) => c, + Err(e) => { + log::info!("[Clipboard] arboard init failed: {}", e); + return None; + } + }; + match clipboard.get_image() { + Ok(img) => { + let rgba: Vec = if img.bytes.len() == img.width * img.height * 4 { + img.bytes.to_vec() + } else { + // Convert RGB to RGBA + let rgb = img.bytes; + let mut rgba = Vec::with_capacity(rgb.len() / 3 * 4); + for c in rgb.chunks(3) { + rgba.extend_from_slice(&[c[0], c[1], c[2], 255]); + } + rgba + }; + Some((rgba, img.width as u32, img.height as u32)) + } + Err(e) => { + log::info!("[Clipboard] arboard get_image failed: {}", e); + None + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn try_shell_clipboard_image() -> Option<(Vec, u32, u32)> { + let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some() + || std::env::var("XDG_SESSION_TYPE").as_deref() == Ok("wayland"); + + if wayland { + try_wl_paste_image() + } else { + try_x11_clipboard_image() + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn try_wl_paste_image() -> Option<(Vec, u32, u32)> { + use std::process::Command; + + let output = Command::new("wl-paste") + .args(["--type", "image/png", "--no-newline"]) + .output() + .ok()?; + + if !output.status.success() || output.stdout.is_empty() { + return None; + } + + decode_image_to_rgba(&output.stdout) +} + +#[cfg(not(target_arch = "wasm32"))] +fn try_x11_clipboard_image() -> Option<(Vec, u32, u32)> { + use std::process::Command; + + let output = Command::new("xclip") + .args(["-selection", "clipboard", "-target", "image/png", "-o"]) + .output() + .ok()?; + + if !output.status.success() || output.stdout.is_empty() { + return None; + } + + decode_image_to_rgba(&output.stdout) +} + +#[cfg(not(target_arch = "wasm32"))] +fn decode_image_to_rgba(bytes: &[u8]) -> Option<(Vec, u32, u32)> { + use image::GenericImageView; + + let img = image::load_from_memory(bytes).ok()?; + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + Some((rgba.into_raw(), w, h)) +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/io/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/io/mod.rs new file mode 100644 index 0000000..c719003 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/io/mod.rs @@ -0,0 +1,4 @@ +//! I/O modules — tablet input, clipboard, and file I/O. + +pub mod clipboard; +pub mod tablet; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/io/tablet.rs b/hcie-iced-app/crates/hcie-iced-gui/src/io/tablet.rs new file mode 100644 index 0000000..0b62f4b --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/io/tablet.rs @@ -0,0 +1,257 @@ +//! Tablet/stylus input state for pressure-aware brush tools. +//! +//! Bridges graphics-tablet hardware (Wacom, Huion, XP-Pen, etc.) +//! into the Iced canvas so that brush strokes can vary width/opacity +//! by pen pressure. +//! +//! On Linux, an optional evdev background thread scans `/dev/input/event*` +//! for absolute-axis devices that report pressure, then streams updates +//! into the shared state. + +use std::sync::{Arc, Mutex}; + +/// Source of the most recent pressure value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TabletSource { + NoTablet, + Winit, + Evdev, + Touch, +} + +/// Shared tablet state consumed by the canvas each frame. +#[derive(Debug)] +pub struct TabletState { + /// Latest normalized pressure in [0.0, 1.0]. Defaults to 1.0 for mouse. + pressure: f32, + /// Last known stylus/mouse screen position. + pos: Option<(f32, f32)>, + /// True when the stylus is in proximity of the tablet surface. + in_proximity: bool, + /// Where the current pressure value came from. + source: TabletSource, + /// Screen size (width, height) for evdev position mapping. + screen_size: Option<(f32, f32)>, +} + +impl Default for TabletState { + fn default() -> Self { + Self { + pressure: 1.0, + pos: None, + in_proximity: false, + source: TabletSource::NoTablet, + screen_size: None, + } + } +} + +impl TabletState { + pub fn new() -> Self { + Self::default() + } + + /// Get the current pressure, clamped to [0.0, 1.0]. + pub fn current_pressure(&self) -> f32 { + self.pressure.clamp(0.0, 1.0) + } + + /// Get the last known screen position. + pub fn current_pos(&self) -> Option<(f32, f32)> { + self.pos + } + + /// Whether the stylus is in proximity. + pub fn in_proximity(&self) -> bool { + self.in_proximity + } + + /// Get the pressure source. + pub fn source(&self) -> TabletSource { + self.source + } + + /// Update pressure from a touch event. + pub fn set_touch_pressure(&mut self, pressure: f32) { + self.pressure = pressure.clamp(0.0, 1.0); + self.source = TabletSource::Touch; + } + + /// Update pressure from evdev. + pub fn set_evdev_pressure(&mut self, pressure: f32) { + self.pressure = pressure.clamp(0.0, 1.0); + self.source = TabletSource::Evdev; + } + + /// Set the screen-space stylus position. + pub fn set_screen_pos(&mut self, x: f32, y: f32) { + self.pos = Some((x, y)); + self.in_proximity = true; + } + + /// Set position from evdev normalized coords [0..1]. + pub fn set_evdev_pos(&mut self, nx: f32, ny: f32) { + if let Some((sw, sh)) = self.screen_size { + self.pos = Some((nx * sw, ny * sh)); + self.in_proximity = true; + } + } + + /// Update the cached screen size for evdev position mapping. + pub fn set_screen_size(&mut self, w: f32, h: f32) { + self.screen_size = Some((w, h)); + } + + /// Reset pressure to default (for mouse input). + pub fn reset_pressure(&mut self) { + self.pressure = 1.0; + self.source = TabletSource::NoTablet; + } +} + +/// Start the evdev listener thread for tablet pressure on Linux. +#[cfg(all(target_os = "linux", feature = "tablet-evdev"))] +pub fn start_evdev_listener(state: Arc>) { + std::thread::spawn(move || { + log::info!("[tablet-evdev] Scanning /dev/input/event* for tablet devices..."); + + let mut device = match find_tablet_device() { + Some(d) => d, + None => { + log::warn!( + "[tablet-evdev] No absolute-axis tablet device found. \ + Pressure/position will use mouse defaults (1.0)." + ); + return; + } + }; + + let name = device.name().unwrap_or("unknown"); + log::info!("[tablet-evdev] Using device: {}", name); + + let abs_x = evdev::AbsoluteAxisType(0x00); + let abs_y = evdev::AbsoluteAxisType(0x01); + let abs_pressure = evdev::AbsoluteAxisType(0x18); + + let (x_range, y_range, p_range) = match device.get_abs_state() { + Ok(state) => { + let xr = state[abs_x.0 as usize]; + let yr = state[abs_y.0 as usize]; + let pr = state[abs_pressure.0 as usize]; + log::info!( + "[tablet-evdev] ABS_X=[{},{}], ABS_Y=[{},{}], ABS_PRESSURE=[{},{}]", + xr.minimum, xr.maximum, yr.minimum, yr.maximum, pr.minimum, pr.maximum + ); + ( + (xr.minimum, xr.maximum), + (yr.minimum, yr.maximum), + (pr.minimum, pr.maximum), + ) + } + Err(e) => { + log::warn!( + "[tablet-evdev] Failed to read abs state: {}. Using default ranges.", + e + ); + ((0, 0), (0, 0), (0, 0)) + } + }; + + log::info!("[tablet-evdev] Listening for events..."); + + loop { + match device.fetch_events() { + Ok(events) => { + let mut guard = state.lock().unwrap(); + for event in events { + if let evdev::InputEventKind::AbsAxis(axis) = event.kind() { + let value = event.value(); + if axis == abs_pressure { + if p_range.1 > p_range.0 { + let normalized = + (value - p_range.0) as f32 / (p_range.1 - p_range.0) as f32; + guard.set_evdev_pressure(normalized); + } + } else if axis == abs_x { + if x_range.1 > x_range.0 { + let nx = + (value - x_range.0) as f32 / (x_range.1 - x_range.0) as f32; + let ny = guard.pos.map_or(0.5, |p| { + guard.screen_size.map_or(0.5, |s| p.1 / s.1) + }); + guard.set_evdev_pos(nx, ny); + } + } else if axis == abs_y { + if y_range.1 > y_range.0 { + let ny = + (value - y_range.0) as f32 / (y_range.1 - y_range.0) as f32; + let nx = guard.pos.map_or(0.5, |p| { + guard.screen_size.map_or(0.5, |s| p.0 / s.0) + }); + guard.set_evdev_pos(nx, ny); + } + } + } + } + } + Err(e) => { + log::warn!("[tablet-evdev] Read error: {}. Stopping listener.", e); + break; + } + } + } + }); +} + +#[cfg(not(all(target_os = "linux", feature = "tablet-evdev")))] +pub fn start_evdev_listener(_state: Arc>) { + log::info!("[tablet] evdev listener disabled (feature tablet-evdev not enabled or not Linux)"); +} + +#[cfg(all(target_os = "linux", feature = "tablet-evdev"))] +fn find_tablet_device() -> Option { + let mut candidates = Vec::new(); + let mut permission_denied = false; + + for (path, _device) in evdev::enumerate() { + match evdev::Device::open(&path) { + Ok(dev) => { + let name = dev.name().unwrap_or("unknown").to_string(); + let abs = dev.supported_absolute_axes(); + if let Some(abs) = abs { + let has_x = abs.contains(evdev::AbsoluteAxisType(0x00)); + let has_y = abs.contains(evdev::AbsoluteAxisType(0x01)); + let has_p = abs.contains(evdev::AbsoluteAxisType(0x18)); + if has_x && has_y && has_p { + candidates.push((path, dev, name)); + } + } + } + Err(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + permission_denied = true; + } + Err(e) => { + log::debug!("[tablet-evdev] Could not open {:?}: {}", path, e); + } + } + } + + if candidates.is_empty() && permission_denied { + log::warn!( + "[tablet-evdev] Cannot open /dev/input/event* — permission denied. \ + Run: sudo usermod -aG input $USER && relogin" + ); + } + + if candidates.is_empty() { + None + } else { + let (path, dev, name) = candidates.remove(0); + log::info!( + "[tablet-evdev] Selected tablet device: {:?} ({})", + path, + name + ); + Some(dev) + } +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/main.rs b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs new file mode 100644 index 0000000..e38de6f --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs @@ -0,0 +1,67 @@ +//! HCIE Iced GUI — entry point. +//! +//! Launches the Iced application with the HCIE image editor. + +mod app; +mod canvas; +mod color_picker; +mod dialogs; +mod dock; +mod io; +mod panels; +mod sidebar; +mod theme; + +use app::HcieIcedApp; + +fn main() { + let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .try_init(); + + log::info!("Starting HCIE Iced GUI"); + + let args: Vec = std::env::args().collect(); + let mut load_path: Option = None; + + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--help" | "-h" => { + println!("Usage: hcie-iced [OPTIONS] [FILE]"); + println!(); + println!("HCIE — Iced Edition"); + println!(); + println!("Options:"); + println!(" -h, --help Print this help message"); + println!(); + println!("Arguments:"); + println!(" FILE Image file to open on startup"); + std::process::exit(0); + } + other if !other.starts_with('-') => { + load_path = Some(std::path::PathBuf::from(other)); + } + unknown => { + eprintln!("Unknown argument: {}", unknown); + std::process::exit(1); + } + } + i += 1; + } + + // Start tablet evdev listener + let tablet_state = std::sync::Arc::new(std::sync::Mutex::new( + io::tablet::TabletState::new(), + )); + io::tablet::start_evdev_listener(tablet_state.clone()); + + let _ = iced::application("HCIE — Iced Edition", HcieIcedApp::update, HcieIcedApp::view) + .subscription(HcieIcedApp::subscription) + .theme(HcieIcedApp::theme) + .window_size((1280.0, 800.0)) + .run_with(move || { + let mut app = HcieIcedApp::new(load_path); + app.tablet_state = tablet_state; + (app, iced::Task::none()) + }); +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs new file mode 100644 index 0000000..2e28bb3 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs @@ -0,0 +1,98 @@ +//! AI Chat panel — AI assistant for image editing. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input}; +use iced::{Element, Length}; + +/// Build the AI chat panel. +pub fn view(input: &str, messages: &[(String, String)], colors: ThemeColors) -> Element<'static, Message> { + let provider_row = row![ + text("Provider:").size(10), + button(text("Ollama").size(10)).on_press(Message::NoOp).padding([4, 8]), + button(text("Claude").size(10)).on_press(Message::NoOp).padding([4, 8]), + button(text("OpenAI").size(10)).on_press(Message::NoOp).padding([4, 8]), + ] + .spacing(4); + + let mut msg_list = column![].spacing(4); + + if messages.is_empty() { + let welcome = container( + column![ + text("AI Assistant").size(12).font(iced::Font::MONOSPACE), + text("Ask me anything about image editing!").size(11), + text("").size(8), + text("I can help with:").size(10), + text(" - Creating and editing layers").size(10), + text(" - Applying filters and effects").size(10), + text(" - Drawing shapes and text").size(10), + text(" - Color adjustments").size(10), + ] + .spacing(2) + ) + .padding(12) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_hover)), + border: iced::Border::default().rounded(8), + ..Default::default() + }); + + msg_list = msg_list.push(welcome); + } else { + for (role, content) in messages { + let (label, color) = if role == "user" { + ("You", colors.accent) + } else { + ("AI", iced::Color::from_rgb(0.4, 0.9, 0.4)) + }; + + let content_owned = content.clone(); + let msg_text = text(content_owned).size(11); + let msg_text = msg_text.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(color), + }); + + let bubble = container( + column![text(label).size(10).font(iced::Font::MONOSPACE), msg_text].spacing(2) + ) + .width(Length::Fill) + .padding(8) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_hover)), + border: iced::Border::default().rounded(6), + ..Default::default() + }); + + msg_list = msg_list.push(bubble); + } + } + + let input_owned = input.to_string(); + let input_field = text_input("Ask the AI assistant...", &input_owned) + .on_input(|s| Message::AiChatInput(s)) + .width(Length::Fill); + + let send_btn = button(text("Send").size(11)).on_press(Message::AiChatSend).padding([6, 12]); + let clear_btn = button(text("Clear").size(11)).on_press(Message::AiChatClear).padding([6, 12]); + let input_row = row![input_field, send_btn, clear_btn].spacing(4); + + let panel = column![ + text("AI Assistant").size(13).font(iced::Font::MONOSPACE), + provider_row, + horizontal_rule(1), + scrollable(msg_list).height(Length::Fill), + horizontal_rule(1), + input_row, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs new file mode 100644 index 0000000..8a9100c --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs @@ -0,0 +1,91 @@ +//! AI Script panel — AI-powered script generation. +//! +//! Uses an LLM to generate procedural drawing scripts from natural language. +//! Supports Ollama, Claude, and OpenAI providers. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input}; +use iced::{Element, Length}; + +/// Build the AI script panel. +/// +/// Provides a UI for: +/// - Entering a natural language prompt +/// - Selecting an AI provider (Ollama, Claude, OpenAI) +/// - Generating a script from the prompt +/// - Viewing and running the generated script +pub fn view() -> Element<'static, Message> { + let provider_label = text("Provider").size(10); + let provider_selector = row![ + button(text("Ollama").size(10)).on_press(Message::NoOp).padding([4, 8]), + button(text("Claude").size(10)).on_press(Message::NoOp).padding([4, 8]), + button(text("OpenAI").size(10)).on_press(Message::NoOp).padding([4, 8]), + ] + .spacing(4); + + let url_input = text_input("Base URL", "http://localhost:11434") + .width(Length::Fill); + + let model_input = text_input("Model", "llama3") + .width(Length::Fill); + + let prompt_input = text_input("Describe what to draw...", "") + .width(Length::Fill); + + let generate_btn = button(text("Generate Script").size(11)) + .on_press(Message::NoOp) + .padding([6, 12]); + + let run_btn = button(text("Run Script").size(11)) + .on_press(Message::NoOp) + .padding([6, 12]); + + let copy_btn = button(text("Copy").size(11)) + .on_press(Message::NoOp) + .padding([6, 12]); + + let clear_btn = button(text("Clear").size(11)) + .on_press(Message::NoOp) + .padding([6, 12]); + + // Script output area + let script_output = text("Generated script will appear here...") + .size(10) + .font(iced::Font::MONOSPACE); + + let script_area = container( + scrollable(script_output).height(Length::Fill) + ) + .padding(8) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.12, 0.12, 0.12))), + border: iced::Border::default().rounded(4), + ..Default::default() + }); + + let panel = column![ + text("AI Script Generator").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + provider_label, + provider_selector, + url_input, + model_input, + horizontal_rule(1), + prompt_input, + row![generate_btn, run_btn, copy_btn, clear_btn].spacing(4), + horizontal_rule(1), + script_area, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.18, 0.18, 0.18))), + border: iced::Border::default().color(iced::Color::from_rgb(0.3, 0.3, 0.3)).width(1), + ..Default::default() + }) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs new file mode 100644 index 0000000..9b23ad3 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs @@ -0,0 +1,101 @@ +//! Brushes panel — brush preset browser with categories and style selection. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::BrushStyle; +use iced::widget::{column, container, horizontal_rule, scrollable, slider, text}; +use iced::{Element, Length}; + +/// A brush preset entry. +struct BrushPreset { + name: &'static str, + style: BrushStyle, +} + +const BRUSH_PRESETS: &[BrushPreset] = &[ + BrushPreset { name: "Round Soft", style: BrushStyle::SoftRound }, + BrushPreset { name: "Round Hard", style: BrushStyle::HardRound }, + BrushPreset { name: "Square", style: BrushStyle::Square }, + BrushPreset { name: "Pencil", style: BrushStyle::Pencil }, + BrushPreset { name: "Pen", style: BrushStyle::Pen }, + BrushPreset { name: "Ink Pen", style: BrushStyle::InkPen }, + BrushPreset { name: "Calligraphy", style: BrushStyle::Calligraphy }, + BrushPreset { name: "Marker", style: BrushStyle::Marker }, + BrushPreset { name: "Sketch", style: BrushStyle::Sketch }, + BrushPreset { name: "Hatch", style: BrushStyle::Hatch }, + BrushPreset { name: "Oil Paint", style: BrushStyle::Oil }, + BrushPreset { name: "Charcoal", style: BrushStyle::Charcoal }, + BrushPreset { name: "Watercolor", style: BrushStyle::Watercolor }, + BrushPreset { name: "Crayon", style: BrushStyle::Crayon }, + BrushPreset { name: "Airbrush", style: BrushStyle::Airbrush }, + BrushPreset { name: "Spray", style: BrushStyle::Spray }, + BrushPreset { name: "Star", style: BrushStyle::Star }, + BrushPreset { name: "Noise", style: BrushStyle::Noise }, + BrushPreset { name: "Texture", style: BrushStyle::Texture }, + BrushPreset { name: "Glow", style: BrushStyle::Glow }, + BrushPreset { name: "Leaf", style: BrushStyle::Leaf }, + BrushPreset { name: "Clouds", style: BrushStyle::Clouds }, + BrushPreset { name: "Default", style: BrushStyle::Default }, +]; + +/// Build the brushes panel. +pub fn view( + current_size: f32, + current_opacity: f32, + current_hardness: f32, + colors: ThemeColors, +) -> Element<'static, Message> { + let mut preset_list = column![].spacing(1); + + for preset in BRUSH_PRESETS { + let name = preset.name; + let style = preset.style; + + let item = container( + text(name).size(11) + ) + .width(Length::Fill) + .padding([4, 6]) + .style(move |_theme| styles::inactive_item_bg(colors)); + + let clickable = iced::widget::mouse_area(item) + .on_press(Message::BrushStyleSelected(style)); + + preset_list = preset_list.push(clickable); + } + + let size_slider = slider(1.0..=200.0, current_size, |v| Message::BrushSizeChanged(v)) + .step(1.0) + .width(Length::Fill); + + let opacity_slider = slider(0.0..=1.0, current_opacity, |v| Message::BrushOpacityChanged(v)) + .step(0.01) + .width(Length::Fill); + + let hardness_slider = slider(0.0..=1.0, current_hardness, |v| Message::BrushHardnessChanged(v)) + .step(0.01) + .width(Length::Fill); + + let panel = column![ + text("Brushes").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(preset_list).height(Length::Fill), + horizontal_rule(1), + text(format!("Size: {:.0}", current_size)).size(10), + size_slider, + text(format!("Opacity: {:.0}%", current_opacity * 100.0)).size(10), + opacity_slider, + text(format!("Hardness: {:.0}%", current_hardness * 100.0)).size(10), + hardness_slider, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs new file mode 100644 index 0000000..6bfa631 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs @@ -0,0 +1,157 @@ +//! Filters panel — filter selection and parameter editing. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::FilterType; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text}; +use iced::{Element, Length}; + +/// Filter category with its filters. +struct FilterCategory { + name: &'static str, + filters: &'static [FilterEntry], +} + +struct FilterEntry { + name: &'static str, + filter_type: FilterType, +} + +const FILTER_CATEGORIES: &[FilterCategory] = &[ + FilterCategory { + name: "Blur", + filters: &[ + FilterEntry { name: "Box Blur", filter_type: FilterType::BoxBlur }, + FilterEntry { name: "Gaussian Blur", filter_type: FilterType::GaussianBlur }, + FilterEntry { name: "Motion Blur", filter_type: FilterType::MotionBlur }, + FilterEntry { name: "Median Filter", filter_type: FilterType::MedianFilter }, + ], + }, + FilterCategory { + name: "Sharpen", + filters: &[ + FilterEntry { name: "Unsharp Mask", filter_type: FilterType::UnsharpMask }, + ], + }, + FilterCategory { + name: "Pixelate", + filters: &[ + FilterEntry { name: "Mosaic", filter_type: FilterType::Mosaic }, + FilterEntry { name: "Crystallize", filter_type: FilterType::Crystallize }, + ], + }, + FilterCategory { + name: "Distort", + filters: &[ + FilterEntry { name: "Pinch", filter_type: FilterType::Pinch }, + FilterEntry { name: "Twirl", filter_type: FilterType::Twirl }, + ], + }, + FilterCategory { + name: "Stylize", + filters: &[ + FilterEntry { name: "Oil Paint", filter_type: FilterType::OilPaint }, + ], + }, + FilterCategory { + name: "Color & Light", + filters: &[ + FilterEntry { name: "Levels", filter_type: FilterType::Levels }, + FilterEntry { name: "Vibrance", filter_type: FilterType::Vibrance }, + FilterEntry { name: "Exposure", filter_type: FilterType::Exposure }, + FilterEntry { name: "Posterize", filter_type: FilterType::Posterize }, + FilterEntry { name: "Threshold", filter_type: FilterType::Threshold }, + FilterEntry { name: "Channel Mixer", filter_type: FilterType::ChannelMixer }, + FilterEntry { name: "Black & White", filter_type: FilterType::BlackAndWhite }, + FilterEntry { name: "Selective Color", filter_type: FilterType::SelectiveColor }, + FilterEntry { name: "Gamma Correction", filter_type: FilterType::GammaCorrection }, + FilterEntry { name: "Extract Channel", filter_type: FilterType::ExtractChannel }, + FilterEntry { name: "Gradient Map", filter_type: FilterType::GradientMap }, + ], + }, + FilterCategory { + name: "Restore", + filters: &[ + FilterEntry { name: "Dehaze", filter_type: FilterType::Dehaze }, + ], + }, + FilterCategory { + name: "Noise & Pattern", + filters: &[ + FilterEntry { name: "Noise Pattern", filter_type: FilterType::NoisePattern }, + ], + }, +]; + +/// Build the filters panel. +pub fn view(selected_filter: Option, colors: ThemeColors) -> Element<'static, Message> { + let mut filter_list = column![].spacing(2); + + for category in FILTER_CATEGORIES { + let cat_name = text(category.name) + .size(12) + .font(iced::Font::MONOSPACE); + + let mut cat_filters = column![].spacing(1).padding(iced::Padding::ZERO.left(12)); + + for entry in category.filters { + let is_selected = selected_filter == Some(entry.filter_type); + let filter_text = text(entry.name).size(11); + let filter_text = if is_selected { + filter_text.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.accent), + }) + } else { + filter_text + }; + + let item_style = if is_selected { + styles::active_item_bg(colors) + } else { + styles::inactive_item_bg(colors) + }; + + let item = container(filter_text) + .width(Length::Fill) + .padding([3, 6]) + .style(move |_theme| item_style.clone()); + + let filter_type = entry.filter_type; + let clickable = iced::widget::mouse_area(item) + .on_press(Message::FilterSelect(filter_type)); + + cat_filters = cat_filters.push(clickable); + } + + filter_list = filter_list.push(cat_name); + filter_list = filter_list.push(cat_filters); + } + + let apply_btn = button(text("Apply").size(11)) + .on_press(Message::FilterApply) + .padding([6, 12]); + + let reset_btn = button(text("Reset").size(11)) + .on_press(Message::FilterReset) + .padding([6, 12]); + + let buttons = row![apply_btn, reset_btn].spacing(8); + + let panel = column![ + text("Filters").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(filter_list).height(Length::Fill), + horizontal_rule(1), + buttons, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs new file mode 100644 index 0000000..c6f4d15 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs @@ -0,0 +1,55 @@ +//! Vector geometry panel — shape properties for vector layers. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::VectorShape; +use iced::widget::{column, container, horizontal_rule, scrollable, text}; +use iced::{Element, Length}; + +/// Build the geometry panel for vector shapes. +pub fn view<'a>(shapes: &'a [VectorShape], colors: ThemeColors) -> Element<'a, Message> { + let panel = if shapes.is_empty() { + column![ + text("Geometry").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text("No vector shapes").size(11), + text("Use vector tools to create shapes").size(10), + ] + } else { + let mut shape_list = column![].spacing(2); + + for (i, shape) in shapes.iter().enumerate() { + let (shape_name, shape_info) = match shape { + VectorShape::Line { x1, y1, x2, y2, stroke, .. } => ("Line", format!("({:.0},{:.0})→({:.0},{:.0}) w:{:.1}", x1, y1, x2, y2, stroke)), + VectorShape::Rect { x1, y1, x2, y2, fill, stroke, .. } => ("Rect", format!("{}×{} fill:{} stroke:{:.1}", (x2-x1).abs() as u32, (y2-y1).abs() as u32, fill, stroke)), + VectorShape::Circle { x1, y1, x2, y2, fill, stroke, .. } => ("Circle", format!("r:{:.0} fill:{} stroke:{:.1}", ((x2-x1).abs() + (y2-y1).abs()) / 4.0, fill, stroke)), + _ => ("Shape", "Custom".to_string()), + }; + + let item = container( + column![text(format!("{}. {}", i + 1, shape_name)).size(11), text(shape_info).size(9)].spacing(2) + ) + .width(Length::Fill) + .padding([4, 6]) + .style(move |_theme| styles::inactive_item_bg(colors)); + + shape_list = shape_list.push(item); + } + + column![ + text("Geometry").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text(format!("{} shapes", shapes.len())).size(10), + horizontal_rule(1), + scrollable(shape_list).height(Length::Fill), + ] + }; + + container(panel.spacing(4).padding(8)) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs new file mode 100644 index 0000000..b0bc2f4 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs @@ -0,0 +1,66 @@ +//! History panel — undo/redo timeline. +//! +//! Displays the undo history as a scrollable list with +//! click-to-jump and current position highlighting. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use iced::widget::{column, container, horizontal_rule, scrollable, text}; +use iced::{Element, Length}; + +/// Build the history panel element. +pub fn view<'a>(descriptions: &[(usize, String)], current_idx: i32, colors: ThemeColors) -> Element<'a, Message> { + let mut history_list = column![].spacing(1); + + for (idx, desc) in descriptions { + let is_current = *idx as i32 == current_idx; + let is_future = *idx as i32 > current_idx; + + let label = text(format!("{}: {}", idx, desc)) + .size(11); + + let label = if is_current { + label.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.accent), + }) + } else if is_future { + label.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.text_disabled), + }) + } else { + label + }; + + let item_style = if is_current { + styles::active_item_bg(colors) + } else { + styles::inactive_item_bg(colors) + }; + + let item = container(label) + .width(Length::Fill) + .padding([3, 6]) + .style(move |_theme| item_style.clone()); + + let clickable = iced::widget::mouse_area(item) + .on_press(Message::HistoryJumpTo(*idx as i32)); + + history_list = history_list.push(clickable); + } + + let panel = column![ + text("History").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(history_list).height(Length::Fill), + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs new file mode 100644 index 0000000..f393b8c --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs @@ -0,0 +1,51 @@ +//! Layer details panel — detailed information about the active layer. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::LayerInfo; +use iced::widget::{column, container, horizontal_rule, row, scrollable, text}; +use iced::{Element, Length}; + +/// Build the layer details panel. +pub fn view<'a>(layers: &'a [LayerInfo], active_layer_id: u64, colors: ThemeColors) -> Element<'a, Message> { + let active_layer = layers.iter().find(|l| l.id == active_layer_id); + + let content: Element<'a, Message> = match active_layer { + Some(layer) => { + let layer_type = format!("{:?}", layer.layer_type); + let blend_mode = format!("{:?}", layer.blend_mode); + + column![ + row![text("Name:").size(10), text(&layer.name).size(11)].spacing(4), + row![text("ID:").size(10), text(format!("{}", layer.id)).size(10)].spacing(4), + row![text("Type:").size(10), text(layer_type).size(10)].spacing(4), + horizontal_rule(1), + row![text("Blend:").size(10), text(blend_mode).size(10)].spacing(4), + row![text("Opacity:").size(10), text(format!("{:.0}%", layer.opacity * 100.0)).size(10)].spacing(4), + row![text("Visible:").size(10), text(if layer.visible { "Yes" } else { "No" }).size(10)].spacing(4), + row![text("Locked:").size(10), text(if layer.locked { "Yes" } else { "No" }).size(10)].spacing(4), + horizontal_rule(1), + row![text("Effects:").size(10), text(if layer.has_effects { "Yes" } else { "No" }).size(10)].spacing(4), + ] + .spacing(4) + .into() + } + None => text("No layer selected").size(11).into(), + }; + + let panel = column![ + text("Layer Details").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(content).height(Length::Fill), + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs new file mode 100644 index 0000000..fef4b3d --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs @@ -0,0 +1,69 @@ +//! Layer styles panel — layer effects editor. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::LayerStyle; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text}; +use iced::{Element, Length}; + +/// Build the layer styles panel. +pub fn view<'a>(styles_list: &'a [LayerStyle], colors: ThemeColors) -> Element<'a, Message> { + let mut style_list = column![].spacing(4); + + if styles_list.is_empty() { + style_list = style_list.push(text("No layer styles").size(11)); + } else { + for (i, style) in styles_list.iter().enumerate() { + let (name, enabled) = match style { + LayerStyle::DropShadow { enabled, .. } => ("Drop Shadow", *enabled), + LayerStyle::InnerShadow { enabled, .. } => ("Inner Shadow", *enabled), + LayerStyle::OuterGlow { enabled, .. } => ("Outer Glow", *enabled), + LayerStyle::InnerGlow { enabled, .. } => ("Inner Glow", *enabled), + LayerStyle::BevelEmboss { enabled, .. } => ("Bevel & Emboss", *enabled), + LayerStyle::Satin { enabled, .. } => ("Satin", *enabled), + LayerStyle::ColorOverlay { enabled, .. } => ("Color Overlay", *enabled), + LayerStyle::GradientOverlay { enabled, .. } => ("Gradient Overlay", *enabled), + LayerStyle::PatternOverlay { enabled, .. } => ("Pattern Overlay", *enabled), + LayerStyle::Stroke { enabled, .. } => ("Stroke", *enabled), + }; + + let status = if enabled { "ON" } else { "OFF" }; + let toggle_btn = button(text(status).size(10)) + .on_press(Message::LayerStyleToggle(i as u32)) + .padding([2, 6]); + + let item = row![toggle_btn, text(name).size(11)].spacing(4).align_y(iced::Alignment::Center); + + let item_style = if enabled { styles::active_item_bg(colors) } else { styles::inactive_item_bg(colors) }; + + let item_container = container(item) + .width(Length::Fill) + .padding([4, 6]) + .style(move |_theme| item_style.clone()); + + style_list = style_list.push(item_container); + } + } + + let add_style_btn = button(text("+ Add Style").size(10)) + .on_press(Message::LayerStyleAdd) + .padding([4, 8]); + + let panel = column![ + text("Layer Styles").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(style_list).height(Length::Fill), + horizontal_rule(1), + add_style_btn, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs new file mode 100644 index 0000000..3e47957 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs @@ -0,0 +1,151 @@ +//! Layers panel — layer management with visibility, opacity, blend mode. +//! +//! Displays the layer stack with controls for visibility toggling, +//! opacity adjustment, blend mode selection, and layer operations. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::{BlendMode, LayerInfo}; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, slider, text}; +use iced::{Element, Length}; + +/// Build the layers panel element. +pub fn view<'a>(layers: &'a [LayerInfo], active_layer_id: u64, colors: ThemeColors) -> Element<'a, Message> { + let mut layer_list = column![].spacing(1); + + // Render layers in reverse order (top layer first in UI) + for layer in layers.iter().rev() { + let is_active = layer.id == active_layer_id; + + let name_text = text(&layer.name) + .size(11) + .width(Length::Fill); + + let name_text = if is_active { + name_text.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.accent), + }) + } else { + name_text + }; + + // Visibility toggle + let vis_icon = if layer.visible { "👁" } else { " " }; + let vis_btn = button(text(vis_icon).size(10)) + .on_press(Message::LayerToggleVisibility(layer.id)) + .padding([2, 4]); + + // Lock toggle + let lock_icon = if layer.locked { "🔒" } else { " " }; + let lock_btn = button(text(lock_icon).size(10)) + .on_press(Message::LayerToggleLock(layer.id)) + .padding([2, 4]); + + let name_row = row![ + vis_btn, + lock_btn, + name_text, + ] + .spacing(2) + .align_y(iced::Alignment::Center); + + let layer_item = if is_active { + container(name_row) + .width(Length::Fill) + .padding([4, 6]) + .style(move |_theme| styles::active_item_bg(colors)) + } else { + container(name_row) + .width(Length::Fill) + .padding([4, 6]) + .style(move |_theme| styles::inactive_item_bg(colors)) + }; + + // Click to select layer + let clickable = iced::widget::mouse_area(layer_item) + .on_press(Message::LayerSelect(layer.id)); + + layer_list = layer_list.push(clickable); + } + + // Opacity slider for active layer + let active_opacity = layers.iter() + .find(|l| l.id == active_layer_id) + .map(|l| l.opacity) + .unwrap_or(1.0); + + let opacity_slider = slider(0.0..=1.0, active_opacity, move |v| { + Message::LayerSetOpacity(active_layer_id, v) + }) + .step(0.01) + .width(Length::Fill); + + let opacity_row = row![ + text("Opacity").size(10), + text(format!("{:.0}%", active_opacity * 100.0)).size(10), + ] + .spacing(4); + + // Blend mode selector + let current_blend = layers.iter() + .find(|l| l.id == active_layer_id) + .map(|l| l.blend_mode) + .unwrap_or(BlendMode::Normal); + + let blend_label = text(format!("{:?}", current_blend)).size(10); + + // Action buttons + let add_btn = button(text("+").size(12)) + .on_press(Message::LayerAdd) + .padding([4, 8]); + + let del_btn = button(text("−").size(12)) + .on_press(Message::LayerDelete(active_layer_id)) + .padding([4, 8]); + + let merge_btn = button(text("Merge").size(10)) + .on_press(Message::LayerMergeDown) + .padding([4, 8]); + + let flatten_btn = button(text("Flatten").size(10)) + .on_press(Message::LayerFlatten) + .padding([4, 8]); + + let move_up_btn = button(text("▲").size(10)) + .on_press(Message::LayerMoveUp(active_layer_id)) + .padding([4, 4]); + + let move_down_btn = button(text("▼").size(10)) + .on_press(Message::LayerMoveDown(active_layer_id)) + .padding([4, 4]); + + let buttons_row = row![ + add_btn, del_btn, merge_btn, flatten_btn, + iced::widget::horizontal_rule(1), + move_up_btn, move_down_btn, + ] + .spacing(2) + .align_y(iced::Alignment::Center); + + let panel = column![ + text("Layers").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(layer_list).height(Length::Fill), + horizontal_rule(1), + opacity_row, + opacity_slider, + row![text("Blend:").size(10), blend_label].spacing(4), + horizontal_rule(1), + buttons_row, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs new file mode 100644 index 0000000..59d3f95 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs @@ -0,0 +1,319 @@ +//! Menu bar — top-level menus with dropdown support. +//! +//! Renders 9 menus (File, Edit, Tools, Image, Layer, Filter, Select, View, Help) +//! with functional dropdown menus that open on click and close on item selection. + +use crate::app::Message; +use hcie_engine_api::Tool; +use iced::widget::{button, column, container, horizontal_rule, row, text}; +use iced::{Element, Length}; + +/// Menu item definition. +struct MenuItem { + label: &'static str, + shortcut: &'static str, + enabled: bool, +} + +impl MenuItem { + fn new(label: &'static str) -> Self { + Self { label, shortcut: "", enabled: true } + } + + fn with_shortcut(label: &'static str, shortcut: &'static str) -> Self { + Self { label, shortcut, enabled: true } + } + + fn separator() -> Self { + Self { label: "─", shortcut: "", enabled: false } + } + + fn disabled(label: &'static str) -> Self { + Self { label, shortcut: "", enabled: false } + } +} + +/// All menu definitions. +struct MenuDef { + label: &'static str, + items: Vec, +} + +fn all_menus() -> Vec { + vec![ + // File (0) + MenuDef { + label: "File", + items: vec![ + MenuItem::with_shortcut("New", "Ctrl+N"), + MenuItem::with_shortcut("Open...", "Ctrl+O"), + MenuItem::separator(), + MenuItem::with_shortcut("Save", "Ctrl+S"), + MenuItem::with_shortcut("Save As...", "Ctrl+Shift+S"), + MenuItem::separator(), + MenuItem::new("Import SVG..."), + MenuItem::new("Import Brushes..."), + MenuItem::separator(), + MenuItem::new("Export PNG..."), + MenuItem::new("Export JPEG..."), + MenuItem::separator(), + MenuItem::new("Close"), + MenuItem::new("Exit"), + ], + }, + // Edit (1) + MenuDef { + label: "Edit", + items: vec![ + MenuItem::with_shortcut("Undo", "Ctrl+Z"), + MenuItem::with_shortcut("Redo", "Ctrl+Y"), + MenuItem::separator(), + MenuItem::with_shortcut("Cut", "Ctrl+X"), + MenuItem::with_shortcut("Copy", "Ctrl+C"), + MenuItem::with_shortcut("Paste", "Ctrl+V"), + MenuItem::separator(), + MenuItem::with_shortcut("Select All", "Ctrl+A"), + MenuItem::with_shortcut("Deselect", "Ctrl+D"), + MenuItem::new("Invert Selection"), + MenuItem::separator(), + MenuItem::new("Fill..."), + ], + }, + // Tools (2) + MenuDef { + label: "Tools", + items: vec![ + MenuItem::new("Move"), + MenuItem::new("Vector Select"), + MenuItem::separator(), + MenuItem::new("Rectangle Select"), + MenuItem::new("Lasso"), + MenuItem::new("Polygon Select"), + MenuItem::new("Magic Wand"), + MenuItem::separator(), + MenuItem::new("Crop"), + MenuItem::new("Eyedropper"), + MenuItem::separator(), + MenuItem::new("Brush"), + MenuItem::new("Pen"), + MenuItem::new("Spray"), + MenuItem::new("Eraser"), + MenuItem::separator(), + MenuItem::new("Flood Fill"), + MenuItem::new("Gradient"), + MenuItem::separator(), + MenuItem::new("Text"), + MenuItem::separator(), + MenuItem::new("Vector Line"), + MenuItem::new("Vector Rectangle"), + MenuItem::new("Vector Circle"), + MenuItem::separator(), + MenuItem::new("Reset Tool"), + ], + }, + // Image (3) + MenuDef { + label: "Image", + items: vec![ + MenuItem::new("Canvas Size..."), + MenuItem::new("Image Size..."), + MenuItem::separator(), + MenuItem::new("Rotate 90° CW"), + MenuItem::new("Rotate 90° CCW"), + MenuItem::new("Rotate 180°"), + MenuItem::separator(), + MenuItem::new("Flip Horizontal"), + MenuItem::new("Flip Vertical"), + MenuItem::separator(), + MenuItem::new("Crop to Selection"), + ], + }, + // Layer (4) + MenuDef { + label: "Layer", + items: vec![ + MenuItem::new("New Layer"), + MenuItem::new("Duplicate Layer"), + MenuItem::new("Delete Layer"), + MenuItem::separator(), + MenuItem::new("Merge Down"), + MenuItem::new("Flatten Image"), + MenuItem::separator(), + MenuItem::new("Clear Layer"), + MenuItem::separator(), + MenuItem::new("Layer Styles..."), + MenuItem::separator(), + MenuItem::new("Align Left"), + MenuItem::new("Align Center"), + MenuItem::new("Align Right"), + ], + }, + // Filter (5) + MenuDef { + label: "Filter", + items: vec![ + MenuItem::disabled("─ Blur"), + MenuItem::new(" Box Blur"), + MenuItem::new(" Gaussian Blur"), + MenuItem::new(" Motion Blur"), + MenuItem::disabled("─ Sharpen"), + MenuItem::new(" Unsharp Mask"), + MenuItem::disabled("─ Pixelate"), + MenuItem::new(" Mosaic"), + MenuItem::new(" Crystallize"), + MenuItem::disabled("─ Distort"), + MenuItem::new(" Pinch"), + MenuItem::new(" Twirl"), + MenuItem::disabled("─ Stylize"), + MenuItem::new(" Oil Paint"), + MenuItem::disabled("─ Color & Light"), + MenuItem::new(" Levels"), + MenuItem::new(" Vibrance"), + MenuItem::new(" Exposure"), + MenuItem::new(" Posterize"), + MenuItem::new(" Threshold"), + MenuItem::disabled("─ Restore"), + MenuItem::new(" Dehaze"), + ], + }, + // Select (6) + MenuDef { + label: "Select", + items: vec![ + MenuItem::with_shortcut("All", "Ctrl+A"), + MenuItem::with_shortcut("Deselect", "Ctrl+D"), + MenuItem::new("Invert Selection"), + MenuItem::separator(), + MenuItem::new("Grow..."), + MenuItem::new("Shrink..."), + MenuItem::new("Feather..."), + ], + }, + // View (7) + MenuDef { + label: "View", + items: vec![ + MenuItem::new("Zoom In"), + MenuItem::new("Zoom Out"), + MenuItem::new("Zoom Reset"), + MenuItem::separator(), + MenuItem::new("Fit to Window"), + MenuItem::new("100%"), + MenuItem::new("200%"), + MenuItem::separator(), + MenuItem::new("Debug Mode"), + ], + }, + // Help (8) + MenuDef { + label: "Help", + items: vec![ + MenuItem::new("About HCIE"), + MenuItem::new("Documentation"), + MenuItem::new("License"), + ], + }, + ] +} + +/// Build the menu bar element with optional dropdown. +/// +/// `active_menu`: index of the currently open menu (None = no dropdown). +pub fn view<'a>(_active_tool: &Tool, active_menu: Option) -> Element<'a, Message> { + let menus = all_menus(); + let mut bar_items = row![].spacing(0); + + for (idx, menu) in menus.iter().enumerate() { + let is_open = active_menu == Some(idx); + + let menu_btn = button( + text(menu.label).size(12) + ) + .on_press(Message::MenuOpen(idx)) + .padding([6, 12]); + + let menu_btn = if is_open { + menu_btn.style(iced::widget::button::primary) + } else { + menu_btn.style(iced::widget::button::secondary) + }; + + bar_items = bar_items.push(menu_btn); + } + + let bar = container(bar_items) + .width(Length::Fill) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.22, 0.22, 0.22))), + border: iced::Border::default().color(iced::Color::from_rgb(0.3, 0.3, 0.3)).width(1), + ..Default::default() + }); + + // If a menu is open, render the dropdown below + if let Some(menu_idx) = active_menu { + if menu_idx < menus.len() { + let dropdown = render_dropdown(menu_idx, &menus[menu_idx]); + + return column![bar, dropdown] + .width(Length::Fill) + .into(); + } + } + + bar.into() +} + +/// Render a dropdown menu for the given menu index. +fn render_dropdown<'a>(menu_idx: usize, menu: &MenuDef) -> Element<'a, Message> { + let mut items = column![].spacing(0); + + for (item_idx, item) in menu.items.iter().enumerate() { + if item.label == "─" { + // Separator + items = items.push( + container(horizontal_rule(1)) + .width(Length::Fill) + .padding([2, 0]) + ); + } else { + let label_row: Element<'_, Message> = if item.shortcut.is_empty() { + row![text(item.label).size(11)].into() + } else { + row![ + text(item.label).size(11).width(Length::Fill), + text(item.shortcut).size(10), + ] + .spacing(16) + .into() + }; + + let item_container = container(label_row) + .width(Length::Fill) + .padding([4, 16]); + + let item_container = if item.enabled { + let m_idx = menu_idx; + let i_idx = item_idx; + iced::widget::mouse_area(item_container) + .on_press(Message::MenuAction(m_idx, i_idx)) + .interaction(iced::mouse::Interaction::Pointer) + } else { + iced::widget::mouse_area(item_container) + .interaction(iced::mouse::Interaction::NotAllowed) + }; + + items = items.push(item_container); + } + } + + container(items) + .width(220) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.22, 0.22, 0.22))), + border: iced::Border::default() + .color(iced::Color::from_rgb(0.3, 0.3, 0.3)) + .width(1), + ..Default::default() + }) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs new file mode 100644 index 0000000..007df6b --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs @@ -0,0 +1,18 @@ +//! Panel modules — all UI panels for the Iced GUI. + +pub mod ai_chat_panel; +pub mod ai_script_panel; +pub mod brushes; +pub mod filters; +pub mod geometry; +pub mod history; +pub mod layer_details; +pub mod layer_styles; +pub mod layers; +pub mod menus; +pub mod properties; +pub mod script_panel; +pub mod status_bar; +pub mod styles; +pub mod text_editor; +pub mod title_bar; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs new file mode 100644 index 0000000..76c03e5 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs @@ -0,0 +1,134 @@ +//! Properties panel — active object/tool properties. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::Tool; +use iced::widget::{button, column, container, horizontal_rule, row, slider, text}; +use iced::{Element, Length}; + +/// Build the properties panel. +pub fn view<'a>( + active_tool: &Tool, + brush_size: f32, + brush_opacity: f32, + brush_hardness: f32, + selection_rect: Option<(f32, f32, f32, f32)>, + vector_draw: Option<((f32, f32), (f32, f32))>, + colors: ThemeColors, +) -> Element<'a, Message> { + let content: Element<'a, Message> = match active_tool { + Tool::Pen | Tool::Brush | Tool::Eraser | Tool::Spray => { + brush_properties(brush_size, brush_opacity, brush_hardness, colors) + } + Tool::Select => { + selection_properties(selection_rect, colors) + } + Tool::VectorRect | Tool::VectorCircle | Tool::VectorLine => { + vector_properties(vector_draw, colors) + } + Tool::Text => { + text_properties(colors) + } + Tool::Eyedropper => { + column![ + text("Eyedropper").size(11), + text("Click on canvas to pick color").size(10), + ] + .spacing(4) + .into() + } + Tool::FloodFill => { + column![ + text("Flood Fill").size(11), + text("Click on canvas to fill area").size(10), + ] + .spacing(4) + .into() + } + _ => { + text(format!("{:?}", active_tool)).size(11).into() + } + }; + + let panel = column![ + text("Properties").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + content, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} + +fn brush_properties<'a>(size: f32, opacity: f32, hardness: f32, _colors: ThemeColors) -> Element<'a, Message> { + let size_slider = slider(1.0..=200.0, size, |v| Message::BrushSizeChanged(v)) + .step(1.0) + .width(Length::Fill); + let opacity_slider = slider(0.0..=1.0, opacity, |v| Message::BrushOpacityChanged(v)) + .step(0.01) + .width(Length::Fill); + let hardness_slider = slider(0.0..=1.0, hardness, |v| Message::BrushHardnessChanged(v)) + .step(0.01) + .width(Length::Fill); + + column![ + text("Brush Properties").size(11), + row![text("Size").size(10), text(format!("{:.0}", size)).size(10)].spacing(4), + size_slider, + row![text("Opacity").size(10), text(format!("{:.0}%", opacity * 100.0)).size(10)].spacing(4), + opacity_slider, + row![text("Hardness").size(10), text(format!("{:.0}%", hardness * 100.0)).size(10)].spacing(4), + hardness_slider, + ] + .spacing(4) + .into() +} + +fn selection_properties<'a>(rect: Option<(f32, f32, f32, f32)>, _colors: ThemeColors) -> Element<'a, Message> { + match rect { + Some((x0, y0, x1, y1)) => { + let w = (x1 - x0).abs() as u32; + let h = (y1 - y0).abs() as u32; + column![ + text("Selection").size(11), + row![text("Size").size(10), text(format!("{}×{}", w, h)).size(10)].spacing(4), + ] + .spacing(4) + .into() + } + None => column![text("Selection").size(11), text("Drag on canvas").size(10)].spacing(4).into(), + } +} + +fn vector_properties<'a>(draw: Option<((f32, f32), (f32, f32))>, _colors: ThemeColors) -> Element<'a, Message> { + match draw { + Some(((x0, y0), (x1, y1))) => { + let w = (x1 - x0).abs() as u32; + let h = (y1 - y0).abs() as u32; + column![text("Vector Shape").size(11), row![text("Size").size(10), text(format!("{}×{}", w, h)).size(10)].spacing(4)].spacing(4).into() + } + None => column![text("Vector Shape").size(11), text("Drag on canvas").size(10)].spacing(4).into(), + } +} + +fn text_properties<'a>(_colors: ThemeColors) -> Element<'a, Message> { + column![ + text("Text Tool").size(11), + text("Click on canvas to add text").size(10), + horizontal_rule(1), + row![ + button(text("Left").size(10)).on_press(Message::TextAlignChanged("Left".to_string())).padding([4, 8]), + button(text("Center").size(10)).on_press(Message::TextAlignChanged("Center".to_string())).padding([4, 8]), + button(text("Right").size(10)).on_press(Message::TextAlignChanged("Right".to_string())).padding([4, 8]), + ].spacing(4), + ] + .spacing(4) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/script_panel.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/script_panel.rs new file mode 100644 index 0000000..e791c54 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/script_panel.rs @@ -0,0 +1,67 @@ +//! Script panel — procedural drawing DSL editor. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input}; +use iced::{Element, Length}; + +/// Build the script panel. +pub fn view(colors: ThemeColors) -> Element<'static, Message> { + let script_input = text_input("// Write your script here...", "") + .width(Length::Fill); + + let run_btn = button(text("Run Script").size(11)).on_press(Message::NoOp).padding([6, 12]); + let clear_btn = button(text("Clear").size(11)).on_press(Message::NoOp).padding([6, 12]); + let buttons = row![run_btn, clear_btn].spacing(4); + + let reference = column![ + text("Command Reference").size(11).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text("layer Create layer").size(10).font(iced::Font::MONOSPACE), + text("select Select layer").size(10).font(iced::Font::MONOSPACE), + text("color Set color").size(10).font(iced::Font::MONOSPACE), + text("size Set brush size").size(10).font(iced::Font::MONOSPACE), + text("line x1 y1 x2 y2 Draw line").size(10).font(iced::Font::MONOSPACE), + text("rect x1 y1 x2 y2 Draw rectangle").size(10).font(iced::Font::MONOSPACE), + text("circle cx cy r Draw circle").size(10).font(iced::Font::MONOSPACE), + text("fill x y Flood fill").size(10).font(iced::Font::MONOSPACE), + ] + .spacing(2) + .padding(8); + + let reference_container = container(scrollable(reference).height(Length::Fill)) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_app)), + border: iced::Border::default().rounded(4), + ..Default::default() + }); + + let error_display = container(text("No errors").size(10).font(iced::Font::MONOSPACE)) + .padding(4) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_app)), + border: iced::Border::default().rounded(4), + ..Default::default() + }); + + let panel = column![ + text("Script Editor").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + script_input, + buttons, + horizontal_rule(1), + reference_container, + horizontal_rule(1), + error_display, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs new file mode 100644 index 0000000..1e13bbf --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs @@ -0,0 +1,53 @@ +//! Status bar — bottom bar with tool info, coordinates, zoom, canvas size. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::Tool; +use iced::widget::{container, row, slider, text}; +use iced::{Element, Length}; + +/// Build the status bar element. +pub fn view<'a>( + active_tool: &Tool, + cursor_pos: Option<(u32, u32)>, + zoom: f32, + canvas_w: u32, + canvas_h: u32, + colors: ThemeColors, +) -> Element<'a, Message> { + let tool_name = text(active_tool.label()).size(11).font(iced::Font::MONOSPACE); + + let coords = match cursor_pos { + Some((x, y)) => text(format!("({}, {})", x, y)).size(11), + None => text(" ").size(11), + }; + + let zoom_pct = text(format!("{:.0}%", zoom * 100.0)).size(11).font(iced::Font::MONOSPACE); + let canvas_size = text(format!("{}×{}", canvas_w, canvas_h)).size(11); + + let zoom_slider = slider(0.01..=16.0, zoom, |v| Message::CanvasZoomSet(v)) + .step(0.01) + .width(100); + + let bar = row![ + tool_name, + text(" | ").size(11), + coords, + text(" ").size(11).width(Length::Fill), + text("Zoom:").size(11), + zoom_slider, + zoom_pct, + text(" | ").size(11), + canvas_size, + ] + .spacing(4) + .align_y(iced::Alignment::Center) + .padding([2, 8]); + + container(bar) + .width(Length::Fill) + .style(move |_theme| styles::statusbar_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs new file mode 100644 index 0000000..f6d5bf8 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs @@ -0,0 +1,124 @@ +//! Shared style functions for Photoshop-like theming. +//! +//! Provides reusable style helpers that match the Photoshop dark theme. + +use crate::theme::ThemeColors; + +/// Panel background — #2d2d2d +pub fn panel_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_panel)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Panel header — #333333, no horizontal rule look +pub fn panel_header(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_hover)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Active/selected item — subtle highlight, NOT blue tint +pub fn active_item_bg(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_active)), + border: iced::Border::default().color(colors.accent).width(1), + ..Default::default() + } +} + +/// Inactive item — transparent +pub fn inactive_item_bg(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color( + iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0) + )), + border: iced::Border::default(), + ..Default::default() + } +} + +/// Hover item +pub fn hover_item_bg(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_hover)), + border: iced::Border::default(), + ..Default::default() + } +} + +/// Menu bar background +pub fn menubar_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_app)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Title bar background +pub fn titlebar_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_app)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Status bar background +pub fn statusbar_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_app)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Sidebar background +pub fn sidebar_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_panel)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Dropdown menu background +pub fn dropdown_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_panel)), + border: iced::Border::default().color(colors.border_high).width(1), + ..Default::default() + } +} + +/// Document tab bar background +pub fn tabbar_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_app)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} + +/// Active document tab +pub fn active_tab_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_active)), + border: iced::Border::default().color(colors.accent).width(1), + ..Default::default() + } +} + +/// Inactive document tab +pub fn inactive_tab_background(colors: ThemeColors) -> iced::widget::container::Style { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_panel)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + } +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs new file mode 100644 index 0000000..68f16f6 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs @@ -0,0 +1,71 @@ +//! Text editor panel — font, size, color, alignment, effects. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, row, slider, text}; +use iced::{Element, Length}; + +/// Build the text editor panel. +pub fn view( + font_size: f32, + font_color: [u8; 4], + _alignment: &str, + orientation: &str, +) -> Element<'static, Message> { + let size_slider = slider(8.0..=200.0, font_size, |v| Message::TextSizeChanged(v)) + .step(1.0) + .width(Length::Fill); + + let r = font_color[0]; + let g = font_color[1]; + let b = font_color[2]; + + let color_r = slider(0..=255, r, move |v| Message::TextColorChanged([v, g, b, 255])); + let color_g = slider(0..=255, g, move |v| Message::TextColorChanged([r, v, b, 255])); + let color_b = slider(0..=255, b, move |v| Message::TextColorChanged([r, g, v, 255])); + + let align_left = button(text("Left").size(10)) + .on_press(Message::TextAlignChanged("Left".to_string())) + .padding([4, 8]); + let align_center = button(text("Center").size(10)) + .on_press(Message::TextAlignChanged("Center".to_string())) + .padding([4, 8]); + let align_right = button(text("Right").size(10)) + .on_press(Message::TextAlignChanged("Right".to_string())) + .padding([4, 8]); + + let accept_btn = button(text("Accept").size(11)) + .on_press(Message::TextAccept) + .padding([6, 12]); + let cancel_btn = button(text("Cancel").size(11)) + .on_press(Message::TextCancel) + .padding([6, 12]); + + let orient = orientation.to_string(); + + let panel = column![ + text("Text").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + text(format!("Size: {:.0}", font_size)).size(10), + size_slider, + text("Color").size(10), + row![text("R").size(10), color_r].spacing(4), + row![text("G").size(10), color_g].spacing(4), + row![text("B").size(10), color_b].spacing(4), + row![align_left, align_center, align_right].spacing(4), + text(format!("Orientation: {}", orient)).size(10), + horizontal_rule(1), + row![accept_btn, cancel_btn].spacing(8), + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.18, 0.18, 0.18))), + border: iced::Border::default().color(iced::Color::from_rgb(0.3, 0.3, 0.3)).width(1), + ..Default::default() + }) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs new file mode 100644 index 0000000..4dfe322 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs @@ -0,0 +1,63 @@ +//! Title bar — custom title bar with app icon, version, document name, and window controls. +//! Uses ThemeColors for consistent styling. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use iced::widget::{button, container, row, text}; +use iced::{Element, Length}; + +/// Build the title bar element. +pub fn view<'a>( + doc_name: &'a str, + zoom: f32, + canvas_w: u32, + canvas_h: u32, + colors: ThemeColors, +) -> Element<'a, Message> { + let app_name = text("HCIE") + .size(14) + .font(iced::Font::MONOSPACE) + .style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.accent), + }); + + let version = text("v3.0") + .size(10) + .style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.text_secondary), + }); + + let doc_info = text(format!( + "{} — {}×{} @ {:.0}%", + doc_name, canvas_w, canvas_h, zoom * 100.0 + )) + .size(12); + + let minimize_btn = button(text("─").size(10)).on_press(Message::NoOp).padding([4, 8]); + let maximize_btn = button(text("□").size(10)).on_press(Message::NoOp).padding([4, 8]); + let close_btn = button(text("×").size(10)) + .on_press(Message::DialogOpen(crate::app::ActiveDialog::CloseConfirm)) + .padding([4, 8]); + + let controls = row![minimize_btn, maximize_btn, close_btn].spacing(2); + + let bar = row![ + app_name, + version, + text(" ").size(12), + doc_info, + iced::widget::horizontal_rule(1), + controls, + ] + .spacing(4) + .align_y(iced::Alignment::Center) + .padding([0, 8]); + + container(bar) + .width(Length::Fill) + .height(28) + .align_y(iced::Alignment::Center) + .style(move |_theme| styles::titlebar_background(colors)) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs new file mode 100644 index 0000000..e5c0024 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs @@ -0,0 +1,229 @@ +//! Tool sidebar — Photoshop-style vertical toolbox with SVG icons. +//! +//! Features: +//! - Single column (36px) or two-column (72px) mode +//! - Toggle button (◀/▶) at top to expand/collapse +//! - SVG icons with active/hover states +//! - Overlapping color swatches at bottom +//! - Submenu indicator for tools with sub-tools + +use crate::app::{Message, ToolState}; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::Tool; +use iced::widget::{column, container, mouse_area, row, svg, text}; +use iced::{Element, Length}; + +/// Tool slot definition — (primary tool, label, svg_path, sub_tools). +struct ToolSlot { + tool: Tool, + label: &'static str, + icon: &'static str, +} + +/// All tool slots matching egui's TOOL_SLOTS. +const TOOL_SLOTS: &[ToolSlot] = &[ + ToolSlot { tool: Tool::Move, label: "Move", icon: "icons/move.svg" }, + ToolSlot { tool: Tool::VectorSelect, label: "Vector Select", icon: "icons/vector_select.svg" }, + ToolSlot { tool: Tool::Select, label: "Rectangle Select", icon: "icons/rect_select.svg" }, + ToolSlot { tool: Tool::Lasso, label: "Lasso", icon: "icons/lasso.svg" }, + ToolSlot { tool: Tool::MagicWand, label: "Magic Wand", icon: "icons/magic_wand.svg" }, + ToolSlot { tool: Tool::Eyedropper, label: "Eyedropper", icon: "icons/eyedropper.svg" }, + ToolSlot { tool: Tool::Crop, label: "Crop", icon: "icons/crop.svg" }, + ToolSlot { tool: Tool::Brush, label: "Brush", icon: "icons/brush.svg" }, + ToolSlot { tool: Tool::Pen, label: "Pen", icon: "icons/pen.svg" }, + ToolSlot { tool: Tool::Spray, label: "Spray", icon: "icons/spray.svg" }, + ToolSlot { tool: Tool::Eraser, label: "Eraser", icon: "icons/eraser.svg" }, + ToolSlot { tool: Tool::FloodFill, label: "Flood Fill", icon: "icons/bucket.svg" }, + ToolSlot { tool: Tool::Gradient, label: "Gradient", icon: "icons/gradient.svg" }, + ToolSlot { tool: Tool::Text, label: "Text", icon: "icons/text.svg" }, + ToolSlot { tool: Tool::VectorLine, label: "Vector Line", icon: "icons/line.svg" }, + ToolSlot { tool: Tool::VectorRect, label: "Vector Rectangle", icon: "icons/vector_rect.svg" }, + ToolSlot { tool: Tool::VectorCircle, label: "Vector Circle", icon: "icons/circle.svg" }, +]; + +/// Build the sidebar element. +pub fn view<'a>( + tool_state: &'a ToolState, + fg_color: &'a [u8; 4], + bg_color: &'a [u8; 4], + colors: ThemeColors, + two_column: bool, +) -> Element<'a, Message> { + let slot_w: f32 = if two_column { 36.0 } else { 36.0 }; + let sidebar_w: f32 = if two_column { 76.0 } else { 38.0 }; + + // Toggle button at top + let toggle_icon = if two_column { "◀" } else { "▶" }; + let toggle_btn = mouse_area( + container(text(toggle_icon).size(10).color(colors.text_secondary)) + .width(Length::Fill) + .center_x(Length::Fill) + .padding([2, 0]) + ) + .on_press(Message::ToggleToolbox) + .interaction(iced::mouse::Interaction::Pointer); + + let toggle_row = container(toggle_btn) + .width(Length::Fill) + .padding([2, 0]) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_panel)), + border: iced::Border::default().color(colors.border_low).width(1), + ..Default::default() + }); + + // Tool buttons + let mut tool_list = column![].spacing(1); + + if two_column { + // Two-column mode: render in pairs + for chunk in TOOL_SLOTS.chunks(2) { + let mut row_items = row![].spacing(1); + for slot in chunk { + let btn = tool_button(slot, tool_state, colors); + row_items = row_items.push(btn); + } + // Fill remaining space if odd + if chunk.len() == 1 { + row_items = row_items.push( + container(text("").size(1)) + .width(slot_w) + .height(28) + ); + } + tool_list = tool_list.push(row_items); + } + } else { + // Single column mode + for slot in TOOL_SLOTS { + let btn = tool_button(slot, tool_state, colors); + tool_list = tool_list.push(btn); + } + } + + // Color swatches at bottom + let fg = fg_color; + let bg = bg_color; + + let fg_swatch = container(text("")) + .width(18) + .height(18) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + fg[0] as f32 / 255.0, fg[1] as f32 / 255.0, fg[2] as f32 / 255.0, fg[3] as f32 / 255.0, + ))), + border: iced::Border::default().rounded(2).color(colors.border_high).width(1), + ..Default::default() + }); + + let bg_swatch = container(text("")) + .width(18) + .height(18) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + bg[0] as f32 / 255.0, bg[1] as f32 / 255.0, bg[2] as f32 / 255.0, bg[3] as f32 / 255.0, + ))), + border: iced::Border::default().rounded(2).color(colors.border_high).width(1), + ..Default::default() + }); + + let swap_btn = mouse_area( + container(text("⇅").size(8).color(colors.text_secondary)) + .padding(2) + ) + .on_press(Message::SwapColors) + .interaction(iced::mouse::Interaction::Pointer); + + let color_section = container( + row![fg_swatch, bg_swatch, swap_btn].spacing(1) + ) + .padding([4, 0]) + .center_x(Length::Fill); + + // Separator lines + let sep_style = move |_theme: &iced::Theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.border_low)), + ..Default::default() + }; + + let sidebar = column![ + toggle_row, + container(text("").height(1)).width(Length::Fill).style(sep_style.clone()), + tool_list, + container(text("").height(1)).width(Length::Fill).style(sep_style), + color_section, + ] + .width(sidebar_w) + .spacing(0); + + container(sidebar) + .style(move |_theme| styles::sidebar_background(colors)) + .height(Length::Fill) + .into() +} + +/// Create a single tool button. +fn tool_button<'a>(slot: &ToolSlot, tool_state: &ToolState, colors: ThemeColors) -> Element<'a, Message> { + let is_active = std::mem::discriminant(&tool_state.active_tool) + == std::mem::discriminant(&slot.tool); + + let btn_style = if is_active { + iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_active)), + border: iced::Border::default().color(colors.accent).width(1), + ..Default::default() + } + } else { + iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), + border: iced::Border::default(), + ..Default::default() + } + }; + + // Try to load SVG icon - search multiple paths + let icon_paths = [ + std::path::PathBuf::from(slot.icon), // relative to cwd + std::path::Path::new("hcie-iced-app/assets").join(slot.icon), // relative to project root + std::path::PathBuf::from(format!("/mnt/extra/00_PROJECTS/hcie-rust-v3.05/hcie-iced-app/assets/{}", slot.icon)), // absolute + ]; + + let icon_element: Element<'_, Message> = if let Some(icon_path) = icon_paths.iter().find(|p| p.exists()) { + let handle = svg::Handle::from_path(icon_path); + let icon_color = if is_active { colors.accent } else { colors.text_primary }; + svg(handle) + .width(20) + .height(20) + .style(move |_theme: &iced::Theme, _status: iced::widget::svg::Status| svg::Style { + color: Some(icon_color), + }) + .into() + } else { + // Fallback to text label + text(slot.label.chars().next().unwrap_or('?').to_string()) + .size(14) + .color(if is_active { colors.accent } else { colors.text_primary }) + .into() + }; + + let btn = container(icon_element) + .width(28) + .height(28) + .center_y(20) + .center_x(20) + .style(move |_theme| btn_style.clone()); + + let tool = slot.tool; + let clickable = mouse_area(btn) + .on_press(Message::ToolSelected(tool)) + .interaction(iced::mouse::Interaction::Pointer); + + // Tooltip on hover + iced::widget::tooltip( + clickable, + iced::widget::text(slot.label).size(11), + iced::widget::tooltip::Position::Right, + ) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs b/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs new file mode 100644 index 0000000..5349474 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs @@ -0,0 +1,39 @@ +//! Theme system — shared color tokens for all panels. +//! +//! Uses the iced-panel-adapter ThemeColors for consistent styling +//! across all panels and the main application. + +pub use iced_panel_adapter::theme::{ThemeColors, ThemePreset}; + +use std::sync::{Arc, Mutex}; + +/// Shared theme state for the application. +#[derive(Clone)] +pub struct ThemeState { + colors: Arc>, +} + +impl ThemeState { + /// Create a new theme state with the default preset. + pub fn new() -> Self { + Self { + colors: Arc::new(Mutex::new(ThemeColors::get(ThemePreset::default()))), + } + } + + /// Get the current theme colors. + pub fn colors(&self) -> ThemeColors { + *self.colors.lock().unwrap() + } + + /// Switch to a different theme preset. + pub fn set_preset(&self, preset: ThemePreset) { + *self.colors.lock().unwrap() = ThemeColors::get(preset); + } +} + +impl Default for ThemeState { + fn default() -> Self { + Self::new() + } +} diff --git a/hcie-iced-app/crates/iced-panel-adapter/Cargo.toml b/hcie-iced-app/crates/iced-panel-adapter/Cargo.toml new file mode 100644 index 0000000..825223d --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-adapter/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "iced-panel-adapter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +hcie-engine-api = { path = "../../../hcie-engine-api" } +iced = { workspace = true } +serde_json = { workspace = true } diff --git a/hcie-iced-app/crates/iced-panel-adapter/src/lib.rs b/hcie-iced-app/crates/iced-panel-adapter/src/lib.rs new file mode 100644 index 0000000..3499d19 --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-adapter/src/lib.rs @@ -0,0 +1,128 @@ +//! Iced panel adapter — bridges engine `ModulePanel` / `WidgetDescription` +//! types to Iced widget trees. +//! +//! This crate is the Iced equivalent of `egui-panel-adapter`, providing +//! dynamic widget rendering for filter parameter panels and other +//! engine-provided UI definitions. + +pub mod theme; +pub use theme::{ThemeColors, ThemePreset}; + +use hcie_engine_api::{ModulePanel, WidgetDescription}; +use iced::widget::{button, checkbox, horizontal_rule, pick_list, slider, text}; +use iced::{Element, Length}; + +/// Message type for panel widget interactions. +#[derive(Debug, Clone)] +pub enum PanelMessage { + /// A widget value changed: (widget_id, new_json_value). + ValueChanged(String, serde_json::Value), +} + +/// Renders a complete `ModulePanel` as an Iced element tree. +pub fn render_module_panel(panel: &ModulePanel) -> Element<'_, PanelMessage> { + let mut col = iced::widget::Column::new().spacing(8); + + col = col.push(text(&panel.title).size(16).font(iced::Font::MONOSPACE)); + + for widget in &panel.widgets { + col = col.push(render_widget(widget)); + } + + col.into() +} + +/// Renders a single `WidgetDescription` as an Iced element. +pub fn render_widget(widget: &WidgetDescription) -> Element<'_, PanelMessage> { + match widget { + WidgetDescription::Label { text: label_text, .. } => { + text(label_text.as_str()).into() + } + WidgetDescription::Button { id, label } => { + let id = id.clone(); + let label = label.clone(); + button(text(label)) + .on_press(PanelMessage::ValueChanged(id, serde_json::Value::Null)) + .into() + } + WidgetDescription::Slider { id, label, min, max, value, .. } => { + let id = id.clone(); + let current = *value; + + let slider_widget = slider(*min..=*max, current, move |v| { + PanelMessage::ValueChanged(id.clone(), serde_json::Value::from(v)) + }) + .step(0.01) + .width(Length::Fill); + + iced::widget::Column::new() + .push(text(format!("{}: {:.2}", label, current)).size(12)) + .push(slider_widget) + .spacing(4) + .into() + } + WidgetDescription::ColorPicker { label, rgba, .. } => { + let r = rgba[0] as f32 / 255.0; + let g = rgba[1] as f32 / 255.0; + let b = rgba[2] as f32 / 255.0; + + let swatch = iced::widget::container(text("")) + .width(24) + .height(24) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(r, g, b))), + border: iced::Border::default().rounded(4), + ..Default::default() + }); + + iced::widget::Row::new() + .push(text(label.as_str()).size(12)) + .push(swatch) + .spacing(8) + .align_y(iced::Alignment::Center) + .into() + } + WidgetDescription::Checkbox { id, label, checked } => { + let id = id.clone(); + let val = *checked; + checkbox(label.as_str(), val) + .on_toggle(move |new_val| { + PanelMessage::ValueChanged(id.clone(), serde_json::Value::Bool(new_val)) + }) + .into() + } + WidgetDescription::Dropdown { id, label, options, selected } => { + let id = id.clone(); + let selected_text = options.get(*selected).cloned().unwrap_or_default(); + + iced::widget::Column::new() + .push(text(label.as_str()).size(12)) + .push( + pick_list(options.clone(), Some(selected_text), move |choice| { + let idx = options.iter().position(|o| *o == choice).unwrap_or(0); + PanelMessage::ValueChanged(id.clone(), serde_json::Value::from(idx)) + }) + .width(Length::Fill) + ) + .spacing(4) + .into() + } + WidgetDescription::Separator => { + horizontal_rule(1).into() + } + WidgetDescription::Group { label, children } => { + let mut inner = iced::widget::Column::new() + .spacing(4) + .padding(iced::Padding::ZERO.left(12)); + for child in children { + inner = inner.push(render_widget(child)); + } + + iced::widget::Column::new() + .push(text(label.as_str()).size(13).font(iced::Font::MONOSPACE)) + .push(inner) + .spacing(4) + .into() + } + } +} diff --git a/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs b/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs new file mode 100644 index 0000000..db74e1d --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs @@ -0,0 +1,123 @@ +//! Theme colors and presets for the Iced GUI panels. +//! +//! Mirrors the egui-panel-adapter theme system with the same color tokens +//! for visual consistency across both GUI implementations. + +use iced::Color; + +/// Available theme presets. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ThemePreset { + #[default] + Photoshop, + ProDark, + Amoled, + PhotoshopLight, + ProLight, +} + +/// Named color tokens for the application theme. +#[derive(Debug, Clone, Copy)] +pub struct ThemeColors { + pub bg_app: Color, + pub bg_panel: Color, + pub bg_hover: Color, + pub bg_active: Color, + pub accent: Color, + pub accent_hover: Color, + pub border_low: Color, + pub border_high: Color, + pub text_primary: Color, + pub text_secondary: Color, + pub text_disabled: Color, + pub danger: Color, + pub is_light: bool, +} + +impl ThemeColors { + /// Resolve colors for a given preset. + pub fn get(preset: ThemePreset) -> Self { + match preset { + ThemePreset::Photoshop => Self { + bg_app: Color::from_rgb(0.118, 0.118, 0.118), // #1e1e1e + bg_panel: Color::from_rgb(0.176, 0.176, 0.176), // #2d2d2d + bg_hover: Color::from_rgb(0.227, 0.227, 0.227), // #3a3a3a + bg_active: Color::from_rgb(0.251, 0.251, 0.251), // #404040 (neutral, NOT blue) + accent: Color::from_rgb(0.176, 0.549, 0.922), // #2d8ceb + accent_hover: Color::from_rgb(0.220, 0.600, 0.950), + border_low: Color::from_rgb(0.102, 0.102, 0.102), // #1a1a1a + border_high: Color::from_rgb(0.267, 0.267, 0.267), // #444444 + text_primary: Color::from_rgb(0.800, 0.800, 0.800), // #cccccc + text_secondary: Color::from_rgb(0.533, 0.533, 0.533), // #888888 + text_disabled: Color::from_rgb(0.333, 0.333, 0.333), // #555555 + danger: Color::from_rgb(0.800, 0.200, 0.200), + is_light: false, + }, + ThemePreset::ProDark => Self { + bg_app: Color::from_rgb(0.149, 0.149, 0.149), + bg_panel: Color::from_rgb(0.188, 0.188, 0.188), + bg_hover: Color::from_rgb(0.235, 0.235, 0.235), + bg_active: Color::from_rgb(0.165, 0.345, 0.482), + accent: Color::from_rgb(0.165, 0.345, 0.482), + accent_hover: Color::from_rgb(0.196, 0.396, 0.553), + border_low: Color::from_rgb(0.118, 0.118, 0.118), + border_high: Color::from_rgb(0.314, 0.314, 0.314), + text_primary: Color::from_rgb(0.933, 0.933, 0.933), + text_secondary: Color::from_rgb(0.604, 0.604, 0.604), + text_disabled: Color::from_rgb(0.400, 0.400, 0.400), + danger: Color::from_rgb(0.780, 0.220, 0.267), + is_light: false, + }, + ThemePreset::Amoled => Self { + bg_app: Color::from_rgb(0.059, 0.059, 0.059), + bg_panel: Color::from_rgb(0.118, 0.118, 0.118), + bg_hover: Color::from_rgb(0.176, 0.176, 0.176), + bg_active: Color::from_rgb(0.122, 0.302, 0.455), + accent: Color::from_rgb(0.122, 0.302, 0.455), + accent_hover: Color::from_rgb(0.153, 0.353, 0.518), + border_low: Color::from_rgb(0.078, 0.078, 0.078), + border_high: Color::from_rgb(0.275, 0.275, 0.275), + text_primary: Color::from_rgb(0.945, 0.945, 0.945), + text_secondary: Color::from_rgb(0.565, 0.565, 0.565), + text_disabled: Color::from_rgb(0.353, 0.353, 0.353), + danger: Color::from_rgb(0.753, 0.220, 0.294), + is_light: false, + }, + ThemePreset::PhotoshopLight => Self { + bg_app: Color::from_rgb(0.941, 0.937, 0.925), + bg_panel: Color::from_rgb(0.906, 0.902, 0.890), + bg_hover: Color::from_rgb(0.855, 0.851, 0.839), + bg_active: Color::from_rgb(0.200, 0.400, 0.600), + accent: Color::from_rgb(0.200, 0.400, 0.600), + accent_hover: Color::from_rgb(0.235, 0.447, 0.651), + border_low: Color::from_rgb(0.780, 0.776, 0.765), + border_high: Color::from_rgb(0.600, 0.596, 0.584), + text_primary: Color::from_rgb(0.145, 0.141, 0.133), + text_secondary: Color::from_rgb(0.400, 0.396, 0.388), + text_disabled: Color::from_rgb(0.600, 0.596, 0.588), + danger: Color::from_rgb(0.800, 0.200, 0.200), + is_light: true, + }, + ThemePreset::ProLight => Self { + bg_app: Color::from_rgb(0.961, 0.961, 0.969), + bg_panel: Color::from_rgb(0.918, 0.918, 0.929), + bg_hover: Color::from_rgb(0.867, 0.867, 0.882), + bg_active: Color::from_rgb(0.263, 0.475, 0.675), + accent: Color::from_rgb(0.263, 0.475, 0.675), + accent_hover: Color::from_rgb(0.294, 0.514, 0.710), + border_low: Color::from_rgb(0.820, 0.820, 0.835), + border_high: Color::from_rgb(0.651, 0.651, 0.671), + text_primary: Color::from_rgb(0.161, 0.169, 0.184), + text_secondary: Color::from_rgb(0.420, 0.427, 0.447), + text_disabled: Color::from_rgb(0.620, 0.627, 0.647), + danger: Color::from_rgb(0.820, 0.220, 0.220), + is_light: true, + }, + } + } + + /// Auto-detect based on iced theme. + pub fn current() -> Self { + Self::get(ThemePreset::default()) + } +} diff --git a/hcie-iced-app/crates/iced-panel-ai-chat/Cargo.toml b/hcie-iced-app/crates/iced-panel-ai-chat/Cargo.toml new file mode 100644 index 0000000..d3a0f35 --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-ai-chat/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "iced-panel-ai-chat" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +hcie-engine-api = { path = "../../../hcie-engine-api" } +iced = { workspace = true } +serde_json = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true } diff --git a/hcie-iced-app/crates/iced-panel-ai-chat/src/lib.rs b/hcie-iced-app/crates/iced-panel-ai-chat/src/lib.rs new file mode 100644 index 0000000..c1e95dd --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-ai-chat/src/lib.rs @@ -0,0 +1,228 @@ +//! Iced AI Chat Panel — streaming AI assistant for creative editing. +//! +//! Connects to Ollama/OpenAI-compatible endpoints for AI-assisted +//! drawing and image manipulation. + +use hcie_engine_api::Engine; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input}; +use iced::{Element, Length}; + +/// Message type for the AI chat panel. +#[derive(Debug, Clone)] +pub enum ChatMessage { + /// User input changed. + InputChanged(String), + /// Send the current message. + Send, + /// Clear the conversation. + Clear, + /// Received a response from the AI. + ResponseReceived(String), + /// Connection error. + Error(String), +} + +/// A single chat message. +#[derive(Debug, Clone)] +pub struct ChatEntry { + pub role: ChatRole, + pub content: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ChatRole { + User, + Assistant, + System, +} + +/// AI Chat panel state. +pub struct AiChatPanel { + /// Current user input. + pub input: String, + /// Conversation history. + pub messages: Vec, + /// Whether a request is in progress. + pub is_loading: bool, + /// API endpoint URL. + pub endpoint_url: String, + /// Model name. + pub model_name: String, +} + +impl Default for AiChatPanel { + fn default() -> Self { + Self { + input: String::new(), + messages: Vec::new(), + is_loading: false, + endpoint_url: "http://localhost:11434".to_string(), + model_name: "llama3".to_string(), + } + } +} + +impl AiChatPanel { + /// Handle a chat message. + pub fn update(&mut self, msg: ChatMessage, engine: &mut Engine) -> bool { + match msg { + ChatMessage::InputChanged(text) => { + self.input = text; + false + } + ChatMessage::Send => { + if self.input.trim().is_empty() { + return false; + } + + let user_msg = self.input.clone(); + self.messages.push(ChatEntry { + role: ChatRole::User, + content: user_msg.clone(), + }); + self.input.clear(); + self.is_loading = true; + + // For now, return a placeholder response + // Real implementation would use reqwest + tokio + let response = generate_placeholder_response(&user_msg, engine); + self.messages.push(ChatEntry { + role: ChatRole::Assistant, + content: response, + }); + self.is_loading = false; + false + } + ChatMessage::Clear => { + self.messages.clear(); + false + } + ChatMessage::ResponseReceived(response) => { + self.messages.push(ChatEntry { + role: ChatRole::Assistant, + content: response, + }); + self.is_loading = false; + false + } + ChatMessage::Error(error) => { + self.messages.push(ChatEntry { + role: ChatRole::System, + content: format!("Error: {}", error), + }); + self.is_loading = false; + false + } + } + } + + /// Build the chat panel view. + pub fn view(&self) -> Element<'_, ChatMessage> { + let mut msg_list = column![].spacing(4); + + for entry in &self.messages { + let (label, color) = match entry.role { + ChatRole::User => ("You", iced::Color::from_rgb(0.4, 0.7, 1.0)), + ChatRole::Assistant => ("AI", iced::Color::from_rgb(0.4, 0.9, 0.4)), + ChatRole::System => ("System", iced::Color::from_rgb(0.9, 0.6, 0.2)), + }; + + let msg_text = text(&entry.content).size(11); + let msg_text = msg_text.style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(color), + }); + + let bubble = container( + column![ + text(label).size(10).font(iced::Font::MONOSPACE), + msg_text, + ] + .spacing(2) + ) + .width(Length::Fill) + .padding(8) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.25, 0.25, 0.25))), + border: iced::Border::default().rounded(6), + ..Default::default() + }); + + msg_list = msg_list.push(bubble); + } + + if self.is_loading { + msg_list = msg_list.push(text("Thinking...").size(11)); + } + + let input_field = text_input("Ask the AI assistant...", &self.input) + .on_input(ChatMessage::InputChanged) + .on_submit(ChatMessage::Send) + .width(Length::Fill); + + let send_btn = button(text("Send").size(11)) + .on_press(ChatMessage::Send) + .padding([6, 12]); + + let clear_btn = button(text("Clear").size(11)) + .on_press(ChatMessage::Clear) + .padding([6, 12]); + + let input_row = row![input_field, send_btn, clear_btn].spacing(4); + + let panel = column![ + text("AI Assistant").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(msg_list).height(Length::Fill), + horizontal_rule(1), + input_row, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.18, 0.18, 0.18))), + border: iced::Border::default().color(iced::Color::from_rgb(0.3, 0.3, 0.3)).width(1), + ..Default::default() + }) + .into() + } +} + +/// Generate a placeholder response based on the user message. +fn generate_placeholder_response(user_msg: &str, engine: &Engine) -> String { + let lower = user_msg.to_lowercase(); + + if lower.contains("help") || lower.contains("what can you do") { + "I can help you with drawing operations. Try asking me to:\n\ + - Draw shapes (rect, circle, line)\n\ + - Change colors\n\ + - Apply filters\n\ + - Manage layers\n\ + - Create procedural art" + .to_string() + } else if lower.contains("draw") || lower.contains("shape") { + let w = engine.canvas_width(); + let h = engine.canvas_height(); + format!( + "I can draw shapes on your {}×{} canvas. \ + Try: 'draw a red rectangle' or 'draw a blue circle'", + w, h + ) + } else if lower.contains("layer") { + let count = engine.get_layer_count(); + format!( + "You currently have {} layers. \ + I can help you add, delete, or manage layers.", + count + ) + } else { + format!( + "I understand you're asking about: '{}'. \ + I'm a basic assistant — try asking about drawing, layers, or filters.", + user_msg + ) + } +} diff --git a/hcie-iced-app/crates/iced-panel-script/Cargo.toml b/hcie-iced-app/crates/iced-panel-script/Cargo.toml new file mode 100644 index 0000000..a8be9c2 --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-script/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "iced-panel-script" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +hcie-engine-api = { path = "../../../hcie-engine-api" } +iced = { workspace = true } +serde_json = { workspace = true } +log = { workspace = true } diff --git a/hcie-iced-app/crates/iced-panel-script/src/lib.rs b/hcie-iced-app/crates/iced-panel-script/src/lib.rs new file mode 100644 index 0000000..a12931f --- /dev/null +++ b/hcie-iced-app/crates/iced-panel-script/src/lib.rs @@ -0,0 +1,274 @@ +//! Iced Script Panel — procedural drawing DSL editor. +//! +//! Provides a text editor for the HCIE-Script DSL with +//! execution and error display. + +use hcie_engine_api::Engine; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input}; +use iced::{Element, Length}; + +/// Message type for the script panel. +#[derive(Debug, Clone)] +pub enum ScriptMessage { + /// Script text changed. + TextChanged(String), + /// Execute the current script. + Run, + /// Clear the script editor. + Clear, + /// Script execution result. + ExecutionResult(String), +} + +/// Script panel state. +pub struct ScriptPanel { + /// Current script text. + pub script_text: String, + /// Last execution result/error. + pub result: Option, + /// Whether the script is currently executing. + pub is_running: bool, +} + +impl Default for ScriptPanel { + fn default() -> Self { + Self { + script_text: String::new(), + result: None, + is_running: false, + } + } +} + +impl ScriptPanel { + /// Handle a script message. + pub fn update(&mut self, msg: ScriptMessage, engine: &mut Engine) -> bool { + match msg { + ScriptMessage::TextChanged(text) => { + self.script_text = text; + false + } + ScriptMessage::Run => { + self.is_running = true; + self.result = None; + // Parse and execute the script + match execute_dsl_script(engine, &self.script_text) { + Ok(msg) => { + self.result = Some(msg); + self.is_running = false; + true // composite changed + } + Err(e) => { + self.result = Some(format!("Error: {}", e)); + self.is_running = false; + false + } + } + } + ScriptMessage::Clear => { + self.script_text.clear(); + self.result = None; + false + } + ScriptMessage::ExecutionResult(msg) => { + self.result = Some(msg); + self.is_running = false; + false + } + } + } + + /// Build the script panel view. + pub fn view(&self) -> Element<'_, ScriptMessage> { + let editor = text_input("Enter DSL script...", &self.script_text) + .on_input(ScriptMessage::TextChanged) + .width(Length::Fill); + + let run_btn = button(text("Run").size(11)) + .on_press(ScriptMessage::Run) + .padding([6, 12]); + + let clear_btn = button(text("Clear").size(11)) + .on_press(ScriptMessage::Clear) + .padding([6, 12]); + + let buttons = row![run_btn, clear_btn].spacing(8); + + let result_text = match &self.result { + Some(r) => text(r.as_str()).size(11), + None => text("").size(11), + }; + + let panel = column![ + text("Script").size(13).font(iced::Font::MONOSPACE), + horizontal_rule(1), + scrollable(editor).height(Length::Fill), + horizontal_rule(1), + buttons, + text("Output:").size(10), + result_text, + ] + .spacing(4) + .padding(8); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgb(0.18, 0.18, 0.18))), + border: iced::Border::default().color(iced::Color::from_rgb(0.3, 0.3, 0.3)).width(1), + ..Default::default() + }) + .into() + } +} + +/// Execute a DSL script string. +fn execute_dsl_script(engine: &mut Engine, script: &str) -> Result { + let mut lines_executed = 0; + + for line in script.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + let cmd = parts[0].to_lowercase(); + let args = parts.get(1).unwrap_or(&""); + + match cmd.as_str() { + "layer" => { + let name = if args.is_empty() { "Script Layer" } else { args }; + engine.add_layer(name); + lines_executed += 1; + } + "select" => { + if let Ok(id) = args.parse::() { + engine.set_active_layer(id); + lines_executed += 1; + } + } + "color" => { + if let Some(color) = parse_color(args) { + engine.set_color(color); + lines_executed += 1; + } + } + "size" => { + if let Ok(size) = args.parse::() { + // Create a new tip with the desired size + let mut tip = hcie_engine_api::BrushTip::default(); + tip.size = size; + engine.set_brush_tip(tip); + lines_executed += 1; + } + } + "opacity" => { + if let Ok(op) = args.parse::() { + let mut tip = hcie_engine_api::BrushTip::default(); + tip.opacity = op; + engine.set_brush_tip(tip); + lines_executed += 1; + } + } + "line" => { + let coords: Vec<&str> = args.split(',').collect(); + if coords.len() == 4 { + if let (Ok(x1), Ok(y1), Ok(x2), Ok(y2)) = ( + coords[0].trim().parse::(), + coords[1].trim().parse::(), + coords[2].trim().parse::(), + coords[3].trim().parse::(), + ) { + engine.draw_line(x1, y1, x2, y2); + lines_executed += 1; + } + } + } + "rect" => { + let coords: Vec<&str> = args.split(',').collect(); + if coords.len() == 4 { + if let (Ok(x1), Ok(y1), Ok(x2), Ok(y2)) = ( + coords[0].trim().parse::(), + coords[1].trim().parse::(), + coords[2].trim().parse::(), + coords[3].trim().parse::(), + ) { + engine.draw_rect(x1, y1, x2, y2); + lines_executed += 1; + } + } + } + "circle" => { + let coords: Vec<&str> = args.split(',').collect(); + if coords.len() == 3 { + if let (Ok(cx), Ok(cy), Ok(r)) = ( + coords[0].trim().parse::(), + coords[1].trim().parse::(), + coords[2].trim().parse::(), + ) { + engine.draw_ellipse(cx, cy, r, r); + lines_executed += 1; + } + } + } + "fill" => { + let coords: Vec<&str> = args.split(',').collect(); + if coords.len() >= 2 { + if let (Ok(x), Ok(y)) = ( + coords[0].trim().parse::(), + coords[1].trim().parse::(), + ) { + let color = if coords.len() >= 5 { + [ + coords[2].trim().parse::().unwrap_or(0), + coords[3].trim().parse::().unwrap_or(0), + coords[4].trim().parse::().unwrap_or(0), + 255, + ] + } else { + [0, 0, 0, 255] + }; + engine.flood_fill(x, y, color, 32); + lines_executed += 1; + } + } + } + _ => { + log::warn!("Unknown DSL command: {}", cmd); + } + } + } + + Ok(format!("Executed {} commands", lines_executed)) +} + +/// Parse a color string like "255,0,0" or "red". +fn parse_color(s: &str) -> Option<[u8; 4]> { + let s = s.trim().to_lowercase(); + match s.as_str() { + "red" => Some([255, 0, 0, 255]), + "green" => Some([0, 255, 0, 255]), + "blue" => Some([0, 0, 255, 255]), + "black" => Some([0, 0, 0, 255]), + "white" => Some([255, 255, 255, 255]), + "yellow" => Some([255, 255, 0, 255]), + "cyan" => Some([0, 255, 255, 255]), + "magenta" => Some([255, 0, 255, 255]), + _ => { + let parts: Vec<&str> = s.split(',').collect(); + if parts.len() >= 3 { + if let (Ok(r), Ok(g), Ok(b)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + parts[2].trim().parse::(), + ) { + let a = parts.get(3).and_then(|a| a.trim().parse::().ok()).unwrap_or(255); + return Some([r, g, b, a]); + } + } + None + } + } +} diff --git a/line_count_report.txt b/line_count_report.txt index 6cf2093..d8312ee 100644 --- a/line_count_report.txt +++ b/line_count_report.txt @@ -2,7 +2,7 @@ SATIR SAYISI RAPORU Proje: HCIE-Rust v4 Konum: /mnt/extra/00_PROJECTS/hcie-rust-v3.05 -Tarih: 2026-07-09 04:57:39 +Tarih: 2026-07-11 15:12:11 ========================================= ----------------------------------------- @@ -10,17 +10,18 @@ Tarih: 2026-07-09 04:57:39 ----------------------------------------- hcie-ai 2402 satır hcie-blend 530 satır - hcie-brush-engine 3668 satır + hcie-brush-engine 3753 satır hcie-color 85 satır hcie-composite 985 satır hcie-document 666 satır - hcie-draw 502 satır - hcie-egui-app 35426 satır - hcie-engine-api 5401 satır + hcie-draw 517 satır + hcie-egui-app 36320 satır + hcie-engine-api 5766 satır hcie-engine-api-orig 3874 satır hcie-filter 3238 satır hcie-fx 4077 satır hcie-history 100 satır + hcie-iced-app 4261 satır hcie-io 11382 satır hcie-kra 1392 satır hcie-native 153 satır @@ -33,7 +34,7 @@ Tarih: 2026-07-09 04:57:39 hcie-vector 2298 satır hcie-vision 3131 satır - TOPLAM RUST: 90234 satır + TOPLAM RUST: 95854 satır ----------------------------------------- C++ / HEADER (.cpp, .h) @@ -84,10 +85,10 @@ Tarih: 2026-07-09 04:57:39 ----------------------------------------- SHELL SCRIPT (.sh) ----------------------------------------- - (kök dizin) 3035 satır + (kök dizin) 1225 satır logs/ 220 satır - TOPLAM SHELL: 3255 satır + TOPLAM SHELL: 1445 satır ----------------------------------------- DOKÜMANTASYON / VERİ DOSYALARI @@ -95,20 +96,20 @@ Tarih: 2026-07-09 04:57:39 JSON (.json) -------------------------------- - (kök dizin) 6517 satır + (kök dizin) 34587 satır - TOPLAM JSON: 6517 satır + TOPLAM JSON: 34587 satır MARKDOWN (.md) -------------------------------- - (kök dizin) 11605 satır + (kök dizin) 10777 satır - TOPLAM MARKDOWN: 11605 satır + TOPLAM MARKDOWN: 10777 satır ========================================= KOD SATIRLARI TOPLAMI (GRAND TOTAL) ========================================= - Rust: 90234 satır + Rust: 95854 satır C++ / Header: 5031 satır JavaScript: 0 satır Svelte: 0 satır @@ -116,11 +117,11 @@ Tarih: 2026-07-09 04:57:39 Python: 2267 satır HTML: 0 satır CSS: 0 satır - Shell Script: 3255 satır + Shell Script: 1445 satır ----------------------------------------- - KOD TOPLAMI: 100787 satır + KOD TOPLAMI: 104597 satır - RUST + C++/H TOPLAMI: 95265 satır + RUST + C++/H TOPLAMI: 100885 satır (JSON ve Markdown dosyaları yukarıda ayrı olarak gösterilmiştir) =========================================