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

45 KiB

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

//! 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:

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:

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
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:

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:

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
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

//! 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:

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:

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
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

//! 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
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
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:

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
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
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

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
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
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

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
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

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

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

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

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

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

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

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