//! Tool Settings panel — shows all properties for the active tool. //! //! **Purpose:** Right-side panel showing tool-specific settings like the reference images. //! - Brush: Size, Opacity, Hardness, Flow, Spacing //! - Eraser: Size, Opacity, Hardness //! - Vector: Fill, Stroke Width, Opacity, Radius/Points/Sides (shape-specific) //! - Select: Feather, Anti-alias //! - Magic Wand: Tolerance, Anti-alias, Contiguous //! - Text: Font, Size, Angle, Color, Alignment, Orientation //! - Gradient: Type, Opacity //! //! All controls emit real Messages wired to `app.rs` handlers; no `NoOp` remains. use crate::app::{Message, ToolState}; use crate::panels::styles; use crate::settings::AppSettings; use crate::theme::ThemeColors; use crate::widgets::plain_slider::plain_slider; use hcie_engine_api::Tool; use iced::widget::{button, checkbox, column, container, horizontal_rule, row, scrollable, text, text_input}; use iced::{Element, Length}; /// Build the tool settings panel for the active tool. pub fn view<'a>( tool_state: &'a ToolState, settings: &'a AppSettings, fg_color: [u8; 4], text_draft: Option<&'a crate::app::TextDraft>, colors: ThemeColors, ) -> Element<'a, Message> { let content = match tool_state.active_tool { Tool::Brush | Tool::Eraser | Tool::Pen => brush_settings(tool_state, settings, colors), Tool::Spray => spray_settings(tool_state, settings, colors), Tool::VectorLine | Tool::VectorRect | Tool::VectorCircle | Tool::VectorArrow | Tool::VectorStar | Tool::VectorPolygon | Tool::VectorRhombus | Tool::VectorCylinder | Tool::VectorHeart | Tool::VectorBubble | Tool::VectorGear | Tool::VectorCross | Tool::VectorCrescent | Tool::VectorBolt | Tool::VectorArrow4 => vector_settings(tool_state, settings, colors), Tool::Select | Tool::Lasso | Tool::PolygonSelect => select_settings(settings, colors), Tool::MagicWand | Tool::SmartSelect => magic_wand_settings(settings, colors), Tool::Text => text_settings(settings, fg_color, text_draft, colors), Tool::Move => move_settings(settings, colors), Tool::Gradient => gradient_settings(settings, colors), Tool::FloodFill => flood_fill_settings(settings, colors), _ => column![text("No settings").size(11)].spacing(4), }; let reset_btn = button(text("Reset to Defaults").size(10)) .on_press(Message::ResetToolDefaults) .padding([4, 8]) .width(Length::Fill); let panel = column![ scrollable(content).height(Length::Fill), horizontal_rule(1), reset_btn, ] .spacing(2) .padding(4); container(panel) .width(Length::Fill) .height(Length::Fill) .style(move |_theme| styles::panel_background(colors)) .into() } fn brush_settings( ts: &ToolState, settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); column![ section_header("Brush Settings", c), prop_slider( "Size", ts.brush_size, 1.0, 500.0, Message::BrushSizeChanged, c ), prop_slider( "Opacity", ts.brush_opacity * 100.0, 0.0, 100.0, |v| Message::BrushOpacityChanged(v / 100.0), c ), prop_slider( "Hardness", ts.brush_hardness * 100.0, 0.0, 100.0, |v| Message::BrushHardnessChanged(v / 100.0), c ), prop_slider( "Flow", ts2.brush_flow * 100.0, 0.0, 100.0, |v| Message::BrushFlowChanged(v / 100.0), c ), prop_slider( "Spacing", ts.brush_spacing * 100.0, 1.0, 100.0, |v| Message::BrushSpacingChanged(v / 100.0), c ), checkbox("Color Variant", ts.brush_color_variant) .on_toggle(Message::BrushColorVariantToggled) .size(11), prop_slider( "Variant Amount", ts.brush_variant_amount * 100.0, 0.0, 100.0, |v| Message::BrushVariantAmountChanged(v / 100.0), c ), checkbox("Cyclic Color", ts.brush_cyclic_color) .on_toggle(Message::BrushCyclicColorToggled) .size(11), prop_slider( "Cyclic Speed", ts.brush_cyclic_speed.clamp(0.01, 5.0), 0.01, 5.0, Message::BrushCyclicSpeedChanged, c ), ] .spacing(4) } fn spray_settings( ts: &ToolState, settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); column![ section_header("Spray Settings", c), prop_slider( "Size", ts.brush_size, 1.0, 500.0, Message::BrushSizeChanged, c ), prop_slider( "Particle Size", ts2.spray_particle_size, 1.0, 20.0, Message::SprayParticleSizeChanged, c ), prop_slider( "Density", ts2.spray_density as f32, 10.0, 1000.0, |v| Message::SprayDensityChanged(v as u32), c ), prop_slider( "Opacity", ts.brush_opacity * 100.0, 0.0, 100.0, |v| Message::BrushOpacityChanged(v / 100.0), c ), prop_slider( "Hardness", ts.brush_hardness * 100.0, 0.0, 100.0, |v| Message::BrushHardnessChanged(v / 100.0), c ), checkbox("Color Variant", ts.brush_color_variant) .on_toggle(Message::BrushColorVariantToggled) .size(11), prop_slider( "Variant Amount", ts.brush_variant_amount * 100.0, 0.0, 100.0, |v| Message::BrushVariantAmountChanged(v / 100.0), c ), checkbox("Cyclic Color", ts.brush_cyclic_color) .on_toggle(Message::BrushCyclicColorToggled) .size(11), prop_slider( "Cyclic Speed", ts.brush_cyclic_speed.clamp(0.01, 5.0), 0.01, 5.0, Message::BrushCyclicSpeedChanged, c ), ] .spacing(4) } fn vector_settings( ts: &ToolState, settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); let mut col = column![ section_header("Vector Settings", c), prop_slider( "Stroke Width", ts2.vector_stroke_width, 0.0, 50.0, Message::VectorStrokeChanged, c ), prop_slider( "Opacity", ts2.vector_opacity * 100.0, 0.0, 100.0, |v| Message::VectorOpacityChanged(v / 100.0), c ), iced::widget::checkbox("Fill", ts2.vector_fill) .on_toggle(Message::VectorFillToggled) .size(11), ] .spacing(4); // Shape-specific controls. match ts.active_tool { Tool::VectorRect => { col = col.push(prop_slider( "Corner Radius", ts2.vector_radius, 0.0, 100.0, Message::VectorRadiusChanged, c, )); } Tool::VectorStar => { col = col.push(prop_slider( "Points", ts2.vector_points as f32, 3.0, 20.0, |v| Message::VectorPointsChanged(v as u32), c, )); } Tool::VectorPolygon => { col = col.push(prop_slider( "Sides", ts2.vector_sides as f32, 3.0, 12.0, |v| Message::VectorSidesChanged(v as u32), c, )); } _ => {} } col } fn select_settings( settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); column![ section_header("Selection Settings", c), prop_slider( "Feather", ts2.select_feather, 0.0, 250.0, Message::SelectionFeatherChanged, c ), iced::widget::checkbox("Anti-alias", ts2.selection_anti_alias) .on_toggle(Message::SelectionAntiAliasToggled) .size(11), ] .spacing(4) } fn magic_wand_settings( settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); column![ section_header("Magic Wand Settings", c), prop_slider( "Tolerance", ts2.magic_wand_tolerance, 0.0, 255.0, |v| Message::SelectionToleranceChanged(v as u8), c ), iced::widget::checkbox("Contiguous", ts2.selection_contiguous) .on_toggle(Message::SelectionContiguousToggled) .size(11), iced::widget::checkbox("Anti-alias", ts2.selection_anti_alias) .on_toggle(Message::SelectionAntiAliasToggled) .size(11), ] .spacing(4) } fn text_settings( settings: &AppSettings, fg_color: [u8; 4], text_draft: Option<&crate::app::TextDraft>, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); let draft_font = text_draft .map(|d| d.font.clone()) .unwrap_or_else(|| ts2.text_font.clone()); let draft_size = text_draft.map(|d| d.size).unwrap_or(ts2.text_size); let draft_angle = text_draft.map(|d| d.angle).unwrap_or(ts2.text_angle); let draft_color = text_draft.map(|d| d.color).unwrap_or(fg_color); let draft_content = text_draft.map(|d| d.content.clone()).unwrap_or_default(); let _alignment = text_draft .map(|d| format!("{:?}", d.alignment)) .unwrap_or_else(|| "Left".to_string()); let _orientation = text_draft .map(|d| format!("{:?}", d.orientation)) .unwrap_or_else(|| "Horizontal".to_string()); column![ section_header("Text Settings", c), row![ text("Font:").size(10).width(Length::Fixed(50.0)), text_input("Font", &draft_font) .on_input(Message::TextFontChanged) .size(10) .width(Length::Fill), ] .spacing(4) .align_y(iced::Alignment::Center), prop_slider("Size", draft_size, 4.0, 200.0, Message::TextSizeChanged, c), prop_slider( "Angle", draft_angle, -180.0, 180.0, Message::TextAngleChanged, c ), color_row("Color:", draft_color, c), row![ text("Content:").size(10).width(Length::Fixed(50.0)), text_input("Text", &draft_content) .on_input(Message::TextContentChanged) .size(10) .width(Length::Fill), ] .spacing(4) .align_y(iced::Alignment::Center), row![ text("Align:").size(10).width(Length::Fixed(50.0)), button(text("Left").size(9)) .on_press(Message::TextAlignChanged("Left".to_string())) .padding([2, 4]), button(text("Center").size(9)) .on_press(Message::TextAlignChanged("Center".to_string())) .padding([2, 4]), button(text("Right").size(9)) .on_press(Message::TextAlignChanged("Right".to_string())) .padding([2, 4]), ] .spacing(3), row![ text("Orient:").size(10).width(Length::Fixed(50.0)), button(text("H").size(9)) .on_press(Message::TextOrientationChanged("Horizontal".to_string())) .padding([2, 6]), button(text("V").size(9)) .on_press(Message::TextOrientationChanged("Vertical".to_string())) .padding([2, 6]), ] .spacing(3), row![ button(text("Commit").size(10)) .on_press(Message::TextCommit) .padding([4, 10]), button(text("Cancel").size(10)) .on_press(Message::TextCancel) .padding([4, 10]), ] .spacing(6), ] .spacing(4) } fn move_settings(settings: &AppSettings, c: ThemeColors) -> iced::widget::Column<'static, Message> { let _ = settings; column![ section_header("Move Settings", c), iced::widget::checkbox("Apply to New Layer", true) .on_toggle(Message::ToolApplyToNewLayerToggled) .size(11), ] .spacing(4) } fn gradient_settings( settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); let gtype = ts2.gradient_type; column![ section_header("Gradient Settings", c), row![ text("Type:").size(10).width(Length::Fixed(40.0)), button(text("Linear").size(9)) .on_press(Message::GradientTypeChanged(0)) .padding([2, 6]), button(text("Radial").size(9)) .on_press(Message::GradientTypeChanged(1)) .padding([2, 6]), ] .spacing(3), prop_slider( "Opacity", ts2.gradient_opacity * 100.0, 0.0, 100.0, |v| Message::BrushOpacityChanged(v / 100.0), c ), text(if gtype == 1 { "Radial" } else { "Linear" }).size(9), ] .spacing(4) } fn flood_fill_settings( settings: &AppSettings, c: ThemeColors, ) -> iced::widget::Column<'static, Message> { let ts2 = settings.tool_settings.clone(); column![ section_header("Flood Fill Settings", c), prop_slider( "Tolerance", ts2.flood_fill_tolerance, 0.0, 255.0, |v| Message::SelectionToleranceChanged(v as u8), c ), iced::widget::checkbox("Contiguous", ts2.selection_contiguous) .on_toggle(Message::SelectionContiguousToggled) .size(11), ] .spacing(4) } fn section_header(label: &str, _c: ThemeColors) -> Element<'static, Message> { let l = label.to_string(); row![text(l).size(11)].spacing(4).into() } fn prop_slider( label: &str, value: f32, min: f32, max: f32, msg_fn: F, colors: ThemeColors, ) -> Element<'static, Message> where F: Fn(f32) -> Message + 'static, { plain_slider(label, value, min..=max, 0.1, "", 1, colors, msg_fn) } fn color_row(label: &str, color: [u8; 4], _c: ThemeColors) -> Element<'static, Message> { let l = label.to_string(); let swatch = container(text("")) .width(14) .height(14) .style(move |_theme| iced::widget::container::Style { background: Some(iced::Background::Color(iced::Color::from_rgba( color[0] as f32 / 255.0, color[1] as f32 / 255.0, color[2] as f32 / 255.0, color[3] as f32 / 255.0, ))), border: iced::Border::default() .rounded(2) .color(iced::Color::from_rgb(0.3, 0.3, 0.3)) .width(1), ..Default::default() }); row![text(l).size(10).width(Length::Fixed(50.0)), swatch,] .spacing(4) .align_y(iced::Alignment::Center) .into() }