535 lines
16 KiB
Rust
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()
|
|
}
|