Files
hcie-rust-v3.05/docs/compose/plans/2025-07-15-iced-full-gui-migration.md
T

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