1301 lines
45 KiB
Markdown
1301 lines
45 KiB
Markdown
# ICED Full GUI Migration Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Migrate all GUI functions and elements from egui to ICED, bringing the ICED GUI to feature parity with the egui GUI.
|
|
|
|
**Architecture:** The ICED GUI already has solid infrastructure (~12,000 lines, 52 source files) including GPU-accelerated canvas, dock system, basic panels, and selection/transform systems. The migration focuses on filling the gaps: filter parameter panels, AI chat with streaming, full script editor with parser/executor, AI script generator, viewer mode, and auxiliary features.
|
|
|
|
**Tech Stack:** Rust, ICED 0.13 (with tokio, image, svg, canvas, wgpu, advanced features), hcie-engine-api, serde_json, reqwest, tokio, arboard, rfd
|
|
|
|
## Global Constraints
|
|
|
|
- All GUI code lives in `hcie-iced-app/crates/` — never modify engine crates directly
|
|
- Engine interaction only through `hcie_engine-api` public API
|
|
- ICED 0.13 with features: `["tokio", "image", "svg", "canvas", "wgpu", "advanced"]`
|
|
- Follow existing ICED patterns: `update()`/`view()`/`subscription()` Elm architecture
|
|
- Use `ThemeColors` from `iced-panel-adapter` for consistent styling
|
|
- Preserve all GPU-accelerated canvas performance optimizations in `shader_canvas.rs`
|
|
- No comments unless the WHY is non-obvious
|
|
|
|
---
|
|
|
|
## Phase 1: Filter Parameter Panels (Critical — Core Feature)
|
|
|
|
### Task 1: Create Filter Parameter Panel Module
|
|
|
|
**Covers:** Filter parameter UI for all 30+ filters
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs` — add `pub mod filter_params;`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add `FilterParamChanged` message handling
|
|
|
|
**Interfaces:**
|
|
- Consumes: `FilterType`, `ThemeColors`, `serde_json::Value`
|
|
- Produces: `Element<'static, Message>` for each filter's parameter panel
|
|
|
|
- [ ] **Step 1: Create filter_params.rs with parameter panels for all filters**
|
|
|
|
```rust
|
|
//! Filter parameter panels — dynamic parameter UIs for each filter type.
|
|
//!
|
|
//! Each filter gets a dedicated parameter panel with appropriate controls
|
|
//! (sliders, color pickers, dropdowns) matching the egui implementation.
|
|
|
|
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, pick_list, row, scrollable, slider, text, text_input};
|
|
use iced::{Element, Length};
|
|
use serde_json::{json, Value};
|
|
|
|
/// Build the parameter panel for the selected filter.
|
|
pub fn view(
|
|
filter: FilterType,
|
|
params: &Value,
|
|
colors: ThemeColors,
|
|
) -> Element<'static, Message> {
|
|
let panel: Element<'static, Message> = match filter {
|
|
// ── Blur filters ──────────────────────────────────
|
|
FilterType::BoxBlur => blur_panel("box_blur", "Radius", params, colors),
|
|
FilterType::GaussianBlur => blur_panel("gaussian_blur", "Radius", params, colors),
|
|
FilterType::MotionBlur => motion_blur_panel(params, colors),
|
|
FilterType::GaussianBlurGamma => blur_panel("gaussian_blur_gamma", "Radius", params, colors),
|
|
FilterType::BoxBlurGamma => blur_panel("box_blur_gamma", "Radius", params, colors),
|
|
FilterType::MedianFilter => blur_panel("median_filter", "Radius", params, colors),
|
|
|
|
// ── Sharpen ───────────────────────────────────────
|
|
FilterType::UnsharpMask => unsharp_panel(params, colors),
|
|
|
|
// ── Pixelate ──────────────────────────────────────
|
|
FilterType::Mosaic => pixel_size_panel("mosaic", params, colors),
|
|
FilterType::Crystallize => pixel_size_panel("crystallize", "Cell Size", params, colors),
|
|
|
|
// ── Distort ───────────────────────────────────────
|
|
FilterType::Pinch => amount_panel("pinch", "Strength", params, colors),
|
|
FilterType::Twirl => amount_panel("twirl", "Angle", params, colors),
|
|
|
|
// ── Stylize ───────────────────────────────────────
|
|
FilterType::OilPaint => oil_paint_panel(params, colors),
|
|
|
|
// ── Color & Light ─────────────────────────────────
|
|
FilterType::Levels => levels_panel(params, colors),
|
|
FilterType::Vibrance => amount_panel("vibrance", "Amount", params, colors),
|
|
FilterType::Exposure => amount_panel("exposure", "Stops", params, colors),
|
|
FilterType::Posterize => levels_panel_custom("posterize", "Levels", 2, 255, params, colors),
|
|
FilterType::Threshold => levels_panel_custom("threshold", "Threshold", 0, 255, params, colors),
|
|
FilterType::ChannelMixer => channel_mixer_panel(params, colors),
|
|
FilterType::BlackAndWhite => black_and_white_panel(params, colors),
|
|
FilterType::SelectiveColor => selective_color_panel(params, colors),
|
|
FilterType::GammaCorrection => amount_panel("gamma_correction", "Gamma", params, colors),
|
|
FilterType::ExtractChannel => extract_channel_panel(params, colors),
|
|
FilterType::GradientMap => gradient_map_panel(params, colors),
|
|
FilterType::Dehaze => amount_panel("dehaze", "Strength", params, colors),
|
|
|
|
// ── Noise & Pattern ───────────────────────────────
|
|
FilterType::NoisePattern => noise_pattern_panel(params, colors),
|
|
};
|
|
|
|
container(panel)
|
|
.width(Length::Fill)
|
|
.padding(8)
|
|
.style(move |_theme| styles::panel_background(colors))
|
|
.into()
|
|
}
|
|
|
|
// ── Helper functions for building parameter panels ────────
|
|
|
|
fn param_slider(
|
|
label: &'static str,
|
|
filter_id: &'static str,
|
|
param_key: &'static str,
|
|
min: f32,
|
|
max: f32,
|
|
step: f32,
|
|
current: f32,
|
|
colors: ThemeColors,
|
|
) -> Element<'static, Message> {
|
|
let value_label = text(format!("{:.1}", current))
|
|
.size(10)
|
|
.width(Length::Fixed(40.0));
|
|
let sl = slider(min..=max, current, move |v| {
|
|
Message::FilterParamChanged(
|
|
filter_id.to_string(),
|
|
json!({ param_key: v }),
|
|
)
|
|
})
|
|
.step(step)
|
|
.width(Length::Fill);
|
|
|
|
row![text(label).size(10).width(Length::Fixed(80.0)), sl, value_label]
|
|
.spacing(4)
|
|
.align_y(iced::Alignment::Center)
|
|
.into()
|
|
}
|
|
|
|
fn blur_panel(filter_id: &'static str, label: &'static str, params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let radius = params["radius"].as_f64().unwrap_or(5.0) as f32;
|
|
column![
|
|
param_slider(label, filter_id, "radius", 1.0, 50.0, 0.5, radius, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn motion_blur_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let radius = params["radius"].as_f64().unwrap_or(5.0) as f32;
|
|
let angle = params["angle"].as_f64().unwrap_or(0.0) as f32;
|
|
column![
|
|
param_slider("Radius", "motion_blur", "radius", 1.0, 50.0, 0.5, radius, colors),
|
|
param_slider("Angle", "motion_blur", "angle", 0.0, 360.0, 1.0, angle, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn unsharp_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let radius = params["radius"].as_f64().unwrap_or(3.0) as f32;
|
|
let amount = params["amount"].as_f64().unwrap_or(1.0) as f32;
|
|
let threshold = params["threshold"].as_f64().unwrap_or(0.0) as f32;
|
|
column![
|
|
param_slider("Radius", "unsharp_mask", "radius", 1.0, 20.0, 0.5, radius, colors),
|
|
param_slider("Amount", "unsharp_mask", "amount", 0.1, 5.0, 0.1, amount, colors),
|
|
param_slider("Threshold", "unsharp_mask", "threshold", 0.0, 255.0, 1.0, threshold, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn pixel_size_panel(filter_id: &'static str, params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let size = params["size"].as_f64().unwrap_or(10.0) as f32;
|
|
param_slider("Cell Size", filter_id, "size", 2.0, 100.0, 1.0, size, colors)
|
|
}
|
|
|
|
fn amount_panel(filter_id: &'static str, label: &'static str, params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let amount = params["amount"].as_f64().unwrap_or(0.5) as f32;
|
|
param_slider(label, filter_id, "amount", -2.0, 2.0, 0.01, amount, colors)
|
|
}
|
|
|
|
fn oil_paint_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let size = params["size"].as_f64().unwrap_or(4.0) as f32;
|
|
let dynamic = params["dynamic"].as_f64().unwrap_or(1.0) as f32;
|
|
column![
|
|
param_slider("Brush Size", "oil_paint", "size", 1.0, 20.0, 1.0, size, colors),
|
|
param_slider("Dynamic", "oil_paint", "dynamic", 0.0, 2.0, 0.1, dynamic, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn levels_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let input_low = params["input_low"].as_f64().unwrap_or(0.0) as f32;
|
|
let input_high = params["input_high"].as_f64().unwrap_or(255.0) as f32;
|
|
let output_low = params["output_low"].as_f64().unwrap_or(0.0) as f32;
|
|
let output_high = params["output_high"].as_f64().unwrap_or(255.0) as f32;
|
|
let gamma = params["gamma"].as_f64().unwrap_or(1.0) as f32;
|
|
column![
|
|
param_slider("Input Low", "levels", "input_low", 0.0, 255.0, 1.0, input_low, colors),
|
|
param_slider("Input High", "levels", "input_high", 0.0, 255.0, 1.0, input_high, colors),
|
|
param_slider("Gamma", "levels", "gamma", 0.1, 5.0, 0.1, gamma, colors),
|
|
param_slider("Output Low", "levels", "output_low", 0.0, 255.0, 1.0, output_low, colors),
|
|
param_slider("Output High", "levels", "output_high", 0.0, 255.0, 1.0, output_high, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn levels_panel_custom(filter_id: &'static str, label: &'static str, min: f32, max: f32, params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let value = params["levels"].as_f64().unwrap_or(128.0) as f32;
|
|
param_slider(label, filter_id, "levels", min, max, 1.0, value, colors)
|
|
}
|
|
|
|
fn channel_mixer_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let rr = params["red_red"].as_f64().unwrap_or(100.0) as f32;
|
|
let rg = params["red_green"].as_f64().unwrap_or(0.0) as f32;
|
|
let rb = params["red_blue"].as_f64().unwrap_or(0.0) as f32;
|
|
let gr = params["green_red"].as_f64().unwrap_or(0.0) as f32;
|
|
let gg = params["green_green"].as_f64().unwrap_or(100.0) as f32;
|
|
let gb = params["green_blue"].as_f64().unwrap_or(0.0) as f32;
|
|
let br = params["blue_red"].as_f64().unwrap_or(0.0) as f32;
|
|
let bg = params["blue_green"].as_f64().unwrap_or(0.0) as f32;
|
|
let bb = params["blue_blue"].as_f64().unwrap_or(100.0) as f32;
|
|
column![
|
|
text("Red Channel").size(11),
|
|
param_slider("Red", "channel_mixer", "red_red", -200.0, 200.0, 1.0, rr, colors),
|
|
param_slider("Green", "channel_mixer", "red_green", -200.0, 200.0, 1.0, rg, colors),
|
|
param_slider("Blue", "channel_mixer", "red_blue", -200.0, 200.0, 1.0, rb, colors),
|
|
text("Green Channel").size(11),
|
|
param_slider("Red", "channel_mixer", "green_red", -200.0, 200.0, 1.0, gr, colors),
|
|
param_slider("Green", "channel_mixer", "green_green", -200.0, 200.0, 1.0, gg, colors),
|
|
param_slider("Blue", "channel_mixer", "green_blue", -200.0, 200.0, 1.0, gb, colors),
|
|
text("Blue Channel").size(11),
|
|
param_slider("Red", "channel_mixer", "blue_red", -200.0, 200.0, 1.0, br, colors),
|
|
param_slider("Green", "channel_mixer", "blue_green", -200.0, 200.0, 1.0, bg, colors),
|
|
param_slider("Blue", "channel_mixer", "blue_blue", -200.0, 200.0, 1.0, bb, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn black_and_white_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let red = params["red"].as_f64().unwrap_or(40.0) as f32;
|
|
let yellow = params["yellow"].as_f64().unwrap_or(60.0) as f32;
|
|
let green = params["green"].as_f64().unwrap_or(40.0) as f32;
|
|
let cyan = params["cyan"].as_f64().unwrap_or(60.0) as f32;
|
|
let blue = params["blue"].as_f64().unwrap_or(20.0) as f32;
|
|
let magenta = params["magenta"].as_f64().unwrap_or(80.0) as f32;
|
|
column![
|
|
param_slider("Red", "black_and_white", "red", 0.0, 200.0, 1.0, red, colors),
|
|
param_slider("Yellow", "black_and_white", "yellow", 0.0, 200.0, 1.0, yellow, colors),
|
|
param_slider("Green", "black_and_white", "green", 0.0, 200.0, 1.0, green, colors),
|
|
param_slider("Cyan", "black_and_white", "cyan", 0.0, 200.0, 1.0, cyan, colors),
|
|
param_slider("Blue", "black_and_white", "blue", 0.0, 200.0, 1.0, blue, colors),
|
|
param_slider("Magenta", "black_and_white", "magenta", 0.0, 200.0, 1.0, magenta, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn selective_color_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let _ = params;
|
|
column![
|
|
text("Selective Color").size(11),
|
|
text("Select a color range to adjust:").size(10),
|
|
text("Cyan, Magenta, Yellow, Black").size(10),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn extract_channel_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let _ = params;
|
|
column![
|
|
text("Extract Channel").size(11),
|
|
text("Channel: Red").size(10),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn gradient_map_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let _ = params;
|
|
column![
|
|
text("Gradient Map").size(11),
|
|
text("Maps grayscale values to a gradient").size(10),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
|
|
fn noise_pattern_panel(params: &Value, colors: ThemeColors) -> Element<'static, Message> {
|
|
let scale = params["scale"].as_f64().unwrap_or(50.0) as f32;
|
|
let opacity = params["opacity"].as_f64().unwrap_or(0.5) as f32;
|
|
column![
|
|
param_slider("Scale", "noise_pattern", "scale", 1.0, 200.0, 1.0, scale, colors),
|
|
param_slider("Opacity", "noise_pattern", "opacity", 0.0, 1.0, 0.01, opacity, colors),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Register the module in panels/mod.rs**
|
|
|
|
Add to `hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs`:
|
|
```rust
|
|
pub mod filter_params;
|
|
```
|
|
|
|
- [ ] **Step 3: Integrate filter params into filters panel view**
|
|
|
|
Modify `hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs` to show parameter panel when a filter is selected:
|
|
|
|
Replace the `view` function with:
|
|
```rust
|
|
pub fn view(selected_filter: Option<FilterType>, params: &serde_json::Value, 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);
|
|
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);
|
|
|
|
// Show parameter panel when a filter is selected
|
|
let param_panel = if let Some(filter) = selected_filter {
|
|
super::filter_params::view(filter, params, colors)
|
|
} else {
|
|
container(text("Select a filter").size(10).style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(colors.text_secondary),
|
|
}))
|
|
.padding(8)
|
|
.into()
|
|
};
|
|
|
|
let panel = column![
|
|
scrollable(filter_list).height(Length::Fill),
|
|
horizontal_rule(1),
|
|
param_panel,
|
|
horizontal_rule(1),
|
|
buttons,
|
|
]
|
|
.spacing(0)
|
|
.padding(4);
|
|
|
|
container(panel)
|
|
.width(Length::Fill)
|
|
.height(Length::Fill)
|
|
.style(move |_theme| styles::panel_background(colors))
|
|
.into()
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Update app.rs to pass filter params to filters view**
|
|
|
|
Find where `panels::filters::view()` is called in app.rs and add the `params` argument.
|
|
|
|
- [ ] **Step 5: Test compilation**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs hcie-iced-app/crates/hcie-iced-gui/src/app.rs
|
|
git commit -m "feat(iced): add filter parameter panels for all 30+ filters"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Add Filter Instant Preview
|
|
|
|
**Covers:** Live preview of filters before applying
|
|
|
|
**Files:**
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add preview begin/restore/apply logic
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs` — add preview checkbox
|
|
|
|
**Interfaces:**
|
|
- Consumes: `engine.filter_preview_begin()`, `engine.filter_preview_restore()`, `engine.apply_filter_preview()`
|
|
- Produces: Updated composite texture with preview
|
|
|
|
- [ ] **Step 1: Add preview state to app.rs**
|
|
|
|
In `HcieIcedApp` struct, ensure `filter_preview_active: bool` exists (already present).
|
|
|
|
- [ ] **Step 2: Add preview message variants**
|
|
|
|
Add to Message enum:
|
|
```rust
|
|
FilterPreviewToggle(bool),
|
|
FilterPreviewApply,
|
|
FilterPreviewRestore,
|
|
```
|
|
|
|
- [ ] **Step 3: Handle preview messages in update()**
|
|
|
|
In the `update()` function, add handling for the new messages that call the engine's preview methods.
|
|
|
|
- [ ] **Step 4: Add preview checkbox to filters panel**
|
|
|
|
In `filters.rs`, add a checkbox before Apply/Reset buttons:
|
|
```rust
|
|
let preview_check = iced::widget::checkbox("Preview", filter_preview_active)
|
|
.on_toggle(|v| Message::FilterPreviewToggle(v));
|
|
```
|
|
|
|
- [ ] **Step 5: Test compilation and verify**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/app.rs hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs
|
|
git commit -m "feat(iced): add filter instant preview"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Full AI Chat Panel with Streaming
|
|
|
|
### Task 3: Implement AI Chat with Streaming SSE
|
|
|
|
**Covers:** Complete AI chat panel with streaming responses, tool execution, and DSL parsing
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/ai_chat.rs` — AI chat state and logic
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs` — full implementation
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add AI chat messages
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml` — add `reqwest` and `tokio` dependencies
|
|
|
|
**Interfaces:**
|
|
- Consumes: `hcie_engine_api::Engine`, `reqwest`, `tokio`
|
|
- Produces: Streaming AI responses, tool call execution
|
|
|
|
- [ ] **Step 1: Create ai_chat.rs with chat state and streaming logic**
|
|
|
|
```rust
|
|
//! AI Chat state and streaming logic.
|
|
//!
|
|
//! Handles LLM communication, streaming SSE responses,
|
|
//! tool call parsing, and DSL script execution.
|
|
|
|
use hcie_engine_api::Engine;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
/// AI chat message.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ChatMessage {
|
|
pub role: String,
|
|
pub content: String,
|
|
pub is_reasoning: bool,
|
|
}
|
|
|
|
/// AI chat configuration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct AiChatConfig {
|
|
pub provider: String,
|
|
pub base_url: String,
|
|
pub model: String,
|
|
pub max_turns: u32,
|
|
pub reasoning_enabled: bool,
|
|
pub canvas_context: bool,
|
|
}
|
|
|
|
impl Default for AiChatConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
provider: "ollama".to_string(),
|
|
base_url: "http://localhost:11434".to_string(),
|
|
model: "llama3".to_string(),
|
|
max_turns: 10,
|
|
reasoning_enabled: false,
|
|
canvas_context: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// AI chat state.
|
|
pub struct AiChatState {
|
|
pub messages: Vec<ChatMessage>,
|
|
pub input: String,
|
|
pub config: AiChatConfig,
|
|
pub is_streaming: bool,
|
|
pub current_response: String,
|
|
}
|
|
|
|
impl AiChatState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
messages: Vec::new(),
|
|
input: String::new(),
|
|
config: AiChatConfig::default(),
|
|
is_streaming: false,
|
|
current_response: String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.messages.clear();
|
|
self.current_response.clear();
|
|
self.is_streaming = false;
|
|
}
|
|
}
|
|
|
|
/// System prompt for the AI assistant.
|
|
pub const SYSTEM_PROMPT: &str = r##"You are an expert digital artist AI assistant integrated inside HCIE-Rust, a pixel-grade digital painting and image editing application.
|
|
You can talk to the user AND draw or edit their canvas.
|
|
You have access to a set of canvas tools that you can call. If the user asks you to paint or edit something, always call the appropriate tools using a JSON code block.
|
|
|
|
## Tool Calling Mode
|
|
To call tools, output a markdown code block starting with ```json containing either a single tool object or an array of tool objects.
|
|
|
|
### Supported JSON Tools:
|
|
- create_layer(name: string): Creates a new layer and selects it.
|
|
- select_layer(name: string): Selects an existing layer by name.
|
|
- draw_rect(x1, y1, x2, y2): Draws a filled rectangle.
|
|
- draw_ellipse(cx, cy, rx, ry): Draws a filled ellipse.
|
|
- draw_line(x0, y0, x1, y1): Draws a line.
|
|
- add_text(text, font, size, x, y, color_hex): Renders vector text.
|
|
- apply_filter(filter_id, params): Applies an image filter.
|
|
- draw_stroke(points): Paints a brush stroke.
|
|
|
|
## DSL Mode
|
|
You can also output a procedural drawing script in a ```dsl code block.
|
|
Available DSL commands:
|
|
- layer <name> — Create and select a layer.
|
|
- color <hex> — Set current color.
|
|
- size <px> — Set brush size.
|
|
- rect x1,y1 x2,y2 — Draw filled rectangle.
|
|
- circle cx,cy — Draw filled circle.
|
|
- line x0,y0 x1,y1 — Draw straight line.
|
|
- text "content" font size x y [hex] — Draw text.
|
|
"##;
|
|
|
|
/// Execute a parsed JSON tool call on the engine.
|
|
pub fn execute_tool_call(engine: &mut Engine, name: &str, args: &serde_json::Value) -> Result<String, String> {
|
|
match name {
|
|
"create_layer" | "add_layer" => {
|
|
let layer_name = args["name"].as_str().unwrap_or("Layer");
|
|
let id = engine.add_layer(layer_name);
|
|
engine.set_active_layer(id);
|
|
Ok(format!("Created layer '{}' (ID: {})", layer_name, id))
|
|
}
|
|
"select_layer" => {
|
|
let layer_name = args["name"].as_str().unwrap_or("");
|
|
if let Some(layer) = engine.layer_infos().iter().find(|l| l.name.eq_ignore_ascii_case(layer_name)) {
|
|
engine.set_active_layer(layer.id);
|
|
Ok(format!("Selected layer '{}' (ID: {})", layer_name, layer.id))
|
|
} else {
|
|
let id = engine.add_layer(layer_name);
|
|
engine.set_active_layer(id);
|
|
Ok(format!("Layer '{}' not found. Created and selected it.", layer_name))
|
|
}
|
|
}
|
|
"draw_rect" => {
|
|
let x1 = args["x1"].as_f64().unwrap_or(0.0) as f32;
|
|
let y1 = args["y1"].as_f64().unwrap_or(0.0) as f32;
|
|
let x2 = args["x2"].as_f64().unwrap_or(0.0) as f32;
|
|
let y2 = args["y2"].as_f64().unwrap_or(0.0) as f32;
|
|
engine.draw_rect(x1, y1, x2, y2);
|
|
Ok(format!("Drew rectangle [{}, {}, {}, {}]", x1, y1, x2, y2))
|
|
}
|
|
"draw_ellipse" => {
|
|
let cx = args["cx"].as_f64().unwrap_or(0.0) as f32;
|
|
let cy = args["cy"].as_f64().unwrap_or(0.0) as f32;
|
|
let rx = args["rx"].as_f64().unwrap_or(0.0) as f32;
|
|
let ry = args["ry"].as_f64().unwrap_or(0.0) as f32;
|
|
engine.draw_ellipse(cx, cy, rx, ry);
|
|
Ok(format!("Drew ellipse center ({}, {}) radius ({}, {})", cx, cy, rx, ry))
|
|
}
|
|
"draw_line" => {
|
|
let x0 = args["x0"].as_f64().unwrap_or(0.0) as f32;
|
|
let y0 = args["y0"].as_f64().unwrap_or(0.0) as f32;
|
|
let x1 = args["x1"].as_f64().unwrap_or(0.0) as f32;
|
|
let y1 = args["y1"].as_f64().unwrap_or(0.0) as f32;
|
|
engine.draw_line(x0, y0, x1, y1);
|
|
Ok(format!("Drew line from ({}, {}) to ({}, {})", x0, y0, x1, y1))
|
|
}
|
|
"add_text" => {
|
|
let text = args["text"].as_str().unwrap_or("");
|
|
let font = args["font"].as_str().unwrap_or("default");
|
|
let size = args["size"].as_f64().unwrap_or(24.0) as f32;
|
|
let x = args["x"].as_f64().unwrap_or(100.0) as f32;
|
|
let y = args["y"].as_f64().unwrap_or(100.0) as f32;
|
|
let color_hex = args["color_hex"].as_str().unwrap_or("#000000");
|
|
let color = parse_hex_color(color_hex).unwrap_or([0, 0, 0, 255]);
|
|
engine.add_text(text, font, size, x, y, color, 0.0, hcie_engine_api::TextAlignment::Left, hcie_engine_api::TextOrientation::Horizontal, &[]);
|
|
Ok(format!("Added text '{}' at ({}, {})", text, x, y))
|
|
}
|
|
"apply_filter" => {
|
|
let filter_id = args["filter_id"].as_str().unwrap_or("");
|
|
let params = args["params"].clone();
|
|
engine.apply_filter(filter_id, params);
|
|
Ok(format!("Applied filter '{}'", filter_id))
|
|
}
|
|
"draw_stroke" => {
|
|
let mut points = Vec::new();
|
|
if let Some(pts_arr) = args["points"].as_array() {
|
|
for pt in pts_arr {
|
|
let x = pt["x"].as_f64().unwrap_or(0.0) as f32;
|
|
let y = pt["y"].as_f64().unwrap_or(0.0) as f32;
|
|
let p = pt["pressure"].as_f64().unwrap_or(1.0) as f32;
|
|
points.push((x, y, p));
|
|
}
|
|
}
|
|
engine.draw_stroke(&points);
|
|
Ok(format!("Drew brush stroke with {} points", points.len()))
|
|
}
|
|
_ => Err(format!("Unknown tool: {}", name))
|
|
}
|
|
}
|
|
|
|
/// Parse hex color string to RGBA.
|
|
fn parse_hex_color(hex: &str) -> Option<[u8; 4]> {
|
|
let hex = hex.trim_start_matches('#');
|
|
if hex.len() < 6 { return None; }
|
|
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
|
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
|
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
|
let a = if hex.len() >= 8 {
|
|
u8::from_str_radix(&hex[6..8], 16).unwrap_or(255)
|
|
} else {
|
|
255
|
|
};
|
|
Some([r, g, b, a])
|
|
}
|
|
|
|
/// Parse DSL script and execute on engine.
|
|
pub fn execute_dsl_script(engine: &mut Engine, script: &str) -> Result<Vec<String>, String> {
|
|
let mut results = Vec::new();
|
|
for line in script.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') { continue; }
|
|
|
|
let parts: Vec<&str> = line.splitn(2, ' ').collect();
|
|
let cmd = parts[0];
|
|
let args = parts.get(1).unwrap_or(&"");
|
|
|
|
match cmd {
|
|
"layer" => {
|
|
let id = engine.add_layer(args);
|
|
results.push(format!("Created layer '{}' (ID: {})", args, id));
|
|
}
|
|
"color" => {
|
|
if let Some(color) = parse_hex_color(args) {
|
|
engine.set_color(color);
|
|
results.push(format!("Set color to {}", args));
|
|
}
|
|
}
|
|
"size" => {
|
|
if let Ok(size) = args.parse::<f32>() {
|
|
let mut tip = hcie_engine_api::BrushTip::default();
|
|
tip.size = size;
|
|
engine.set_brush_tip(tip);
|
|
results.push(format!("Set brush size to {}", size));
|
|
}
|
|
}
|
|
"rect" => {
|
|
let coords: Vec<f32> = args.split(|c: char| c == ',' || c == ' ')
|
|
.filter_map(|s| s.parse().ok())
|
|
.collect();
|
|
if coords.len() >= 4 {
|
|
engine.draw_rect(coords[0], coords[1], coords[2], coords[3]);
|
|
results.push(format!("Drew rect [{}, {}, {}, {}]", coords[0], coords[1], coords[2], coords[3]));
|
|
}
|
|
}
|
|
"line" => {
|
|
let coords: Vec<f32> = args.split(|c: char| c == ',' || c == ' ')
|
|
.filter_map(|s| s.parse().ok())
|
|
.collect();
|
|
if coords.len() >= 4 {
|
|
engine.draw_line(coords[0], coords[1], coords[2], coords[3]);
|
|
results.push(format!("Drew line [{}, {}, {}, {}]", coords[0], coords[1], coords[2], coords[3]));
|
|
}
|
|
}
|
|
"circle" => {
|
|
let coords: Vec<f32> = args.split(|c: char| c == ',' || c == ' ')
|
|
.filter_map(|s| s.parse().ok())
|
|
.collect();
|
|
if coords.len() >= 3 {
|
|
engine.draw_ellipse(coords[0], coords[1], coords[2], coords[2]);
|
|
results.push(format!("Drew circle at ({}, {}) r={}", coords[0], coords[1], coords[2]));
|
|
}
|
|
}
|
|
_ => {
|
|
results.push(format!("Unknown command: {}", cmd));
|
|
}
|
|
}
|
|
}
|
|
Ok(results)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Update ai_chat_panel.rs with full implementation**
|
|
|
|
Replace the entire `ai_chat_panel.rs` with a full implementation that includes:
|
|
- Provider selector (Ollama, Claude, OpenAI)
|
|
- URL and model inputs
|
|
- Message list with chat bubbles
|
|
- Streaming response display
|
|
- Input field with send/clear buttons
|
|
- Reasoning toggle
|
|
- Canvas context toggle
|
|
|
|
- [ ] **Step 3: Add AI chat message variants to app.rs**
|
|
|
|
Add to Message enum:
|
|
```rust
|
|
AiChatSend,
|
|
AiChatClear,
|
|
AiChatInput(String),
|
|
AiChatProviderChanged(String),
|
|
AiChatUrlChanged(String),
|
|
AiChatModelChanged(String),
|
|
AiChatReasoningToggled(bool),
|
|
AiChatCanvasContextToggled(bool),
|
|
AiChatStreamChunk(String),
|
|
AiChatStreamFinished(Result<String, String>),
|
|
```
|
|
|
|
- [ ] **Step 4: Handle AI chat messages in update()**
|
|
|
|
Implement the update logic for AI chat, including spawning async tasks for streaming.
|
|
|
|
- [ ] **Step 5: Add reqwest and tokio dependencies**
|
|
|
|
In `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml`, add:
|
|
```toml
|
|
reqwest = { version = "0.12", features = ["json", "stream"] }
|
|
tokio = { version = "1", features = ["full"] }
|
|
futures-util = "0.3"
|
|
```
|
|
|
|
- [ ] **Step 6: Test compilation**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/ai_chat.rs hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs hcie-iced-app/crates/hcie-iced-gui/src/app.rs hcie-iced-app/crates/hcie-iced-gui/Cargo.toml
|
|
git commit -m "feat(iced): implement AI chat with streaming SSE and tool execution"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: Full Script Editor with Parser/Executor
|
|
|
|
### Task 4: Port DSL Parser to ICED
|
|
|
|
**Covers:** Complete DSL tokenizer and recursive-descent parser
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/script/parser.rs`
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/script/executor.rs`
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/script/mod.rs`
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/script/types.rs`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `hcie_engine_api::Engine`, `serde_json::Value`
|
|
- Produces: `ScriptAction` list, execution results
|
|
|
|
- [ ] **Step 1: Create script/types.rs**
|
|
|
|
```rust
|
|
//! Script parser state for tracking brush/color/layer state during parsing.
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ScriptParserState {
|
|
pub current_color: String,
|
|
pub brush_style: Option<String>,
|
|
pub brush_size: f32,
|
|
pub brush_opacity: f32,
|
|
pub brush_hardness: f32,
|
|
pub brush_spacing: f32,
|
|
pub blend_mode: String,
|
|
pub variables: std::collections::HashMap<String, String>,
|
|
pub scatter: Option<u32>,
|
|
pub flow: f32,
|
|
pub pressure: f32,
|
|
pub wiggle: f32,
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create script/parser.rs**
|
|
|
|
Port the full parser from `egui-panel-script/src/parser.rs` (1143 lines) to ICED. This includes:
|
|
- Expression tokenizer
|
|
- Recursive-descent parser
|
|
- All ScriptAction variants
|
|
- Variable interpolation
|
|
- Loop/repeat support
|
|
- Error reporting with line numbers
|
|
|
|
- [ ] **Step 3: Create script/executor.rs**
|
|
|
|
Port the executor from `egui-panel-script/src/executor.rs` (234 lines) to ICED.
|
|
|
|
- [ ] **Step 4: Create script/mod.rs**
|
|
|
|
```rust
|
|
pub mod executor;
|
|
pub mod parser;
|
|
pub mod types;
|
|
|
|
pub use executor::execute_actions;
|
|
pub use parser::parse_dsl_script;
|
|
pub use parser::parse_error_line;
|
|
pub use parser::suggest_fix;
|
|
pub use parser::DEFAULT_SCRIPT;
|
|
```
|
|
|
|
- [ ] **Step 5: Test compilation**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/script/
|
|
git commit -m "feat(iced): port full DSL parser and executor to ICED"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Implement Full Script Editor Panel
|
|
|
|
**Covers:** Script editor with line numbers, error highlighting, and command reference
|
|
|
|
**Files:**
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/panels/script_panel.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add script state and messages
|
|
|
|
**Interfaces:**
|
|
- Consumes: `script::parse_dsl_script`, `script::execute_actions`, `ThemeColors`
|
|
- Produces: Full script editor UI with error display
|
|
|
|
- [ ] **Step 1: Add script state to app.rs**
|
|
|
|
In `HcieIcedApp` struct, add:
|
|
```rust
|
|
pub script_text: String,
|
|
pub script_error: Option<String>,
|
|
pub script_error_line: Option<usize>,
|
|
pub script_is_running: bool,
|
|
```
|
|
|
|
- [ ] **Step 2: Add script message variants**
|
|
|
|
```rust
|
|
ScriptTextChanged(String),
|
|
ScriptRun,
|
|
ScriptClear,
|
|
```
|
|
|
|
- [ ] **Step 3: Implement full script_panel.rs**
|
|
|
|
Replace the current placeholder with a full implementation including:
|
|
- Monospace text editor with line numbers
|
|
- Error highlighting (red line numbers for error lines)
|
|
- Run/Clear buttons with status indicator
|
|
- Collapsible command reference
|
|
- Error display with line context and suggestions
|
|
|
|
- [ ] **Step 4: Handle script messages in update()**
|
|
|
|
- [ ] **Step 5: Test compilation**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/panels/script_panel.rs hcie-iced-app/crates/hcie-iced-gui/src/app.rs
|
|
git commit -m "feat(iced): implement full script editor with line numbers and error highlighting"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: AI Script Generator
|
|
|
|
### Task 6: Implement AI Script Generator with Working LLM Calls
|
|
|
|
**Covers:** AI script generation with provider selection and API calls
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/ai_script.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add AI script state and messages
|
|
|
|
**Interfaces:**
|
|
- Consumes: `reqwest`, `tokio`, `script::parse_dsl_script`, `script::execute_actions`
|
|
- Produces: Generated DSL scripts, execution results
|
|
|
|
- [ ] **Step 1: Create ai_script.rs**
|
|
|
|
Port the full AI script generator from `egui-panel-ai-script/src/lib.rs` (373 lines) to ICED, including:
|
|
- Provider selection (Ollama, Claude, OpenAI)
|
|
- API calls for script generation
|
|
- System prompt for HCIE DSL
|
|
- Script cleaning and validation
|
|
- Run/Copy functionality
|
|
|
|
- [ ] **Step 2: Add AI script state to app.rs**
|
|
|
|
```rust
|
|
pub ai_script_prompt: String,
|
|
pub ai_script_output: String,
|
|
pub ai_script_error: Option<String>,
|
|
pub ai_script_is_running: bool,
|
|
pub ai_script_provider: String,
|
|
pub ai_script_model: String,
|
|
pub ai_script_url: String,
|
|
```
|
|
|
|
- [ ] **Step 3: Add AI script message variants**
|
|
|
|
```rust
|
|
AiScriptPromptChanged(String),
|
|
AiScriptProviderChanged(String),
|
|
AiScriptModelChanged(String),
|
|
AiScriptUrlChanged(String),
|
|
AiScriptGenerate,
|
|
AiScriptRun,
|
|
AiScriptCopy,
|
|
AiScriptClear,
|
|
AiScriptResult(Result<String, String>),
|
|
```
|
|
|
|
- [ ] **Step 4: Implement full ai_script_panel.rs**
|
|
|
|
- [ ] **Step 5: Test compilation**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/ai_script.rs hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs hcie-iced-app/crates/hcie-iced-gui/src/app.rs
|
|
git commit -m "feat(iced): implement AI script generator with working LLM calls"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Viewer Mode
|
|
|
|
### Task 7: Implement FastStone-like Image Viewer
|
|
|
|
**Covers:** Directory browser, thumbnail grid, preview panel, keyboard navigation
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/viewer/mod.rs`
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/viewer/directory_tree.rs`
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/viewer/thumbnail_grid.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add viewer state and mode
|
|
|
|
**Interfaces:**
|
|
- Consumes: `std::path::PathBuf`, `image` crate
|
|
- Produces: Viewer UI with directory navigation and image preview
|
|
|
|
- [ ] **Step 1: Create viewer/mod.rs**
|
|
|
|
```rust
|
|
pub mod directory_tree;
|
|
pub mod thumbnail_grid;
|
|
|
|
use crate::app::Message;
|
|
use crate::theme::ThemeColors;
|
|
use iced::widget::{column, container, row, scrollable, text};
|
|
use iced::{Element, Length};
|
|
|
|
/// Viewer mode state.
|
|
pub struct ViewerState {
|
|
pub current_dir: std::path::PathBuf,
|
|
pub selected_file: Option<std::path::PathBuf>,
|
|
pub thumbnails: Vec<ThumbnailEntry>,
|
|
pub is_fullscreen: bool,
|
|
}
|
|
|
|
pub struct ThumbnailEntry {
|
|
pub path: std::path::PathBuf,
|
|
pub name: String,
|
|
pub is_directory: bool,
|
|
pub thumbnail: Option<Vec<u8>>,
|
|
}
|
|
|
|
impl ViewerState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
current_dir: dirs::home_dir().unwrap_or_default(),
|
|
selected_file: None,
|
|
thumbnails: Vec::new(),
|
|
is_fullscreen: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn view(state: &ViewerState, colors: ThemeColors) -> Element<'static, Message> {
|
|
let dir_tree = directory_tree::view(&state.current_dir, colors);
|
|
let thumbnails = thumbnail_grid::view(&state.thumbnails, colors);
|
|
|
|
row![
|
|
container(dir_tree).width(Length::Fixed(200.0)),
|
|
container(thumbnails).width(Length::Fill),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create directory_tree.rs**
|
|
|
|
Implement directory navigation with tree view.
|
|
|
|
- [ ] **Step 3: Create thumbnail_grid.rs**
|
|
|
|
Implement thumbnail grid with lazy loading.
|
|
|
|
- [ ] **Step 4: Add viewer state to app.rs and viewer mode toggle**
|
|
|
|
- [ ] **Step 5: Test compilation**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/viewer/
|
|
git commit -m "feat(iced): implement FastStone-like image viewer"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 6: Auxiliary Features
|
|
|
|
### Task 8: Add Dock Profile Management
|
|
|
|
**Covers:** Save/load/rename/delete dock profiles
|
|
|
|
**Files:**
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add dock profile messages
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/dialogs/dock_profile.rs`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `serde_json`, `std::fs`
|
|
- Produces: Dock profile CRUD operations
|
|
|
|
- [ ] **Step 1: Create dock_profile.rs dialog**
|
|
|
|
- [ ] **Step 2: Add dock profile message variants**
|
|
|
|
- [ ] **Step 3: Handle dock profile messages in update()**
|
|
|
|
- [ ] **Step 4: Test compilation**
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/dialogs/dock_profile.rs hcie-iced-app/crates/hcie-iced-gui/src/app.rs
|
|
git commit -m "feat(iced): add dock profile management"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Add ABR Brush Import
|
|
|
|
**Covers:** Parse Photoshop .abr files and import brush presets
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/brush_import.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs` — add import button
|
|
|
|
**Interfaces:**
|
|
- Consumes: `std::fs::File`, `std::io::Read`
|
|
- Produces: `Vec<BrushPreset>`
|
|
|
|
- [ ] **Step 1: Port ABR parser from egui**
|
|
|
|
Port `brush_import.rs` (631 lines) from egui to ICED.
|
|
|
|
- [ ] **Step 2: Add import button to brushes panel**
|
|
|
|
- [ ] **Step 3: Test compilation**
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/brush_import.rs hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs
|
|
git commit -m "feat(iced): add ABR brush import"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Add Pressure Indicator HUD
|
|
|
|
**Covers:** HUD showing current pen pressure during strokes
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/panels/pressure_indicator.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs` — render pressure indicator
|
|
|
|
**Interfaces:**
|
|
- Consumes: `tablet_state: Arc<Mutex<TabletState>>`
|
|
- Produces: Pressure indicator overlay
|
|
|
|
- [ ] **Step 1: Create pressure_indicator.rs**
|
|
|
|
- [ ] **Step 2: Integrate into canvas view**
|
|
|
|
- [ ] **Step 3: Test compilation**
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/panels/pressure_indicator.rs hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs
|
|
git commit -m "feat(iced): add pressure indicator HUD"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Add Internationalization Support
|
|
|
|
**Covers:** English/Turkish translation keys
|
|
|
|
**Files:**
|
|
- Create: `hcie-iced-app/crates/hcie-iced-gui/src/i18n.rs`
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add language state
|
|
|
|
**Interfaces:**
|
|
- Consumes: `std::collections::HashMap<String, String>`
|
|
- Produces: Translated UI strings
|
|
|
|
- [ ] **Step 1: Create i18n.rs with translation keys**
|
|
|
|
Port from egui's `i18n.rs` (76 lines).
|
|
|
|
- [ ] **Step 2: Add language state to app.rs**
|
|
|
|
- [ ] **Step 3: Test compilation**
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/i18n.rs hcie-iced-app/crates/hcie-iced-gui/src/app.rs
|
|
git commit -m "feat(iced): add internationalization support"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 7: Polish and Integration
|
|
|
|
### Task 12: Add Missing Dialogs
|
|
|
|
**Covers:** Image Size, Canvas Size, About dialogs
|
|
|
|
**Files:**
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/dialogs/` — add missing dialogs
|
|
|
|
- [ ] **Step 1: Add Image Size dialog**
|
|
|
|
- [ ] **Step 2: Add Canvas Size dialog**
|
|
|
|
- [ ] **Step 3: Add About dialog**
|
|
|
|
- [ ] **Step 4: Test compilation**
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/dialogs/
|
|
git commit -m "feat(iced): add missing dialogs (Image Size, Canvas Size, About)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 13: Add Keyboard Shortcuts
|
|
|
|
**Covers:** All keyboard shortcuts from egui
|
|
|
|
**Files:**
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — add keyboard handling in subscription()
|
|
|
|
- [ ] **Step 1: Add keyboard shortcut handling**
|
|
|
|
Port all keyboard shortcuts from egui's `menus.rs` (903 lines) to ICED's subscription system.
|
|
|
|
- [ ] **Step 2: Test compilation**
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add hcie-iced-app/crates/hcie-iced-gui/src/app.rs
|
|
git commit -m "feat(iced): add all keyboard shortcuts from egui"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 14: Final Integration Test
|
|
|
|
**Covers:** End-to-end testing of all migrated features
|
|
|
|
**Files:**
|
|
- Modify: `hcie-iced-app/crates/hcie-iced-gui/tests/` — add integration tests
|
|
|
|
- [ ] **Step 1: Run full build**
|
|
|
|
Run: `cargo build -p hcie-iced-gui`
|
|
Expected: Compiles without errors
|
|
|
|
- [ ] **Step 2: Run existing tests**
|
|
|
|
Run: `cargo test -p hcie-iced-gui`
|
|
Expected: All tests pass
|
|
|
|
- [ ] **Step 3: Manual verification**
|
|
|
|
Launch the app and verify:
|
|
- Filter parameter panels work for all filters
|
|
- AI chat with streaming works
|
|
- Script editor with parser/executor works
|
|
- AI script generator works
|
|
- Viewer mode works
|
|
- All dialogs work
|
|
- Keyboard shortcuts work
|
|
|
|
- [ ] **Step 4: Final commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(iced): complete GUI migration from egui to ICED"
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Phase | Tasks | Description |
|
|
|-------|-------|-------------|
|
|
| 1 | 1-2 | Filter parameter panels + instant preview |
|
|
| 2 | 3 | AI chat with streaming SSE |
|
|
| 3 | 4-5 | Full script editor with parser/executor |
|
|
| 4 | 6 | AI script generator |
|
|
| 5 | 7 | Viewer mode |
|
|
| 6 | 8-11 | Auxiliary features (dock profiles, brush import, pressure HUD, i18n) |
|
|
| 7 | 12-14 | Polish and integration |
|
|
|
|
**Total Tasks:** 14
|
|
**Estimated Effort:** Large (multiple days of focused work)
|
|
**Critical Path:** Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7
|