From c7082046aaead3a14d50e339445737277c34704e Mon Sep 17 00:00:00 2001 From: Halit Can Date: Wed, 15 Jul 2026 16:11:27 +0300 Subject: [PATCH] feat(iced): add filter parameter panels for all 25+ filters - Create filter_params.rs with per-filter parameter editing UI - Support all FilterType variants: blur, sharpen, pixelate, distort, stylize, color/light adjustments, dehaze, noise pattern - Float/int sliders with labels and numeric display - Toggle buttons for boolean params and mode selectors - Integrate parameter panel into filters.rs below the filter list - Update dock/view.rs to pass filter_params to the filters panel --- .../crates/hcie-iced-gui/src/dock/view.rs | 2 +- .../hcie-iced-gui/src/panels/filter_params.rs | 688 ++++++++++++++++++ .../hcie-iced-gui/src/panels/filters.rs | 21 +- .../crates/hcie-iced-gui/src/panels/mod.rs | 1 + 4 files changed, 710 insertions(+), 2 deletions(-) create mode 100644 hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs index abac0ec..b30fa04 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs @@ -66,7 +66,7 @@ pub fn dock_view<'a>( ) } PaneType::Filters => { - crate::panels::filters::view(app.selected_filter, colors) + crate::panels::filters::view(app.selected_filter, &app.filter_params, colors) } PaneType::ColorPicker => { crate::color_picker::view(&app.fg_color, &app.bg_color, &app.recent_colors, app.color_tab) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs new file mode 100644 index 0000000..e07c809 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs @@ -0,0 +1,688 @@ +//! Filter parameter panels — per-filter parameter editing UI. +//! +//! **Purpose:** Provides slider/input controls for each of the 25+ filter types, +//! allowing users to tune filter parameters in real-time with live preview. +//! +//! **Pattern:** Each filter gets a dedicated view function that reads current +//! parameter values from a `serde_json::Value` and emits +//! `Message::FilterParamChanged(key, value)` on user interaction. +//! Default values match the engine's `unwrap_or` fallbacks for consistency. + +use crate::app::Message; +use crate::theme::ThemeColors; +use hcie_engine_api::FilterType; +use iced::widget::{column, container, row, slider, text}; +use iced::{Element, Length}; +use serde_json::json; + +/// Read a float parameter from the JSON params, falling back to a default. +fn param_f64(params: &serde_json::Value, key: &str, default: f64) -> f64 { + params + .get(key) + .and_then(|v| v.as_f64()) + .unwrap_or(default) +} + +/// Read an integer parameter from the JSON params, falling back to a default. +fn param_u64(params: &serde_json::Value, key: &str, default: u64) -> u64 { + params + .get(key) + .and_then(|v| v.as_u64()) + .unwrap_or(default) +} + +/// Read a boolean parameter from the JSON params, falling back to a default. +fn param_bool(params: &serde_json::Value, key: &str, default: bool) -> bool { + params + .get(key) + .and_then(|v| v.as_bool()) + .unwrap_or(default) +} + +/// Read a string parameter from the JSON params, falling back to a default. +fn param_str<'a>(params: &'a serde_json::Value, key: &str, default: &'a str) -> &'a str { + params + .get(key) + .and_then(|v| v.as_str()) + .unwrap_or(default) +} + +/// Build a labeled float slider row. +/// +/// Returns a row with label, slider, and numeric display. The slider +/// emits `Message::FilterParamChanged(filter_id, json!({key: new_value}))` +/// when the user drags it. +fn float_slider( + label: &str, + key: &str, + filter_id: &str, + value: f64, + min: f64, + max: f64, + step: f64, + _colors: ThemeColors, +) -> Element<'static, Message> { + let val = value as f32; + let min_f = min as f32; + let max_f = max as f32; + let step_f = step as f32; + let key_owned = key.to_string(); + let filter_owned = filter_id.to_string(); + let label_owned = label.to_string(); + + let lbl = text(label_owned).size(11).width(Length::Fixed(80.0)); + let display = text(format!("{:.1}", value)).size(11).width(Length::Fixed(40.0)); + + let s = slider(min_f..=max_f, val, move |v| { + let rounded = (v as f64 / step).round() * step; + Message::FilterParamChanged( + filter_owned.clone(), + json!({ key_owned.clone(): rounded }), + ) + }) + .step(step_f) + .width(Length::Fill); + + row![lbl, s, display].spacing(4).into() +} + +/// Build a labeled integer slider row. +fn int_slider( + label: &str, + key: &str, + filter_id: &str, + value: u64, + min: u64, + max: u64, + _colors: ThemeColors, +) -> Element<'static, Message> { + let val = value as f32; + let min_f = min as f32; + let max_f = max as f32; + let key_owned = key.to_string(); + let filter_owned = filter_id.to_string(); + let label_owned = label.to_string(); + + let lbl = text(label_owned).size(11).width(Length::Fixed(80.0)); + let display = text(format!("{}", value)).size(11).width(Length::Fixed(40.0)); + + let s = slider(min_f..=max_f, val, move |v| { + Message::FilterParamChanged( + filter_owned.clone(), + json!({ key_owned.clone(): v.round() as u64 }), + ) + }) + .step(1.0) + .width(Length::Fill); + + row![lbl, s, display].spacing(4).into() +} + +/// Build a section header for a group of parameters. +fn section_header(label: &str, colors: ThemeColors) -> Element<'static, Message> { + let label_owned = label.to_string(); + container(text(label_owned).size(11).style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.text_secondary), + })) + .padding(iced::Padding::default().top(4.0).bottom(2.0)) + .into() +} + +/// Build the parameter panel for a specific filter type. +/// +/// Returns a column of slider/input widgets tailored to the filter's parameters. +/// The panel reads current values from `params` and emits +/// `Message::FilterParamChanged` on user interaction. +pub fn view( + filter: FilterType, + params: &serde_json::Value, + colors: ThemeColors, +) -> Element<'static, Message> { + let content: Element<'static, Message> = match filter { + // ── Blur ───────────────────────────────────────────────────────── + FilterType::BoxBlur | FilterType::BoxBlurGamma => { + let filter_id = if filter == FilterType::BoxBlur { + "box_blur" + } else { + "box_blur_gamma" + }; + let radius = param_u64(params, "radius", 3); + column![ + section_header("Box Blur", colors), + int_slider("Radius", "radius", filter_id, radius, 1, 100, colors), + ] + .spacing(2) + .into() + } + FilterType::GaussianBlur | FilterType::GaussianBlurGamma => { + let filter_id = if filter == FilterType::GaussianBlur { + "gaussian_blur" + } else { + "gaussian_blur_gamma" + }; + let radius = param_f64(params, "radius", 2.0); + column![ + section_header("Gaussian Blur", colors), + float_slider("Radius", "radius", filter_id, radius, 0.1, 50.0, 0.1, colors), + ] + .spacing(2) + .into() + } + FilterType::MotionBlur => { + let angle = param_f64(params, "angle", 0.0); + let distance = param_u64(params, "distance", 10); + column![ + section_header("Motion Blur", colors), + float_slider("Angle", "angle", "motion_blur", angle, 0.0, 360.0, 1.0, colors), + int_slider("Distance", "distance", "motion_blur", distance, 1, 500, colors), + ] + .spacing(2) + .into() + } + FilterType::MedianFilter => { + let radius = param_u64(params, "radius", 1); + column![ + section_header("Median Filter", colors), + int_slider("Radius", "radius", "median_filter", radius, 1, 20, colors), + ] + .spacing(2) + .into() + } + + // ── Sharpen ────────────────────────────────────────────────────── + FilterType::UnsharpMask => { + let amount = param_f64(params, "amount", 1.0); + let radius = param_f64(params, "radius", 1.0); + column![ + section_header("Unsharp Mask", colors), + float_slider("Amount", "amount", "sharpen", amount, 0.1, 5.0, 0.1, colors), + float_slider("Radius", "radius", "sharpen", radius, 0.1, 20.0, 0.1, colors), + ] + .spacing(2) + .into() + } + + // ── Pixelate ───────────────────────────────────────────────────── + FilterType::Mosaic => { + let size = param_u64(params, "size", 8); + column![ + section_header("Mosaic", colors), + int_slider("Size", "size", "mosaic", size, 2, 128, colors), + ] + .spacing(2) + .into() + } + FilterType::Crystallize => { + let size = param_u64(params, "size", 16); + column![ + section_header("Crystallize", colors), + int_slider("Cell Size", "size", "crystallize", size, 4, 128, colors), + ] + .spacing(2) + .into() + } + + // ── Distort ────────────────────────────────────────────────────── + FilterType::Pinch => { + let amount = param_f64(params, "amount", 0.5); + column![ + section_header("Pinch", colors), + float_slider("Amount", "amount", "pinch", amount, -1.0, 1.0, 0.01, colors), + ] + .spacing(2) + .into() + } + FilterType::Twirl => { + let angle = param_f64(params, "angle", 45.0); + column![ + section_header("Twirl", colors), + float_slider("Angle", "angle", "twirl", angle, -360.0, 360.0, 1.0, colors), + ] + .spacing(2) + .into() + } + + // ── Stylize ────────────────────────────────────────────────────── + FilterType::OilPaint => { + let radius = param_u64(params, "radius", 3); + column![ + section_header("Oil Paint", colors), + int_slider("Radius", "radius", "oil_paint", radius, 1, 20, colors), + ] + .spacing(2) + .into() + } + + // ── Color & Light ──────────────────────────────────────────────── + FilterType::Levels => { + let shadows = param_f64(params, "shadows", 0.0); + let midtones = param_f64(params, "midtones", 50.0); + let highlights = param_f64(params, "highlights", 100.0); + let out_min = param_f64(params, "output_min", 0.0); + let out_max = param_f64(params, "output_max", 100.0); + column![ + section_header("Levels", colors), + float_slider("Shadows", "shadows", "levels", shadows, 0.0, 255.0, 1.0, colors), + float_slider("Midtones", "midtones", "levels", midtones, 0.0, 255.0, 1.0, colors), + float_slider("Highlights", "highlights", "levels", highlights, 0.0, 255.0, 1.0, colors), + float_slider("Out Min", "output_min", "levels", out_min, 0.0, 255.0, 1.0, colors), + float_slider("Out Max", "output_max", "levels", out_max, 0.0, 255.0, 1.0, colors), + ] + .spacing(2) + .into() + } + FilterType::Vibrance => { + let vibrance = param_f64(params, "vibrance", 0.0); + column![ + section_header("Vibrance", colors), + float_slider("Vibrance", "vibrance", "vibrance", vibrance, -100.0, 100.0, 1.0, colors), + ] + .spacing(2) + .into() + } + FilterType::Exposure => { + let exposure = param_f64(params, "exposure", 0.0); + let offset = param_f64(params, "offset", 0.0); + let gamma = param_f64(params, "gamma_correction", 1.0); + column![ + section_header("Exposure", colors), + float_slider("Exposure", "exposure", "exposure", exposure, -5.0, 5.0, 0.01, colors), + float_slider("Offset", "offset", "exposure", offset, -1.0, 1.0, 0.01, colors), + float_slider("Gamma", "gamma_correction", "exposure", gamma, 0.1, 5.0, 0.01, colors), + ] + .spacing(2) + .into() + } + FilterType::Posterize => { + let levels = param_u64(params, "levels", 4); + column![ + section_header("Posterize", colors), + int_slider("Levels", "levels", "posterize", levels, 2, 32, colors), + ] + .spacing(2) + .into() + } + FilterType::Threshold => { + let min_lum = param_f64(params, "min_luminance", 50.0); + let max_lum = param_f64(params, "max_luminance", 100.0); + column![ + section_header("Threshold", colors), + float_slider("Min Luminance", "min_luminance", "threshold", min_lum, 0.0, 100.0, 1.0, colors), + float_slider("Max Luminance", "max_luminance", "threshold", max_lum, 0.0, 100.0, 1.0, colors), + ] + .spacing(2) + .into() + } + FilterType::ChannelMixer => { + let mono = param_bool(params, "monochrome", false); + let mono_r = param_f64(params, "monochrome_r", 40.0); + let mono_g = param_f64(params, "monochrome_g", 40.0); + let mono_b = param_f64(params, "monochrome_b", 20.0); + let mono_c = param_f64(params, "monochrome_constant", 0.0); + let mut col = column![ + section_header("Channel Mixer", colors), + ] + .spacing(2); + + // Monochrome toggle + let filter_id = "channel_mixer"; + { + let mono_val = mono; + let lbl = text("Monochrome").size(11).width(Length::Fixed(80.0)); + let toggle_text = if mono_val { "ON" } else { "OFF" }; + let toggle_btn = iced::widget::button(text(toggle_text).size(11)) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "monochrome": !mono_val }), + )) + .padding([2, 8]); + col = col.push(row![lbl, toggle_btn].spacing(4)); + } + + if mono { + col = col.push(float_slider("Monochrome R", "monochrome_r", filter_id, mono_r, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Monochrome G", "monochrome_g", filter_id, mono_g, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Monochrome B", "monochrome_b", filter_id, mono_b, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Constant", "monochrome_constant", filter_id, mono_c, -200.0, 200.0, 1.0, colors)); + } else { + // RGB channels + let red_r = param_f64(params, "red_r", 100.0); + let red_g = param_f64(params, "red_g", 0.0); + let red_b = param_f64(params, "red_b", 0.0); + let red_c = param_f64(params, "red_constant", 0.0); + let green_r = param_f64(params, "green_r", 0.0); + let green_g = param_f64(params, "green_g", 100.0); + let green_b = param_f64(params, "green_b", 0.0); + let green_c = param_f64(params, "green_constant", 0.0); + let blue_r = param_f64(params, "blue_r", 0.0); + let blue_g = param_f64(params, "blue_g", 0.0); + let blue_b = param_f64(params, "blue_b", 100.0); + let blue_c = param_f64(params, "blue_constant", 0.0); + + col = col.push(section_header("Red Channel", colors)); + col = col.push(float_slider("Red→R", "red_r", filter_id, red_r, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Red→G", "red_g", filter_id, red_g, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Red→B", "red_b", filter_id, red_b, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Red Const", "red_constant", filter_id, red_c, -200.0, 200.0, 1.0, colors)); + + col = col.push(section_header("Green Channel", colors)); + col = col.push(float_slider("Green→R", "green_r", filter_id, green_r, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Green→G", "green_g", filter_id, green_g, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Green→B", "green_b", filter_id, green_b, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Green Const", "green_constant", filter_id, green_c, -200.0, 200.0, 1.0, colors)); + + col = col.push(section_header("Blue Channel", colors)); + col = col.push(float_slider("Blue→R", "blue_r", filter_id, blue_r, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Blue→G", "blue_g", filter_id, blue_g, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Blue→B", "blue_b", filter_id, blue_b, -200.0, 200.0, 1.0, colors)); + col = col.push(float_slider("Blue Const", "blue_constant", filter_id, blue_c, -200.0, 200.0, 1.0, colors)); + } + + col.into() + } + FilterType::BlackAndWhite => { + let reds = param_f64(params, "reds", 40.0); + let yellows = param_f64(params, "yellows", 60.0); + let greens = param_f64(params, "greens", 40.0); + let cyans = param_f64(params, "cyans", 60.0); + let blues = param_f64(params, "blues", 20.0); + let magentas = param_f64(params, "magentas", 80.0); + let tint_r = param_f64(params, "tint_r", 255.0); + let tint_g = param_f64(params, "tint_g", 255.0); + let tint_b = param_f64(params, "tint_b", 255.0); + column![ + section_header("Black & White", colors), + float_slider("Reds", "reds", "black_and_white", reds, 0.0, 100.0, 1.0, colors), + float_slider("Yellows", "yellows", "black_and_white", yellows, 0.0, 100.0, 1.0, colors), + float_slider("Greens", "greens", "black_and_white", greens, 0.0, 100.0, 1.0, colors), + float_slider("Cyans", "cyans", "black_and_white", cyans, 0.0, 100.0, 1.0, colors), + float_slider("Blues", "blues", "black_and_white", blues, 0.0, 100.0, 1.0, colors), + float_slider("Magentas", "magentas", "black_and_white", magentas, 0.0, 100.0, 1.0, colors), + section_header("Tint Color", colors), + float_slider("Tint R", "tint_r", "black_and_white", tint_r, 0.0, 255.0, 1.0, colors), + float_slider("Tint G", "tint_g", "black_and_white", tint_g, 0.0, 255.0, 1.0, colors), + float_slider("Tint B", "tint_b", "black_and_white", tint_b, 0.0, 255.0, 1.0, colors), + ] + .spacing(2) + .into() + } + FilterType::SelectiveColor => { + let filter_id = "selective_color"; + // Mode toggle + let mode = param_str(params, "mode", "relative"); + let is_absolute = mode == "absolute"; + + let mut col = column![ + section_header("Selective Color", colors), + ] + .spacing(2); + + // Mode toggle + { + let lbl = text("Mode").size(11).width(Length::Fixed(80.0)); + let toggle_btn = iced::widget::button(text(mode.to_string()).size(11)) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "mode": if is_absolute { "relative" } else { "absolute" } }), + )) + .padding([2, 8]); + col = col.push(row![lbl, toggle_btn].spacing(4)); + } + + // Per-color-range CMYK sliders + let ranges = [ + ("Reds", "reds"), + ("Yellows", "yellows"), + ("Greens", "greens"), + ("Cyans", "cyans"), + ("Blues", "blues"), + ("Magentas", "magentas"), + ("Whites", "whites"), + ("Neutrals", "neutrals"), + ("Blacks", "blacks"), + ]; + let cmyk = [("Cyan", "cyan"), ("Magenta", "magenta"), ("Yellow", "yellow"), ("Black", "black")]; + + for (range_name, range_key) in &ranges { + col = col.push(section_header(range_name, colors)); + for (cmyk_name, cmyk_key) in &cmyk { + let param_key = format!("{}_{}", range_key, cmyk_key); + let val = param_f64(params, ¶m_key, 0.0); + col = col.push(float_slider( + cmyk_name, + ¶m_key, + filter_id, + val, + -100.0, + 100.0, + 1.0, + colors, + )); + } + } + + col.into() + } + FilterType::GammaCorrection => { + let gamma = param_f64(params, "gamma", 2.2); + let inverse = param_bool(params, "inverse", false); + let mut col = column![ + section_header("Gamma Correction", colors), + float_slider("Gamma", "gamma", "gamma_correction", gamma, 0.1, 5.0, 0.1, colors), + ] + .spacing(2); + + { + let lbl = text("Inverse").size(11).width(Length::Fixed(80.0)); + let toggle_text = if inverse { "ON" } else { "OFF" }; + let toggle_btn = iced::widget::button(text(toggle_text).size(11)) + .on_press(Message::FilterParamChanged( + "gamma_correction".to_string(), + json!({ "inverse": !inverse }), + )) + .padding([2, 8]); + col = col.push(row![lbl, toggle_btn].spacing(4)); + } + + col.into() + } + FilterType::ExtractChannel => { + let channel = param_str(params, "channel", "red"); + let channels = ["red", "green", "blue", "alpha"]; + let mut col = column![ + section_header("Extract Channel", colors), + ] + .spacing(2); + + for ch in &channels { + let is_active = channel == *ch; + let filter_id = "extract_channel"; + let ch_label = text(ch.to_string()).size(11).width(Length::Fixed(80.0)); + let btn = iced::widget::button(text(if is_active { format!(">> {}", ch) } else { ch.to_string() }).size(11)) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "channel": ch }), + )) + .padding([2, 8]); + col = col.push(row![ch_label, btn].spacing(4)); + } + + col.into() + } + FilterType::GradientMap => { + let reverse = param_bool(params, "reverse", false); + column![ + section_header("Gradient Map", colors), + { + let lbl = text("Reverse").size(11).width(Length::Fixed(80.0)); + let toggle_text = if reverse { "ON" } else { "OFF" }; + let toggle_btn = iced::widget::button(text(toggle_text).size(11)) + .on_press(Message::FilterParamChanged( + "gradient_map".to_string(), + json!({ "reverse": !reverse }), + )) + .padding([2, 8]); + row![lbl, toggle_btn].spacing(4) + }, + { + text("Gradient stops configured via color picker").size(10) + }, + ] + .spacing(2) + .into() + } + + // ── Restore ────────────────────────────────────────────────────── + FilterType::Dehaze => { + let strength = param_f64(params, "strength", 50.0); + column![ + section_header("Dehaze", colors), + float_slider("Strength", "strength", "dehaze", strength, 0.0, 100.0, 1.0, colors), + ] + .spacing(2) + .into() + } + + // ── Noise & Pattern ────────────────────────────────────────────── + FilterType::NoisePattern => { + let filter_id = "noise_pattern"; + let scale = param_f64(params, "scale", 10.0); + let reverse = param_bool(params, "reverse", false); + let noise_type = param_str(params, "noise_type", "perlin"); + let fractal_type = param_str(params, "fractal_type", "none"); + let fractal_octaves = param_u64(params, "fractal_octaves", 3); + let fractal_lacunarity = param_f64(params, "fractal_lacunarity", 2.0); + let fractal_gain = param_f64(params, "fractal_gain", 0.5); + let domain_warp_type = param_str(params, "domain_warp_type", "none"); + let domain_warp_amp = param_f64(params, "domain_warp_amplitude", 100.0); + let cellular_distance = param_str(params, "cellular_distance_function", "euclidean"); + let cellular_return = param_str(params, "cellular_return_type", "nearest"); + let cellular_jitter = param_f64(params, "cellular_jitter", 1.0); + + let mut col = column![ + section_header("Noise Pattern", colors), + float_slider("Scale", "scale", filter_id, scale, 0.1, 100.0, 0.1, colors), + ] + .spacing(2); + + // Reverse toggle + { + let lbl = text("Reverse").size(11).width(Length::Fixed(80.0)); + let toggle_text = if reverse { "ON" } else { "OFF" }; + let toggle_btn = iced::widget::button(text(toggle_text).size(11)) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "reverse": !reverse }), + )) + .padding([2, 8]); + col = col.push(row![lbl, toggle_btn].spacing(4)); + } + + // Noise type selector + { + let noise_types = ["value", "perlin", "simplex", "cellular"]; + col = col.push(section_header("Noise Type", colors)); + for nt in &noise_types { + let is_active = noise_type == *nt; + let btn = iced::widget::button( + text(if is_active { format!(">> {}", nt) } else { nt.to_string() }).size(11), + ) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "noise_type": nt }), + )) + .padding([2, 8]); + col = col.push(btn); + } + } + + // Fractal type selector + { + let fractal_types = ["none", "fbm", "ridged", "ping_pong", "domain_warp"]; + col = col.push(section_header("Fractal Type", colors)); + for ft in &fractal_types { + let is_active = fractal_type == *ft; + let btn = iced::widget::button( + text(if is_active { format!(">> {}", ft) } else { ft.to_string() }).size(11), + ) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "fractal_type": ft }), + )) + .padding([2, 8]); + col = col.push(btn); + } + } + + if fractal_type != "none" { + col = col.push(int_slider("Octaves", "fractal_octaves", filter_id, fractal_octaves, 1, 10, colors)); + col = col.push(float_slider("Lacunarity", "fractal_lacunarity", filter_id, fractal_lacunarity, 1.0, 4.0, 0.1, colors)); + col = col.push(float_slider("Gain", "fractal_gain", filter_id, fractal_gain, 0.0, 2.0, 0.01, colors)); + } + + // Domain warp selector + { + let warp_types = ["none", "basic", "general"]; + col = col.push(section_header("Domain Warp", colors)); + for wt in &warp_types { + let is_active = domain_warp_type == *wt; + let btn = iced::widget::button( + text(if is_active { format!(">> {}", wt) } else { wt.to_string() }).size(11), + ) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "domain_warp_type": wt }), + )) + .padding([2, 8]); + col = col.push(btn); + } + } + + if domain_warp_type != "none" { + col = col.push(float_slider("Warp Amp", "domain_warp_amplitude", filter_id, domain_warp_amp, 0.0, 500.0, 1.0, colors)); + } + + // Cellular settings (shown when noise_type is "cellular") + if noise_type == "cellular" { + col = col.push(section_header("Cellular", colors)); + // Distance function + let dist_funcs = ["euclidean", "manhattan", "chebyshev", "euclidean_sq"]; + for df in &dist_funcs { + let is_active = cellular_distance == *df; + let btn = iced::widget::button( + text(if is_active { format!(">> {}", df) } else { df.to_string() }).size(11), + ) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "cellular_distance_function": df }), + )) + .padding([2, 8]); + col = col.push(btn); + } + // Return type + let ret_types = ["nearest", "second", "distance", "difference", "sum"]; + col = col.push(section_header("Return Type", colors)); + for rt in &ret_types { + let is_active = cellular_return == *rt; + let btn = iced::widget::button( + text(if is_active { format!(">> {}", rt) } else { rt.to_string() }).size(11), + ) + .on_press(Message::FilterParamChanged( + filter_id.to_string(), + json!({ "cellular_return_type": rt }), + )) + .padding([2, 8]); + col = col.push(btn); + } + col = col.push(float_slider("Jitter", "cellular_jitter", filter_id, cellular_jitter, 0.0, 1.0, 0.01, colors)); + } + + col.into() + } + }; + + container(content) + .width(Length::Fill) + .padding(4) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs index 7395c74..4bf040f 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs @@ -86,7 +86,15 @@ const FILTER_CATEGORIES: &[FilterCategory] = &[ ]; /// Build the filters panel. -pub fn view(selected_filter: Option, colors: ThemeColors) -> Element<'static, Message> { +/// +/// Shows the filter category list on top, and when a filter is selected, +/// displays its parameter editing panel below the list. The Apply/Reset +/// buttons are always at the bottom. +pub fn view( + selected_filter: Option, + filter_params: &serde_json::Value, + colors: ThemeColors, +) -> Element<'static, Message> { let mut filter_list = column![].spacing(2); for category in FILTER_CATEGORIES { @@ -139,9 +147,20 @@ pub fn view(selected_filter: Option, colors: ThemeColors) -> Element let buttons = row![apply_btn, reset_btn].spacing(8); + // When a filter is selected, show its parameter panel below the list. + let param_section = match selected_filter { + Some(filter) => { + let params_panel = super::filter_params::view(filter, filter_params, colors); + column![horizontal_rule(1), scrollable(params_panel).height(Length::Fill)] + .spacing(0) + } + None => column![], + }; + // Panel title is in the dock tab — no duplicate inside the panel. let panel = column![ scrollable(filter_list).height(Length::Fill), + param_section, horizontal_rule(1), buttons, ] diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs index 38b3e6d..ddced85 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs @@ -3,6 +3,7 @@ pub mod ai_chat_panel; pub mod ai_script_panel; pub mod brushes; +pub mod filter_params; pub mod filters; pub mod geometry; pub mod history;