Files
hcie-rust-v3.05/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_settings.rs
T
Your Name 19a7e51813
mandatory-regression-gate / deterministic-tests (push) Waiting to run
mandatory-regression-gate / protected-performance-path (push) Waiting to run
feat: add cyclic color and speed options for brush settings
2026-07-25 17:34:41 +03:00

535 lines
16 KiB
Rust

//! 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<F>(
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()
}