Add Iced panel adapter and AI chat/script panels

- Implemented a theme system in `iced-panel-adapter` for consistent styling across panels.
- Created `iced-panel-ai-chat` for an AI assistant interface, including message handling and UI components.
- Developed `iced-panel-script` for a DSL editor with execution capabilities, allowing procedural drawing commands.
- Introduced message types for interaction in both chat and script panels.
- Added color and theme management for improved visual consistency.
This commit is contained in:
2026-07-11 18:13:03 +03:00
parent 0d1b0eae4c
commit f4ad22b55b
113 changed files with 8604 additions and 141 deletions
+1
View File
@@ -0,0 +1 @@
{"pid":373799,"startedAt":1783749172698}
@@ -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
@@ -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<usize>` 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
Generated
+1659 -125
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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"
+8
View File
@@ -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" }
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff" id="svg1">
<g transform="translate(40.96,40.96) scale(13.440000) translate(-0.00,-0.00)">
<g id="layer1" transform="matrix(0.85769537,0,0,0.86353624,2.1241392,1.9256237)">
<path style="fill-opacity: 1; stroke: currentColor; stroke-width: 2.40; stroke-dasharray: none; stroke-opacity: 1; paint-order: normal; stroke-linejoin: miter; stroke-linecap: round; fill: none;" d="M 9.9117002,21.429319 C 9.9897002,21.083319 9.695,20.016 9.828,19.562 c 0.363,-1.233 0.964,-2.808 1.907,-4.02 3.879,-4.979 11.494,-11.209 17.316,-14.298 0.368,-0.195 0.908,-0.082 1.327,0.278 0.421,0.361 0.621,0.885 0.491,1.286 -2.197,6.806 -7.654,15.855 -11.991,20.352 -1.217,1.257 -2.679247,1.413078 -4.408277,2.223973" transform="matrix(0.91733221,0,0,0.92858849,0.0902633,2.1896622)" id="path3"/>
<path style="fill-opacity: 1; stroke: currentColor; stroke-width: 2.40; stroke-linecap: round; stroke-dasharray: none; stroke-opacity: 1; paint-order: normal; fill: none;" d="m 14.174,25.656 c -0.440868,0.206762 0.05243,1.126067 -0.056,1.796 -0.21657,1.338067 -0.961573,2.946306 -3.168,3.151 C 8.8123292,30.801315 6.52319,30.546738 4.212,29.887 3.7801793,29.763735 3.3523883,29.600932 2.926,29.417 2.5363642,29.248922 2.1559447,29.024383 1.816,28.74 1.5869232,28.548365 1.3996288,28.313692 1.268,28.061 1.1006255,27.739686 1.0516667,27.404166 1.125,27.12 1.251667,26.629165 1.6914367,26.414163 1.986,26.307 2.78544,26.016162 3.2079624,25.544434 3.614,24.829 3.7709638,24.552431 3.918,24.257 4.085,23.918 l 0.19,-0.384 C 4.52,23.04 4.808,22.49 5.188,21.906 6.098,20.504 7.0344495,20.270595 8.2484495,20.509595 8.5284495,20.565595 13.935,25.573 14.174,25.656" transform="matrix(0.91733221,0,0,0.92858849,0.0902633,2.1896622)" id="path1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff" fill="currentColor" id="svg1">
<g transform="translate(40.96,41.17) scale(0.420000) translate(-0.00,-0.00)">
<path d="M996 990q-28 33-68 33t-68-33t-28-79t28-79l36-51V584q0 26-17 43L517 990q-34 33-81 33t-81-33L33 668Q0 634 0 587t33-81l287-286v100q0 53 37.5 90.5T448 448t90.5-37.5T576 320v-76l268 268h52q27 0 45.5 18.5T960 576v205l36 51q28 33 28 79t-28 79zM448.5 384q-26.5 0-45.5-19t-19-45V64q0-27 19-45.5T448.5 0t45 18.5T512 64v256q0 26-18.5 45t-45 19z" id="path1" style="fill-opacity: 1; fill-rule: nonzero; fill: currentColor;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 664 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<circle cx="12" cy="12" r="10" style="stroke-width: 3.01px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 331 B

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g transform="translate(40.96,40.96) scale(13.440000) translate(3.08,2.00)">
<g id="Layer_1" transform="translate(-4.3083, -2.1539)">
<g transform="matrix(1.0769, 0, 0, 1.0769, 0, 0)">
<path d="M20, 4.7L20, 9.1C22.4, 10.5 24, 13.1 24, 16C24, 20.4 20.4, 24 16, 24C11.6, 24 8, 20.4 8, 16C8, 13 9.6, 10.5 12, 9.1L12, 4.7C7.3, 6.3 4, 10.8 4, 16C4, 22.6 9.4, 28 16, 28C22.6, 28 28, 22.6 28, 16C28, 10.8 24.7, 6.3 20, 4.7z" fill="currentColor"/>
</g>
</g>
<g id="Layer_1" transform="translate(-4.3083, -2.1539)">
<g transform="matrix(1.0769, 0, 0, 1.0769, 0, 0)">
<path d="M17, 14L15, 14C14.4, 14 14, 13.6 14, 13L14, 3C14, 2.4 14.4, 2 15, 2L17, 2C17.6, 2 18, 2.4 18, 3L18, 13C18, 13.6 17.6, 14 17, 14z" fill="currentColor"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 904 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12 19 6.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 331 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M17 15h2V7c0-1.1-.9-2-2-2H9v2h8v8zM7 17V1H5v4H1v2h4v10c0 1.1.9 2 2 2h10v4h2v-4h4v-2H7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 334 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M15.1 4.3c-.8-.8-2-.8-2.8 0L3.4 13.2c-.8.8-.8 2 0 2.8L7 19.6c.4.4.9.6 1.4.6h7.5c.6 0 1-.4 1-1s-.4-1-1-1H11l8.6-8.6-4.5-5.3zM8.4 18l-3.6-3.6 8.7-8.7 3.6 3.6L8.4 18z"/>
<path d="M21 20H12c-.6 0-1 .4-1 1s.4 1 1 1h9c.6 0 1-.4 1-1s-.4-1-1-1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 488 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff" id="svg1">
<g transform="translate(40.96,40.96) scale(7.680000) translate(-0.00,-0.00)">
<path d="m39.6485 28.9024.6563-.7266c1.1484-1.1953 1.1953-2.6016-.0235-3.8203l-.7031-.6797c3.5859-3.2109 7.5703-3.6563 10.0313-6.1641 3.492-3.5156 2.3438-8.4375-.0936-10.8984-2.4379-2.4844-7.3127-3.5391-10.8987-.0938-2.5312 2.4376-2.9531 6.4454-6.164 10.0313l-.6797-.7032c-1.2187-1.2187-2.625-1.1718-3.8203-.0234l-.7266.6563c-1.4297 1.3828-1.1718 2.6015.0703 3.8437l.9844.9844-17.6953 17.7188c-7.2422 7.2421-3.75 6.1171-7.6875 11.6718l2.086 2.2266c5.3905-3.9141 4.664-.0703 12-7.4063l17.8124-17.6953 1.0079 1.0078c1.2421 1.2422 2.4609 1.5 3.8437.0704zm-29.5313 17.2265c-.8671-.9375-.7031-1.8281.2344-2.7656l19.9453-20.0391 2.5547 2.5547-20.039 20.0156c-.8203.8438-1.8985 1.1016-2.6954.2344z" id="path1" style="stroke-width: 2.34;fill: currentColor;fill-opacity:1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 426 B

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="white" stop-opacity="1"/>
<stop offset="100%" stop-color="white" stop-opacity="0.1"/>
</linearGradient>
</defs>
<rect x="3" y="7" width="18" height="10" rx="1" fill="url(#g)" stroke="currentColor" stroke-width="1.80"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 571 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M4 12L12 4M8 16L16 8M12 20L20 12"/>
<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="2 3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 401 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 3v12M12 15l-4-4M12 15l4-4"/>
<path d="M4 17v2c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 404 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 3C7 3 3 7 3 12c0 5 4 9 9 9 5 0 9-4 9-9"/>
<path d="M12 3v8M12 11L9 8M12 11l3-3"/>
<circle cx="19" cy="5" r="2.5" fill="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 451 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linecap="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 4C7 4 3 7.6 3 12c0 2.5 1.3 4.7 3.3 6.2C6.6 19.4 9.1 21 12 21"/>
<path d="M12 4c2.8 0 5 3.6 5 8s-2.2 8-5 8"/>
<ellipse cx="12" cy="12" rx="5" ry="8" stroke-dasharray="3 2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 486 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linecap="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<line x1="4" y1="20" x2="20" y2="4" style="paint-order: stroke; stroke-width: 3.01px;"/>
<circle cx="4" cy="20" r="1.116" fill="currentColor" style="paint-order: stroke; stroke-width: 3.01px;"/>
<circle cx="20" cy="4" r="1.116" fill="currentColor" style="paint-order: stroke; stroke-width: 3.01px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 600 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M18 10h-1V7c0-2.8-2.2-5-5-5S7 4.2 7 7v3H6c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V7c0-1.7 1.4-3.1 3.1-3.1s3.1 1.4 3.1 3.1v3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 447 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" fill="currentColor" color="#fff" viewBox="0 0 512 512" id="svg1">
<g transform="translate(40.96,40.96) scale(13.440000) translate(-0.00,-0.00)">
<path d="m 18.632458,7.197339 a 0.8418775,0.84971561 0 1 0 1.683755,0 V 4.0890806 a 0.8418775,0.84971561 0 0 0 -1.683755,0 z m 7.576894,0.05948 A 0.84187713,0.84971524 0 0 0 25.018937,6.0553216 l -2.177094,2.1973637 a 0.84187713,0.84971524 0 1 0 1.190415,1.2014974 z M 14.916413,9.4541827 A 0.84187713,0.84971524 0 1 0 16.106827,8.2526853 L 13.929732,6.0553216 a 0.84187713,0.84971524 0 0 0 -1.190415,1.2014975 z m -1.045611,4.2485763 a 0.84187729,0.8497154 0 1 0 0,-1.69943 h -3.079588 a 0.84187729,0.8497154 0 1 0 0,1.69943 z m 14.286654,0 a 0.84187729,0.8497154 0 1 0 0,-1.69943 h -3.079587 a 0.84187729,0.8497154 0 0 0 0,1.69943 z m -3.138519,5.948007 a 0.84187713,0.84971524 0 1 0 1.190415,-1.201497 l -2.177094,-2.197365 a 0.84187713,0.84971524 0 1 0 -1.190415,1.201497 z m -6.386479,1.966241 a 0.8418775,0.84971561 0 0 0 1.683755,0 v -3.108259 a 0.8418775,0.84971561 0 0 0 -1.683755,0 z m 3.12168,-8.661997 a 0.84187713,0.84971524 0 0 0 0,-1.199799 l -1.192097,-1.203197 a 0.84187713,0.84971524 0 0 0 -1.190415,0 l -2.177095,2.199064 a 0.84187713,0.84971524 0 0 0 0,1.201498 l 1.192099,1.203196 a 0.84187713,0.84971524 0 0 0 1.190414,0 l 2.177094,-2.197364 z m -5.051263,5.09829 a 0.84187713,0.84971524 0 0 0 0,-1.199797 l -1.192097,-1.203196 a 0.84187713,0.84971524 0 0 0 -1.190414,0 L 3.7244976,26.346521 a 0.84187713,0.84971524 0 0 0 0,1.201499 l 1.192098,1.203195 a 0.84187713,0.84971524 0 0 0 1.1904143,0 z" id="path1" style="stroke-width: 1.34;fill: currentColor;fill-opacity:1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 3L9 6H11V11H6V9L3 12L6 15V13H11V18H9L12 21L15 18H13V13H18V15L21 12L18 9V11H13V6H15L12 3Z" stroke="currentColor" stroke-width="1.80" stroke-linejoin="round" fill="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 410 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 283 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 348 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="svg1">
<g transform="translate(40.96,40.96) scale(13.440000) translate(2.00,2.85)">
<g id="Layer_1" transform="translate(0, 0.0196)" style="fill: currentColor;fill-opacity:1">
<g transform="matrix(0.875683128833771, 0, 0, 0.875683128833771, 0, 0)" id="g1" style="fill: currentColor;fill-opacity:1">
<path d="M31.1, 8.3C30, 7.6 28.6, 8.1 28.2, 9.2L26.3, 13.3C26.1, 13.7 25.7, 13.9 25.3, 13.9L25.3, 13.9C24.6, 13.9 24.1, 13.3 24.3, 12.6L26, 4.4C26.2, 3.3 25.6, 2.3 24.5, 2C23.4, 1.8 22.4, 2.4 22.1, 3.5L20.2, 11.1C20.1, 11.6 19.7, 11.9 19.2, 11.9L19.1, 11.9C18.5, 11.9 18, 11.4 18, 10.8L18, 2.1C18, 1.1 17.3, 0.2 16.4, 0C15.1, -0.2 14, 0.8 14, 2L14, 10.9C14, 11.5 13.5, 12 12.9, 12L12.8, 12C12.3, 12 11.9, 11.7 11.8, 11.2L10, 3.6C9.7, 2.4 8.4, 1.7 7.3, 2.2C6.3, 2.5 5.9, 3.6 6.1, 4.6L8.6, 15.9C8.8, 16.7 7.9, 17.4 7.1, 17L3.1, 14.4C2.3, 13.9 1.3, 14 0.6, 14.7C-0.2, 15.5 -0.2, 16.8 0.6, 17.6L10.9, 28C12.1, 29.3 13.9, 30 15.8, 30L20, 30C22.9, 30 24.7, 28 25.9, 25.2L31.8, 10.9C32.2, 10 31.9, 8.9 31.1, 8.3z" id="path1" style="fill-opacity: 1; fill: none; stroke: rgb(255, 255, 255); stroke-width: 2.12419px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>.cls-1{fill:#4285f4;}.cls-1,.cls-2,.cls-4{fill-rule:evenodd;}.cls-2{fill:#1e88e5;}.cls-3,.cls-4{fill:#aecbfa;}.cls-5{fill:#669df6;}</style>
</defs>
<title>Icon_24px_AIHub_Color</title>
<g data-name="Product Icons">
<polygon class="cls-1" points="20.69 12.43 12 16.78 12 22 20.69 17.65 20.69 12.43"/>
<path class="cls-2" d="M6.78,4.61,3.31,6.35Z"/>
<polygon class="cls-1" points="17.22 8.09 17.22 8.09 17.22 12.43 20.69 10.7 20.69 6.35 17.22 8.09"/>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(.3 0 0 .3 -.23126488 -.2)">
<path fill="#ffc107" d="M24.639 4c-8.6 0-18.43 4.954-18.43 18.947C6.209 33.108 13.842 44 25.519 44h.175c4.385-.079 9.105-2.918 9.105-8.959 0-1.157-.431-2.159-.809-3.042-.102-.233-.199-.462-.288-.694-.864-2.175.089-2.971 2.329-4.565 2.537-1.806 6.013-4.278 5.968-10.819C41.999 12.032 37.756 4 24.639 4zm.56 35.2c-2.241 0-4.001-1.76-4.001-3.999 0-2.241 1.76-4.001 4.001-4.001 2.238 0 3.998 1.76 3.998 4.001 0 2.239-1.759 3.999-3.998 3.999z"/>
<circle cx="34.5" cy="16.5" r="3.5" fill="#9c27b0"/>

After

Width:  |  Height:  |  Size: 980 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" fill="currentColor" fill-opacity="0.01"/>
<path d="M24 40.9443C26.123 42.8445 28.9266 43.9999 32 43.9999C38.6274 43.9999 44 38.6273 44 31.9999C44 26.4084 40.1757 21.7101 35 20.3779" stroke-width="4" stroke-linejoin="round" style="stroke: rgb(255, 255, 255); fill: rgb(148, 41, 41); paint-order: fill; fill-rule: nonzero;"/>
<path d="M13 20.3779C7.82432 21.7101 4 26.4084 4 31.9999C4 38.6273 9.37258 43.9999 16 43.9999C22.6274 43.9999 28 38.6273 28 31.9999C28 30.4504 27.7063 28.9696 27.1716 27.6099" stroke-width="4" stroke-linejoin="round" style="stroke: rgb(255, 255, 255); fill: rgb(115, 197, 102);"/>
<path d="M24 28C30.6274 28 36 22.6274 36 16C36 9.37258 30.6274 4 24 4C17.3726 4 12 9.37258 12 16C12 22.6274 17.3726 28 24 28Z" fill="#2F88FF" stroke-width="4" stroke-linejoin="round" style="stroke: rgb(255, 255, 255);"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7q0 1.21875-.47656 2.328125t-1.28125 1.914062q-.80469.804688-1.91406 1.28125Q8.21875 13 7 13q-1.34375 0-2.55469-.566406-1.21093-.566406-2.0625-1.597656-.0547-.07813-.0508-.175782.004-.09766.0664-.160156l1.07031-1.078125q.0781-.07031.19531-.07031.125.01562.17969.09375.57031.742187 1.39844 1.148437Q6.07031 11 7 11q.8125 0 1.55078-.316406t1.27734-.855469q.53907-.539063.85547-1.277344Q11 7.8125 11 7t-.31641-1.550781q-.3164-.738281-.85547-1.277344-.53906-.539063-1.27734-.855469Q7.8125 3 7 3q-.76563 0-1.46875.277344-.70313.277344-1.25.792968l1.07031 1.078126q.24219.234375.10938.539062Q5.32812 6 5 6H1.5q-.203125 0-.351563-.148438Q1 5.703125 1 5.5V2q0-.328125.3125-.460937.304688-.132813.539062.109374L2.86719 2.65625q.83593-.789063 1.91015-1.222656Q5.85156 1 7 1q1.21875 0 2.32813.476562 1.10937.476563 1.91406 1.281251.80469.804687 1.28125 1.914062T13 7z" style="fill: rgb(0, 104, 128);"/>
<path fill="gray" d="M8 4.75v3.5q0 .109375-.0703.179688Q7.85938 8.5 7.75 8.5h-2.5q-.10938 0-.17969-.07031Q5 8.359375 5 8.25v-.5q0-.109375.0703-.179687Q5.14062 7.5 5.25 7.5H7V4.75q0-.109375.0703-.179687Q7.14063 4.5 7.25 4.5h.5q.10938 0 .17969.07031Q8 4.640625 8 4.75z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.97883 9.68508C2.99294 8.89073 2 8.49355 2 8C2 7.50645 2.99294 7.10927 4.97883 6.31492L7.7873 5.19153C9.77318 4.39718 10.7661 4 12 4C13.2339 4 14.2268 4.39718 16.2127 5.19153L19.0212 6.31492C21.0071 7.10927 22 7.50645 22 8C22 8.49355 21.0071 8.89073 19.0212 9.68508L16.2127 10.8085C14.2268 11.6028 13.2339 12 12 12C10.7661 12 9.77318 11.6028 7.7873 10.8085L4.97883 9.68508Z" fill="#1C274C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 8C2 8.49355 2.99294 8.89073 4.97883 9.68508L7.7873 10.8085C9.77318 11.6028 10.7661 12 12 12C13.2339 12 14.2268 11.6028 16.2127 10.8085L19.0212 9.68508C21.0071 8.89073 22 8.49355 22 8C22 7.50645 21.0071 7.10927 19.0212 6.31492L16.2127 5.19153C14.2268 4.39718 13.2339 4 12 4C10.7661 4 9.77318 4.39718 7.7873 5.19153L4.97883 6.31492C2.99294 7.10927 2 7.50645 2 8Z" style="fill: rgb(69, 86, 143); stroke: rgb(255, 255, 255);"/>
<path d="M19.0212 13.6851L16.2127 14.8085C14.2268 15.6028 13.2339 16 12 16C10.7661 16 9.77318 15.6028 7.7873 14.8085L4.97883 13.6851C2.99294 12.8907 2 12.4935 2 12C2 11.5551 2.80681 11.1885 4.42043 10.5388L7.56143 11.7952C9.41007 12.535 10.572 13 12 13C13.428 13 14.5899 12.535 16.4386 11.7952L19.5796 10.5388C21.1932 11.1885 22 11.5551 22 12C22 12.4935 21.0071 12.8907 19.0212 13.6851Z" style="fill: rgb(93, 105, 142); stroke: rgb(255, 255, 255);"/>
<path d="M19.0212 17.6849L16.2127 18.8083C14.2268 19.6026 13.2339 19.9998 12 19.9998C10.7661 19.9998 9.77318 19.6026 7.7873 18.8083L4.97883 17.6849C2.99294 16.8905 2 16.4934 2 15.9998C2 15.5549 2.80681 15.1883 4.42043 14.5386L7.56143 15.795C9.41007 16.5348 10.572 16.9998 12 16.9998C13.428 16.9998 14.5899 16.5348 16.4386 15.795L19.5796 14.5386C21.1932 15.1883 22 15.5549 22 15.9998C22 16.4934 21.0071 16.8905 19.0212 17.6849Z" style="fill: rgb(120, 124, 137); stroke: rgb(255, 255, 255);"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M974.4 332.8c31.2-31.2 16.8-96-14.4-127.2L818.4 64c-31.2-31.2-96-45.6-127.2-14.4L624 116 908 400l66.4-67.2z" fill="#4DA796" /><path d="M451.2 856.8L167.2 572.8l407.2-406.4 180.8 114.4 102.4 168.8z" fill="#EC7BB0" /><path d="M167.2 572.8l0.8-0.8c44-17.6 88 26.4 70.4 70.4l-0.8 0.8c80.8-6.4 148.8 60.8 141.6 141.6l2.4-0.8c44.8-16.8 87.2 28 68.8 72L47.2 991.2c-9.6 3.2-18.4-5.6-15.2-15.2l135.2-403.2z" fill="#EFD1AF" /><path d="M624 116l55.2-55.2 1.6-0.8c43.2-12.8 83.2 27.2 70.4 70.4l-0.8 1.6c77.6 2.4 139.2 64.8 141.6 141.6l2.4-0.8c43.2-12 82.4 28.8 68 72l-55.2 55.2-174.4-125.6L624 116z" fill="#4DA796" /><path d="M623.624 116.144l283.968 283.968-50.344 50.352L573.28 166.48z" fill="#FFFEFE" /><path d="M38.4 990.4c-2.4-0.8-4-3.2-5.6-5.6l204.8-341.6 512-512 0.8-1.6c12.8-43.2-27.2-83.2-70.4-70.4l-1.6 0.8-72 72-8 8-432 432-133.6 404.8c-3.2 8.8 5.6 13.6 5.6 13.6z" fill="#FDFDFC" /><path d="M895.2 272.8l-2.4 0.8-512 512-342.4 204.8c2.4 1.6 5.6 1.6 8.8 0.8l77.6-25.6 325.6-108.8L908 400l16.8-16.8 38.4-38.4c14.4-43.2-24.8-84-68-72z" fill="#0D1014" /><path d="M908 408c-2.4 0-4-0.8-5.6-2.4L618.4 121.6c-1.6-1.6-2.4-3.2-2.4-5.6s0.8-4 2.4-5.6l66.4-66.4c13.6-13.6 32-20 53.6-20 31.2 0 64.8 13.6 85.6 34.4L965.6 200c16.8 16.8 28.8 42.4 32.8 68 4.8 29.6-2.4 54.4-18.4 70.4l-66.4 66.4c-1.6 2.4-4 3.2-5.6 3.2zM635.2 116l272.8 272.8 60.8-60.8c12-12 17.6-32.8 13.6-56.8-3.2-22.4-14.4-44.8-28-59.2L812.8 69.6c-17.6-17.6-48-29.6-74.4-29.6-12 0-29.6 2.4-41.6 15.2l-61.6 60.8zM167.2 580.8c-2.4 0-4-0.8-5.6-2.4-3.2-3.2-3.2-8 0-11.2L568 160.8c3.2-3.2 8-3.2 11.2 0s3.2 8 0 11.2L172.8 578.4c-1.6 1.6-4 2.4-5.6 2.4zM451.2 864.8c-2.4 0-4-0.8-5.6-2.4-3.2-3.2-3.2-8 0-11.2l407.2-407.2c3.2-3.2 8-3.2 11.2 0s3.2 8 0 11.2l-407.2 407.2c-1.6 1.6-4 2.4-5.6 2.4z" fill="#6A576D" /><path d="M908 408c-2.4 0-4-0.8-5.6-2.4-3.2-3.2-3.2-8 0-11.2l53.6-53.6c4.8-16.8 0.8-33.6-11.2-46.4-12.8-12.8-30.4-18.4-48-12.8l-2.4 0.8c-2.4 0.8-4.8 0-7.2-1.6-1.6-1.6-3.2-4-3.2-6.4-2.4-72.8-60.8-132-134.4-134.4-2.4 0-4.8-1.6-6.4-3.2s-1.6-4.8-0.8-7.2l0.8-1.6c5.6-17.6 0.8-35.2-12-48-12.8-12.8-30.4-16.8-47.2-12l-53.6 53.6c-3.2 3.2-8 3.2-11.2 0s-3.2-8 0-11.2l55.2-55.2c0.8-0.8 2.4-1.6 3.2-1.6l1.6-0.8c23.2-7.2 47.2-0.8 64 16 15.2 15.2 21.6 35.2 17.6 56 73.6 7.2 132 65.6 139.2 139.2 20.8-3.2 41.6 3.2 56.8 19.2 16.8 17.6 22.4 41.6 14.4 64.8 0 0.8-0.8 2.4-1.6 3.2l-55.2 55.2c-2.4 0.8-4.8 1.6-6.4 1.6z" fill="#6A576D" /><path d="M857.6 457.6c-2.4 0-4-0.8-5.6-2.4L568 172c-3.2-3.2-3.2-8 0-11.2l50.4-50.4c3.2-3.2 8-3.2 11.2 0l284 284c3.2 3.2 3.2 8 0 11.2l-50.4 50.4c-1.6 0.8-3.2 1.6-5.6 1.6z m-272-291.2l272.8 272.8 38.4-38.4L624 127.2l-38.4 39.2zM44 1000c-6.4 0-12-3.2-16-8-4-5.6-4.8-12-2.4-17.6L160 571.2c0.8-2.4 2.4-4 4.8-4.8l0.8-0.8c8-3.2 15.2-4.8 23.2-4.8 20.8 0 40 10.4 51.2 28 9.6 14.4 12.8 31.2 8.8 47.2 38.4 0 76 16.8 102.4 44.8 24 26.4 36.8 59.2 36.8 94.4 24.8-5.6 50.4 5.6 64.8 26.4 11.2 17.6 13.6 39.2 4.8 58.4-0.8 2.4-2.4 4-4.8 4.8L50.4 999.2c-2.4 0.8-4.8 0.8-6.4 0.8z m129.6-420.8L40 979.2c-0.8 1.6 0 2.4 0.8 3.2 0.8 1.6 2.4 1.6 4 1.6l400-133.6c4.8-13.6 3.2-28-4.8-40-12-17.6-34.4-25.6-55.2-17.6l-2.4 0.8c-2.4 0.8-5.6 0.8-8-0.8-2.4-1.6-3.2-4-3.2-7.2 3.2-35.2-8.8-68-32-93.6-25.6-28-62.4-42.4-100.8-39.2-2.4 0-5.6-0.8-7.2-3.2-1.6-2.4-2.4-4.8-0.8-8l0.8-0.8c5.6-14.4 4-30.4-4-42.4-12-18.4-33.6-26.4-53.6-19.2z" fill="#6A576D" /><path d="M58.4 898.4L32.8 976c-3.2 9.6 5.6 18.4 15.2 15.2l77.6-25.6-67.2-67.2z" fill="#6A576D" /></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>plugin_2_line</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Device" transform="translate(-336.000000, -192.000000)">
<g id="plugin_2_line" transform="translate(336.000000, 192.000000)">
<path d="M24,0 L24,24 L0,24 L0,0 L24,0 Z M12.5934901,23.257841 L12.5819402,23.2595131 L12.5108777,23.2950439 L12.4918791,23.2987469 L12.4918791,23.2987469 L12.4767152,23.2950439 L12.4056548,23.2595131 C12.3958229,23.2563662 12.3870493,23.2590235 12.3821421,23.2649074 L12.3780323,23.275831 L12.360941,23.7031097 L12.3658947,23.7234994 L12.3769048,23.7357139 L12.4804777,23.8096931 L12.4953491,23.8136134 L12.4953491,23.8136134 L12.5071152,23.8096931 L12.6106902,23.7357139 L12.6232938,23.7196733 L12.6232938,23.7196733 L12.6266527,23.7031097 L12.609561,23.275831 C12.6075724,23.2657013 12.6010112,23.2592993 12.5934901,23.257841 L12.5934901,23.257841 Z M12.8583906,23.1452862 L12.8445485,23.1473072 L12.6598443,23.2396597 L12.6498822,23.2499052 L12.6498822,23.2499052 L12.6471943,23.2611114 L12.6650943,23.6906389 L12.6699349,23.7034178 L12.6699349,23.7034178 L12.678386,23.7104931 L12.8793402,23.8032389 C12.8914285,23.8068999 12.9022333,23.8029875 12.9078286,23.7952264 L12.9118235,23.7811639 L12.8776777,23.1665331 C12.8752882,23.1545897 12.8674102,23.1470016 12.8583906,23.1452862 L12.8583906,23.1452862 Z M12.1430473,23.1473072 C12.1332178,23.1423925 12.1221763,23.1452606 12.1156365,23.1525954 L12.1099173,23.1665331 L12.0757714,23.7811639 C12.0751323,23.7926639 12.0828099,23.8018602 12.0926481,23.8045676 L12.108256,23.8032389 L12.3092106,23.7104931 L12.3186497,23.7024347 L12.3186497,23.7024347 L12.3225043,23.6906389 L12.340401,23.2611114 L12.337245,23.2485176 L12.337245,23.2485176 L12.3277531,23.2396597 L12.1430473,23.1473072 Z" id="MingCute" fill-rule="nonzero" style=""/>
<path d="M10.5,4 C9.40564,4 8.69387,5.15167 9.18328,6.13049 L9.34565,6.45523 C9.70051,7.16495 9.18442,8 8.39092,8 L5,8 C4.44772,8 4,8.44772 4,9 L4,10.1944 C5.82084,10.0853 7.5,11.5252 7.5,13.5 C7.5,15.4748 5.82084,16.9147 4,16.8056 L4,19 C4,19.5523 4.44772,20 5,20 L7.19435,20 C7.08534,18.1792 8.52516,16.5 10.5,16.5 C12.4748,16.5 13.9147,18.1792 13.8056,20 L15,20 C15.5523,20 16,19.5523 16,19 L16,15.6091 C16,14.8156 16.835,14.2995 17.5448,14.6544 L17.8695,14.8167 C18.8483,15.3061 20,14.5944 20,13.5 C20,12.4056 18.8483,11.6939 17.8695,12.1833 L17.5448,12.3456 C16.835,12.7005 16,12.1844 16,11.3909 L16,9 C16,8.44772 15.5523,8 15,8 L12.6091,8 C11.8156,8 11.2995,7.16495 11.6544,6.45523 L11.8167,6.13049 C12.3061,5.15167 11.5944,4 10.5,4 Z M7.06441,6 C6.74822,3.98274 8.29477,2 10.5,2 C12.7052,2 14.2518,3.98274 13.9356,6 L15,6 C16.6569,6 18,7.34315 18,9 L18,10.0644 C20.0173,9.74822 22,11.2948 22,13.5 C22,15.7052 20.0173,17.2518 18,16.9356 L18,19 C18,20.6569 16.6569,22 15,22 L12.5929,22 C11.831,22 11.3173,21.221 11.6174,20.5207 L11.7021,20.3231 C12.072,19.4601 11.439,18.5 10.5,18.5 C9.56105,18.5 8.92799,19.4601 9.29786,20.3231 L9.38256,20.5207 C9.68268,21.221 9.169,22 8.40712,22 L5,22 C3.34315,22 2,20.6569 2,19 L2,15.5929 C2,14.831 2.779,14.3173 3.47929,14.6174 L3.67691,14.7021 C4.53994,15.072 5.5,14.439 5.5,13.5 C5.5,12.561 4.53994,11.928 3.67691,12.2979 L3.47929,12.3826 C2.779,12.6827 2,12.169 2,11.4071 L2,9 C2,7.34315 3.34315,6 5,6 L7.06441,6 Z" id="形状" style="fill: rgb(255, 255, 255);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M11 3C10.4477 3 10 3.44772 10 4V4.56876C10 4.99658 9.71288 5.36825 9.31776 5.53229C8.9225 5.6964 8.46228 5.63386 8.15966 5.33123L7.75734 4.92891C7.36681 4.53839 6.73365 4.53839 6.34312 4.92891L4.92891 6.34313C4.53838 6.73365 4.53838 7.36681 4.92891 7.75734L5.33123 8.15966C5.63386 8.46229 5.6964 8.9225 5.53229 9.31776C5.36825 9.71288 4.99658 10 4.56877 10L4 10C3.44772 10 3 10.4477 3 11V13C3 13.5523 3.44772 14 4 14H4.56879C4.99659 14 5.36825 14.2871 5.53228 14.6822C5.69638 15.0775 5.63384 15.5377 5.33123 15.8403L4.92889 16.2426C4.53837 16.6331 4.53837 17.2663 4.92889 17.6568L6.34311 19.071C6.73363 19.4616 7.36679 19.4616 7.75732 19.071L8.1596 18.6688C8.46223 18.3661 8.92247 18.3036 9.31774 18.4677C9.71287 18.6317 10 19.0034 10 19.4313V20C10 20.5523 10.4477 21 11 21H13C13.5523 21 14 20.5523 14 20V19.4312C14 19.0034 14.2871 18.6318 14.6822 18.4677C15.0775 18.3036 15.5377 18.3661 15.8403 18.6688L16.2426 19.071C16.6331 19.4616 17.2663 19.4616 17.6568 19.071L19.071 17.6568C19.4616 17.2663 19.4616 16.6331 19.071 16.2426L18.6688 15.8403C18.3661 15.5377 18.3036 15.0775 18.4677 14.6822C18.6318 14.2871 19.0034 14 19.4312 14H20C20.5523 14 21 13.5523 21 13V11C21 10.4477 20.5523 10 20 10L19.4313 10C19.0034 10 18.6317 9.71287 18.4677 9.31774C18.3036 8.92247 18.3661 8.46223 18.6688 8.1596L19.071 7.75734C19.4615 7.36681 19.4616 6.73365 19.071 6.34312L17.6568 4.92891C17.2663 4.53838 16.6331 4.53838 16.2426 4.92891L15.8403 5.33123C15.5377 5.63384 15.0775 5.69638 14.6822 5.53228C14.2871 5.36825 14 4.99659 14 4.56879V4C14 3.44772 13.5523 3 13 3H11ZM12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14Z" fill="#000000"/>
<path d="M11 3H13C13.5523 3 14 3.44772 14 4V4.56879C14 4.99659 14.2871 5.36825 14.6822 5.53228C15.0775 5.69638 15.5377 5.63384 15.8403 5.33123L16.2426 4.92891C16.6331 4.53838 17.2663 4.53838 17.6568 4.92891L19.071 6.34312C19.4616 6.73365 19.4615 7.36681 19.071 7.75734L18.6688 8.1596C18.3661 8.46223 18.3036 8.92247 18.4677 9.31774C18.6317 9.71287 19.0034 10 19.4313 10L20 10C20.5523 10 21 10.4477 21 11V13C21 13.5523 20.5523 14 20 14H19.4312C19.0034 14 18.6318 14.2871 18.4677 14.6822C18.3036 15.0775 18.3661 15.5377 18.6688 15.8403L19.071 16.2426C19.4616 16.6331 19.4616 17.2663 19.071 17.6568L17.6568 19.071C17.2663 19.4616 16.6331 19.4616 16.2426 19.071L15.8403 18.6688C15.5377 18.3661 15.0775 18.3036 14.6822 18.4677C14.2871 18.6318 14 19.0034 14 19.4312V20C14 20.5523 13.5523 21 13 21H11C10.4477 21 10 20.5523 10 20V19.4313C10 19.0034 9.71287 18.6317 9.31774 18.4677C8.92247 18.3036 8.46223 18.3661 8.1596 18.6688L7.75732 19.071C7.36679 19.4616 6.73363 19.4616 6.34311 19.071L4.92889 17.6568C4.53837 17.2663 4.53837 16.6331 4.92889 16.2426L5.33123 15.8403C5.63384 15.5377 5.69638 15.0775 5.53228 14.6822C5.36825 14.2871 4.99659 14 4.56879 14H4C3.44772 14 3 13.5523 3 13V11C3 10.4477 3.44772 10 4 10L4.56877 10C4.99658 10 5.36825 9.71288 5.53229 9.31776C5.6964 8.9225 5.63386 8.46229 5.33123 8.15966L4.92891 7.75734C4.53838 7.36681 4.53838 6.73365 4.92891 6.34313L6.34312 4.92891C6.73365 4.53839 7.36681 4.53839 7.75734 4.92891L8.15966 5.33123C8.46228 5.63386 8.9225 5.6964 9.31776 5.53229C9.71288 5.36825 10 4.99658 10 4.56876V4C10 3.44772 10.4477 3 11 3Z" stroke-width="1.5" style="stroke: rgb(255, 255, 255); fill: rgb(108, 130, 139);"/>
<path d="M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z" stroke="#000000" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<svg fill="currentColor" height="512px" width="512px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff">
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<path d="M371.2,308.364c-4.655-4.655-11.636-4.655-16.291,0L205.964,457.309c-41.891,41.891-109.382,41.891-151.273,0 c-41.891-41.891-41.891-109.382,0-151.273l53.527-53.527l128,128c4.655,4.655,11.636,4.655,16.291,0 c4.655-4.655,4.655-11.636,0-16.291L116.364,228.073c-4.655-4.655-11.636-4.655-16.291,0L38.4,289.745 c-51.2,51.2-51.2,133.818,0,183.855C64,499.2,96.582,512,130.327,512c33.745,0,66.327-12.8,91.927-38.4L371.2,324.655 C375.855,320,375.855,313.018,371.2,308.364z" style="stroke: rgb(255, 255, 255); stroke-width: 3.70633px;"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="232.727" cy="232.727" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="279.273" cy="279.273" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="290.909" cy="221.091" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="244.364" cy="174.545" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="337.455" cy="267.636" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="174.545" cy="244.364" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="267.636" cy="337.455" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<circle cx="221.091" cy="290.909" r="11.636"/>
</g>
</g>
<g style="" transform="matrix(0.809425, 0, 0, 0.809425, 48.464909, 40.529533)">
<g>
<path d="M473.6,38.4c-51.2-51.2-133.818-51.2-183.855,0L140.8,187.345c-4.655,4.655-4.655,11.636,0,16.291s11.636,4.655,16.291,0 L306.036,54.691c41.891-41.891,109.382-41.891,151.273,0c41.891,41.891,41.891,109.382,0,151.273l-53.527,53.527l-128-128 c-4.655-4.655-11.636-4.655-16.291,0c-4.655,4.655-4.655,11.636,0,16.291l136.145,136.145c2.327,2.327,5.818,3.491,8.146,3.491 c3.491,0,5.818-1.164,8.145-3.491l61.673-61.673C524.8,171.055,524.8,88.436,473.6,38.4z" style="paint-order: fill; stroke: rgb(255, 255, 255); stroke-width: 3.70633px;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff" id="svg4">
<g transform="translate(40.96,40.96) scale(13.440000) translate(4.00,4.03)">
<g id="Layer_1" transform="translate(-4.00000228881836, -4.025)">
<g id="Edit">
<path d="M27.6, 8.2L23.8, 4.4C23.3, 3.9 22.4, 3.9 21.9, 4.4L19.4, 6.9L25.2, 12.7L27.7, 10.2C28.1, 9.6 28.1, 8.8 27.6, 8.2z" id="path1" style="stroke: currentColor; stroke-width: 2.40; stroke-opacity: 1; stroke-linecap: round; stroke-linejoin: round; fill-opacity: 1; fill: none;"/>
</g>
</g>
<g id="g2" transform="translate(-4, -4.02499885559082)">
<g id="g1">
<polygon points="4.637 22.51 4 28 10 27.279" id="polygon1" style="stroke: currentColor; stroke-width: 2.40; stroke-opacity: 1; paint-order: normal; stroke-linecap: round; fill: none;" transform="matrix(0.82823736,0,0,0.8677351,1.2851622,3.2198478)"/>
</g>
</g>
<g id="g4" transform="translate(-3.99999988555908, -4.024999559021)" style="paint-order:normal">
<g id="g3" style="paint-order:normal">
<rect x="5.8000002" y="13.4" width="17.6" height="8.1999998" transform="matrix(0.707,-0.7072,0.7072,0.707,-8.0721,15.4048)" id="rect2" style="stroke: currentColor; stroke-width: 2.40; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: none; stroke-opacity: 1; paint-order: normal; fill-opacity: 1; fill: none;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 342 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 409 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.5 15a9 9 0 11-2.1-9.4L23 10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 402 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff">
<g transform="translate(40.96,40.96) scale(13.440000) translate(4.00,4.00)">
<g id="Save" transform="translate(-4, -4)">
<path d="M27, 4L24, 4L24, 14L8, 14L8, 4L5, 4C4.4, 4 4, 4.4 4, 5L4, 27C4, 27.6 4.4, 28 5, 28L27, 28C27.6, 28 28, 27.6 28, 27L28, 5C28, 4.4 27.6, 4 27, 4zM24, 24L8, 24L8, 18L24, 18L24, 24zM10, 4L10, 12L20, 12L20, 4L10, 4zM14, 10L12, 10L12, 6L14, 6L14, 10z" fill="currentColor"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 549 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<circle cx="6" cy="18" r="2"/>
<circle cx="18" cy="18" r="2"/>
<circle cx="12" cy="6" r="2"/>
<path d="M7.5 16.5L10.5 8M13.5 8L16.5 16.5M8 18h8" stroke-dasharray="2 2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<!-- Crosshair cursor -->
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill-opacity="0.3"/>
<line x1="12" y1="6" x2="12" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="14" x2="12" y2="18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="6" y1="12" x2="10" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="14" y1="12" x2="18" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<!-- Sparkle top-right -->
<path d="M20 4l1 3 3 1-3 1-1 3-1-3-3-1 3-1z" fill="currentColor"/>
<!-- Sparkle bottom-left -->
<path d="M5 17l0.7 2 2 0.7-2 0.7-0.7 2-0.7-2-2-0.7 2-0.7z" fill="currentColor" fill-opacity="0.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M14.7 3.3L20.7 9.3M3.3 14.7L9.3 20.7M8.5 8.5L15.5 15.5M15.5 8.5L8.5 15.5"/>
<rect x="4" y="4" width="16" height="16" rx="2" transform="rotate(45 12 12)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 484 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg fill="currentColor" width="512px" height="512px" viewBox="0 0 512 512" color="#fff" xmlns="http://www.w3.org/2000/svg">
<path d="M 192.201 57.514 C 205.187 58.426 199.922 143.122 192.201 142.58 L 220.557 142.58 L 220.557 157.644 C 162.406 163.846 137.263 206.822 137.263 206.822 L 135.491 209.924 L 135.491 454.487 L 333.977 454.487 L 333.977 209.924 L 332.205 206.822 C 332.205 206.822 307.062 163.846 248.912 157.644 L 248.912 142.58 L 273.707 136.603 C 281.89 142.254 286.959 79.217 274.012 70.065 C 266.447 62.313 185.628 50.778 192.201 57.514 Z M 362.333 57.514 C 354.524 57.514 348.156 63.882 348.156 71.691 C 348.156 79.5 354.524 85.87 362.333 85.87 C 370.142 85.87 376.51 79.5 376.51 71.691 C 376.51 63.882 370.142 57.514 362.333 57.514 Z M 220.886 78.36 C 222.325 71.357 261.074 81.168 260.108 85.87 C 264.295 85.87 262.39 114.011 260.425 114.011 C 264.637 122.464 224.027 126.484 220.82 120.047 C 224.421 120.047 225.031 78.36 220.886 78.36 Z M 319.8 85.87 C 311.991 85.87 305.623 92.238 305.623 100.047 C 305.623 107.856 311.991 114.224 319.8 114.224 C 327.609 114.224 333.977 107.856 333.977 100.047 C 333.977 92.238 327.609 85.87 319.8 85.87 Z M 362.333 114.224 C 354.524 114.224 348.156 120.594 348.156 128.403 C 348.156 136.211 354.524 142.58 362.333 142.58 C 370.142 142.58 376.51 136.211 376.51 128.403 C 376.51 120.594 370.142 114.224 362.333 114.224 Z M 234.734 185.113 C 259.379 185.113 275.993 191.924 287.014 199.29 L 182.454 199.29 C 193.475 191.924 210.09 185.113 234.734 185.113 Z M 163.847 227.646 C 160.916 216.964 302.249 215.352 305.623 227.646 C 291.104 228.007 293.824 426.426 305.623 426.132 C 306.669 413.437 164.756 415.101 163.847 426.132 C 173.356 426.522 175.548 228.125 163.847 227.646 Z M 234.734 284.356 C 211.418 284.356 192.201 303.573 192.201 326.889 C 192.201 350.204 211.418 369.422 234.734 369.422 C 258.05 369.422 277.267 350.204 277.267 326.889 C 277.267 303.573 258.05 284.356 234.734 284.356 Z M 234.734 312.711 C 242.71 312.711 248.912 318.914 248.912 326.889 C 248.912 334.863 242.71 341.066 234.734 341.066 C 226.76 341.066 220.557 334.863 220.557 326.889 C 220.557 318.914 226.76 312.711 234.734 312.711 Z" style="fill: currentColor;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6L12 2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 378 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M5 4v3h2V6h4v13H9v2h6v-2h-2V6h4v1h2V4H5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 288 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M2 5v16c0 .6.4 1 1 1h18c.6 0 1-.4 1-1V5c0-.6-.4-1-1-1H3c-.6 0-1 .4-1 1zm2 1h16v9H4V6zM4 17h16v3H4v-3z"/>
<circle cx="18" cy="18.5" r=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 388 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" color="#fff">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M2 5v16c0 .6.4 1 1 1h18c.6 0 1-.4 1-1V5c0-.6-.4-1-1-1H3c-.6 0-1 .4-1 1zm2 1h16v9H4V6zM4 17h16v3H4v-3z"/>
<circle cx="12" cy="18.5" r=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 388 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.5 15a9 9 0 102.1-9.4L1 10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 396 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<line x1="5" y1="19" x2="19" y2="5"/>
<polyline points="10 5 19 5 19 14"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<line x1="12" y1="2" x2="12" y2="22"/>
<polyline points="8,6 12,2 16,6"/>
<polyline points="8,18 12,22 16,18"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<polyline points="6,8 2,12 6,16"/>
<polyline points="18,8 22,12 18,16"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 560 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 347 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M 12 3 C 6.5 3 2 7.5 2 12 C 2 15 3.5 17.5 6 19 L 4.404 22.11 L 9.036 19.247 C 11.026 19.229 6.885 19.196 11.75 19.264 C 17.457 19.343 21.743 16.529 22 12 C 22.255 7.507 17.5 3 12 3 Z"/>
<path d="M12 3c-1.5 0-2.8.3-4 .8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 526 B

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M 3.303 14.04 C 4.689 15.913 6.918 17.127 9.432 17.127 C 13.635 17.127 17.043 13.73 17.043 9.542 C 17.043 6.956 15.744 4.672 13.762 3.303 C 17.732 4.164 20.697 7.685 20.697 11.899 C 20.697 16.758 16.745 20.697 11.869 20.697 C 7.732 20.697 4.261 17.867 3.303 14.04 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 546 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<line x1="12" y1="2" x2="12" y2="22"/>
<line x1="2" y1="12" x2="22" y2="12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 375 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<ellipse cx="12" cy="12" rx="7" ry="10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 311 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<circle cx="12" cy="12" r="3"/>
<path d="M10.09 2.18L13.91 2.18L15.36 7.03L17.89 3.71L20.29 6.41L17.89 10.86L21.82 10.09V13.91L16.97 15.36L20.29 17.59L17.59 20.29L13.14 17.89L13.91 21.82H10.09L8.64 16.97L6.41 20.29L3.71 17.59L6.11 13.14L2.18 13.91V10.09L7.03 8.64L3.71 6.41L6.41 3.71L10.86 6.11Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 596 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 21C12 21 4 14 4 8.5C4 5.4 6.2 3 9 3c1.7 0 3 .8 3 2.2C12 3.8 13.3 3 15 3c2.8 0 5 2.4 5 5.5C20 14 12 21 12 21z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<polygon points="12,2 22,8 19,20 5,20 2,8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 338 B

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<rect x="3" y="5" width="18" height="14" rx="1"/>
<circle cx="3" cy="5" r="1.5" fill="currentColor"/>
<circle cx="21" cy="5" r="1.5" fill="currentColor"/>
<circle cx="3" cy="19" r="1.5" fill="currentColor"/>
<circle cx="21" cy="19" r="1.5" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 547 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<polygon points="12,3 21,12 12,21 3,12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 335 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" color="#fff" stroke-width="1.80" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M5 3v15l3.5-3.5 2.5 6 2-1-2.5-6H15L5 3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 345 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M12 6C5.6 6 1 12 1 12s4.6 6 11 6 11-6 11-6-4.6-6-11-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4zm0-6.5c-1.4 0-2.5 1.1-2.5 2.5s1.1 2.5 2.5 2.5 2.5-1.1 2.5-2.5S13.4 9.5 12 9.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 420 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<rect x="4" y="4" width="16" height="16" rx="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 306 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linecap="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<path d="M5 12h14"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 300 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="1.00" stroke-linejoin="round">
<g transform="translate(40.96,40.96) scale(17.920000) translate(-0.00,-0.00)">
<rect x="3" y="3" width="10" height="10" rx="1.5"/>
<path d="M9 15v2a2 2 0 002 2h7a2 2 0 002-2v-7a2 2 0 00-2-2h-2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 401 B

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff">
<g transform="translate(40.96,40.96) scale(13.440000) translate(2.02,2.00)">
<g id="Capa_1" transform="translate(-0.0176003814697268, -5.14984130894902E-07)">
<g transform="matrix(0.0942760929465294, 0, 0, 0.0942760929465294, 0, 0)">
<g>
<path d="M150.079, 100.075C150.077, 100.075 150.075, 100.075 150.073, 100.075L120.034, 100.093L120.048, 70.048C120.051, 64.549 115.594, 60.087 110.095, 60.085C110.093, 60.085 110.092, 60.085 110.09, 60.085C104.592, 60.085 100.136, 64.539 100.133, 70.038L100.118, 100.103L70.051, 100.121C64.552, 100.124 60.095, 104.584 60.098, 110.084C60.101, 115.584 64.559, 120.037 70.057, 120.037C70.059, 120.037 70.061, 120.037 70.063, 120.037L100.109, 120.019L100.094, 150.069C100.091, 155.568 104.548, 160.028 110.047, 160.031C110.049, 160.031 110.05, 160.031 110.052, 160.031C115.55, 160.031 120.007, 155.577 120.01, 150.079L120.025, 120.009L150.086, 119.991C155.585, 119.988 160.04, 115.527 160.037, 110.027C160.034, 104.529 155.576, 100.075 150.079, 100.075z" fill="currentColor"/>
</g>
</g>
</g>
<g id="Capa_1" transform="translate(-0.0176, 0)">
<g transform="matrix(0.0942760929465294, 0, 0, 0.0942760929465294, 0, 0)">
<g>
<path d="M288.969, 246.75L206.053, 163.745C229.421, 121.855 223.342, 67.803 187.811, 32.237C167.048, 11.449 139.438, 0 110.069, 0C80.701, 0 53.091, 11.449 32.323, 32.237C-10.54, 75.148 -10.54, 144.966 32.323, 187.868C53.09, 208.661 80.701, 220.111 110.069, 220.111C129.164, 220.111 147.51, 215.266 163.719, 206.167L246.598, 289.138C251.662, 294.209 258.412, 297 265.607, 297C272.8, 297 279.552, 294.209 284.617, 289.138L288.971, 284.778C299.443, 274.293 299.442, 257.232 288.969, 246.75zM46.412, 173.794C11.304, 138.651 11.305, 81.462 46.412, 46.311C63.418, 29.291 86.024, 19.916 110.069, 19.916C134.114, 19.916 156.719, 29.291 173.722, 46.311C208.834, 81.46 208.834, 138.649 173.72, 173.795C156.719, 190.819 134.114, 200.195 110.069, 200.195C86.024, 200.195 63.418, 190.819 46.412, 173.794zM274.879, 270.704L270.524, 275.064C269.225, 276.366 267.478, 277.084 265.607, 277.084C263.737, 277.084 261.991, 276.365 260.689, 275.064L180.378, 194.664C182.94, 192.522 185.424, 190.261 187.81, 187.87C190.203, 185.476 192.453, 182.992 194.578, 180.441L274.879, 260.825C277.553, 263.502 277.553, 268.025 274.879, 270.704z" fill="currentColor"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#fff">
<g transform="translate(40.96,40.96) scale(13.440000) translate(2.02,2.00)">
<g id="Capa_1" transform="translate(-0.0151003814697264, -5.3405761768488E-07)">
<g transform="matrix(0.0942760929465294, 0, 0, 0.0942760929465294, 0, 0)">
<g>
<path d="M150.08, 100.079C150.078, 100.079 150.077, 100.079 150.075, 100.079L70.044, 100.12C64.543, 100.122 60.086, 104.583 60.089, 110.083C60.092, 115.582 64.55, 120.038 70.048, 120.038C70.05, 120.038 70.052, 120.038 70.054, 120.038L150.085, 119.998C155.585, 119.995 160.042, 115.534 160.039, 110.034C160.036, 104.535 155.578, 100.079 150.08, 100.079z" fill="currentColor"/>
</g>
</g>
</g>
<g id="Capa_1" transform="translate(-0.0151, 0)">
<g transform="matrix(0.0942760929465294, 0, 0, 0.0942760929465294, 0, 0)">
<g>
<path d="M288.985, 246.75L206.059, 163.745C229.428, 121.854 223.347, 67.804 187.814, 32.237C167.048, 11.449 139.436, 0 110.064, 0C80.691, 0 53.076, 11.449 32.306, 32.237C-10.558, 75.151 -10.557, 144.967 32.306, 187.871C53.076, 208.664 80.692, 220.114 110.064, 220.114C129.161, 220.114 147.51, 215.269 163.721, 206.17L246.611, 289.139C251.674, 294.208 258.426, 297 265.622, 297C272.818, 297 279.57, 294.208 284.635, 289.139L288.987, 284.784C299.461, 274.298 299.461, 257.236 288.985, 246.75zM46.398, 173.794C11.289, 138.651 11.289, 81.465 46.398, 46.315C63.404, 29.294 86.015, 19.918 110.064, 19.918C134.113, 19.918 156.72, 29.294 173.723, 46.314C208.839, 81.463 208.839, 138.65 173.723, 173.794C156.719, 190.818 134.113, 200.194 110.064, 200.194C86.015, 200.195 63.404, 190.819 46.398, 173.794zM274.894, 270.704L270.541, 275.06C269.24, 276.363 267.493, 277.081 265.621, 277.081C263.75, 277.081 262.004, 276.363 260.702, 275.061L180.382, 194.666C182.944, 192.525 185.427, 190.263 187.814, 187.872C190.207, 185.478 192.459, 182.995 194.585, 180.441L274.895, 260.827C277.567, 263.506 277.568, 268.027 274.894, 270.704z" fill="currentColor"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -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 }
File diff suppressed because it is too large Load Diff
@@ -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<u8> {
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()
}
@@ -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.
@@ -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()
}
@@ -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()
}
@@ -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()
}
@@ -0,0 +1,5 @@
//! Dialog modules — modal dialogs for the Iced GUI.
pub mod new_image;
pub mod adjustments;
pub mod confirm;
@@ -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()
}
@@ -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;
@@ -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<PaneType>,
/// The root pane (always the canvas).
pub root_pane: Pane,
/// Currently focused pane.
pub focused_pane: Option<Pane>,
}
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<Pane> {
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<Pane> {
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<PaneType> {
self.pane_grid.get(pane).copied()
}
/// Get the currently focused pane type.
pub fn focused_type(&self) -> Option<PaneType> {
self.focused_pane.and_then(|p| self.pane_type(p))
}
}
@@ -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)
)
}
@@ -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<Option<(Vec<u8>, 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<Option<String>, 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<u8>, 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<u8> = 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<u8>, 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<u8>, 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<u8>, 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<u8>, 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))
}
@@ -0,0 +1,4 @@
//! I/O modules — tablet input, clipboard, and file I/O.
pub mod clipboard;
pub mod tablet;
@@ -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<Mutex<TabletState>>) {
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<Mutex<TabletState>>) {
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<evdev::Device> {
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)
}
}
@@ -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<String> = std::env::args().collect();
let mut load_path: Option<std::path::PathBuf> = 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())
});
}
@@ -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()
}
@@ -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()
}
@@ -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()
}
@@ -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<FilterType>, 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()
}
@@ -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()
}
@@ -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()
}
@@ -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()
}
@@ -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()
}
@@ -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()
}
@@ -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<MenuItem>,
}
fn all_menus() -> Vec<MenuDef> {
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<usize>) -> 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()
}
@@ -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;
@@ -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()
}
@@ -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 <name> Create layer").size(10).font(iced::Font::MONOSPACE),
text("select <name> Select layer").size(10).font(iced::Font::MONOSPACE),
text("color <r> <g> <b> Set color").size(10).font(iced::Font::MONOSPACE),
text("size <pixels> 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()
}
@@ -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()
}

Some files were not shown because too many files have changed in this diff Show More