Files
hcie-rust-v3.05/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs
T
phantom b49106dc1d BIG REFACTOR GPT SOL
feat: Refactor Iced panel adapter and introduce build metadata management

- Updated Cargo.toml files across multiple crates to use workspace versioning.
- Enhanced the `iced-panel-adapter` to include a new `plain_slider` module and updated widget rendering to support theme colors.
- Added new theme color utilities for recessed and elevated surfaces in `iced-panel-adapter`.
- Introduced a new `hcie-build-info` crate to manage build metadata, including a build ID system.
- Created a build script to synchronize build IDs across the workspace.
- Added a Makefile for simplified build commands for the Iced application.
- Implemented regression tests for vector shape creation history in the engine API.
- Added a new script for managing Cargo commands with synchronized build ID increments.
- Updated line count report to reflect recent changes in codebase.
- Created a visual plan document for future improvements in the Iced history and panel systems.
2026-07-21 04:25:23 +03:00

626 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Properties panel — active object/tool properties and layer info.
//!
//! Displays layer information at the top (name, opacity, visibility, blend mode,
//! type, size), followed by tool-specific settings. When VectorSelect is active
//! and a shape is selected, shows that shape's editable properties.
use crate::app::Message;
use crate::color_picker;
use crate::color_picker::ColorPickerTarget;
use crate::panels::styles;
use crate::panels::typography::{BODY, SECTION};
use crate::theme::ThemeColors;
use crate::widgets::plain_slider;
use hcie_engine_api::{BlendMode, LayerType, Tool, VectorShape};
use iced::widget::{button, checkbox, column, container, horizontal_rule, row, scrollable, text};
use iced::{Element, Length};
/// Human-readable label for a shape at a given index.
fn shape_label(shapes: &[VectorShape], i: usize) -> String {
let name = shapes[i].name();
if !name.is_empty() {
return name;
}
match &shapes[i] {
VectorShape::Line { .. } => format!("Line #{}", i + 1),
VectorShape::Rect { .. } => format!("Rect #{}", i + 1),
VectorShape::Circle { .. } => format!("Circle #{}", i + 1),
VectorShape::Arrow { .. } => format!("Arrow #{}", i + 1),
VectorShape::Star { .. } => format!("Star #{}", i + 1),
VectorShape::Polygon { .. } => format!("Polygon #{}", i + 1),
VectorShape::Rhombus { .. } => format!("Rhombus #{}", i + 1),
VectorShape::Cylinder { .. } => format!("Cylinder #{}", i + 1),
VectorShape::Heart { .. } => format!("Heart #{}", i + 1),
VectorShape::Bubble { .. } => format!("Bubble #{}", i + 1),
VectorShape::Gear { .. } => format!("Gear #{}", i + 1),
VectorShape::Cross { .. } => format!("Cross #{}", i + 1),
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
VectorShape::SvgShape { kind, .. } => format!("{} #{}", kind, i + 1),
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
}
}
/// Human-readable label for a layer type.
fn layer_type_label(lt: LayerType) -> &'static str {
match lt {
LayerType::Raster => "Raster",
LayerType::Vector => "Vector",
LayerType::Text => "Text",
LayerType::Mask => "Mask",
LayerType::Group => "Group",
}
}
/// Human-readable label for a blend mode.
fn blend_mode_label(bm: BlendMode) -> &'static str {
match bm {
BlendMode::Normal => "Normal",
BlendMode::Dissolve => "Dissolve",
BlendMode::Darken => "Darken",
BlendMode::Multiply => "Multiply",
BlendMode::ColorBurn => "Color Burn",
BlendMode::LinearBurn => "Linear Burn",
BlendMode::DarkerColor => "Darker Color",
BlendMode::Lighten => "Lighten",
BlendMode::Screen => "Screen",
BlendMode::ColorDodge => "Color Dodge",
BlendMode::LinearDodge => "Linear Dodge",
BlendMode::LighterColor => "Lighter Color",
BlendMode::Overlay => "Overlay",
BlendMode::SoftLight => "Soft Light",
BlendMode::HardLight => "Hard Light",
BlendMode::VividLight => "Vivid Light",
BlendMode::LinearLight => "Linear Light",
BlendMode::PinLight => "Pin Light",
BlendMode::HardMix => "Hard Mix",
BlendMode::Difference => "Difference",
BlendMode::Exclusion => "Exclusion",
BlendMode::Subtract => "Subtract",
BlendMode::Divide => "Divide",
BlendMode::Hue => "Hue",
BlendMode::Saturation => "Saturation",
BlendMode::Color => "Color",
BlendMode::Luminosity => "Luminosity",
BlendMode::PassThrough => "Pass Through",
}
}
/// Build the properties panel.
pub fn view<'a>(
active_tool: &Tool,
brush_size: f32,
brush_opacity: f32,
brush_hardness: f32,
selection_rect: Option<(f32, f32, f32, f32)>,
vector_draw: Option<((f32, f32), (f32, f32))>,
colors: ThemeColors,
layer_name: &'a str,
layer_opacity: f32,
layer_visible: bool,
layer_blend_mode: BlendMode,
layer_type: LayerType,
layer_width: u32,
layer_height: u32,
selected_vector_shape: Option<usize>,
vector_shapes: &[VectorShape],
vector_stroke: f32,
vector_opacity: f32,
vector_fill: bool,
vector_radius: f32,
vector_points: u32,
vector_sides: u32,
vector_angle: f32,
active_layer_id: u64,
foreground_color: [u8; 4],
background_color: [u8; 4],
settings_spray_particle_size: f32,
settings_spray_density: u32,
) -> Element<'a, Message> {
// Build layer info section at the top
let layer_section = layer_info_section(
layer_name,
layer_opacity,
layer_visible,
layer_blend_mode,
layer_type,
layer_width,
layer_height,
active_layer_id,
colors,
);
// Build tool/shape-specific section
let tool_section: Element<'a, Message> = match active_tool {
Tool::Pen | Tool::Brush | Tool::Eraser => {
brush_properties(brush_size, brush_opacity, brush_hardness, colors)
}
Tool::Spray => spray_properties(
brush_size,
brush_opacity,
brush_hardness,
settings_spray_particle_size,
settings_spray_density,
colors,
),
Tool::Select => selection_properties(selection_rect, colors),
Tool::VectorSelect => vector_select_properties(
selected_vector_shape,
vector_shapes,
vector_angle,
active_layer_id,
colors,
),
Tool::VectorRect
| Tool::VectorCircle
| Tool::VectorLine
| 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_tool_properties(
active_tool,
vector_stroke,
vector_opacity,
vector_fill,
vector_radius,
vector_points,
vector_sides,
vector_draw,
colors,
foreground_color,
background_color,
),
Tool::Text => text_properties(colors),
Tool::Eyedropper => column![
text("Eyedropper").size(SECTION),
text("Click on canvas to pick color").size(BODY),
]
.spacing(4)
.into(),
Tool::FloodFill => column![
text("Flood Fill").size(SECTION),
text("Click on canvas to fill area").size(BODY),
]
.spacing(4)
.into(),
_ => text(format!("{:?}", active_tool)).size(SECTION).into(),
};
let _ = foreground_color;
let _ = background_color;
let panel = column![layer_section, horizontal_rule(1), tool_section,]
.spacing(4)
.padding(4);
container(scrollable(panel))
.width(Length::Fill)
.height(Length::Fill)
.style(move |_theme| styles::panel_background(colors))
.into()
}
/// Layer information section shown at the top of the Properties panel.
fn layer_info_section<'a>(
name: &'a str,
opacity: f32,
visible: bool,
blend_mode: BlendMode,
layer_type: LayerType,
width: u32,
height: u32,
layer_id: u64,
colors: ThemeColors,
) -> Element<'a, Message> {
let opacity_slider = plain_slider(
"Opacity",
opacity,
0.0..=1.0,
0.01,
"%",
0,
colors,
move |v| Message::LayerSetOpacity(layer_id, v),
);
column![
text("Layer Info").size(SECTION),
row![
text("Name").size(BODY).width(Length::Fixed(58.0)),
text(name).size(BODY),
]
.spacing(4),
opacity_slider,
row![
text("Visible").size(BODY).width(Length::Fixed(58.0)),
checkbox("", visible)
.on_toggle(move |_v| Message::LayerToggleVisibility(layer_id))
.size(11),
]
.spacing(4)
.align_y(iced::Alignment::Center),
row![
text("Blend").size(BODY).width(Length::Fixed(58.0)),
text(blend_mode_label(blend_mode)).size(BODY),
]
.spacing(4),
row![
text("Type").size(BODY).width(Length::Fixed(58.0)),
text(layer_type_label(layer_type)).size(BODY),
]
.spacing(4),
row![
text("Size").size(BODY).width(Length::Fixed(58.0)),
text(format!("{}×{}", width, height)).size(BODY),
]
.spacing(4),
]
.spacing(4)
.into()
}
/// VectorSelect shape properties — shown when a shape is selected.
fn vector_select_properties<'a>(
selected: Option<usize>,
shapes: &[VectorShape],
vector_angle: f32,
active_layer_id: u64,
colors: ThemeColors,
) -> Element<'a, Message> {
match selected.and_then(|idx| shapes.get(idx).map(|s| (idx, s))) {
Some((idx, shape)) => {
let label = shape_label(shapes, idx);
let stroke = shape.stroke();
let opacity = shape.shape_opacity();
let hardness = shape.hardness();
let filled = shape.is_filled();
let stroke_color = shape.color();
let fill_color = shape.fill_color().unwrap_or([0, 0, 0, 255]);
let shape_type: String = match shape {
VectorShape::Line { .. } => "Line".into(),
VectorShape::Rect { .. } => "Rect".into(),
VectorShape::Circle { .. } => "Circle".into(),
VectorShape::Arrow { .. } => "Arrow".into(),
VectorShape::Star { .. } => "Star".into(),
VectorShape::Polygon { .. } => "Polygon".into(),
VectorShape::Rhombus { .. } => "Rhombus".into(),
VectorShape::Cylinder { .. } => "Cylinder".into(),
VectorShape::Heart { .. } => "Heart".into(),
VectorShape::Bubble { .. } => "Bubble".into(),
VectorShape::Gear { .. } => "Gear".into(),
VectorShape::Cross { .. } => "Cross".into(),
VectorShape::Crescent { .. } => "Crescent".into(),
VectorShape::Bolt { .. } => "Bolt".into(),
VectorShape::Arrow4 { .. } => "4-way Arrow".into(),
VectorShape::SvgShape { kind, .. } => kind.clone(),
VectorShape::FreePath { .. } => "Free Path".into(),
};
// "Edit SVG" button — only for SvgShape variants (EGUI parity)
let edit_svg_btn: Element<'a, Message> =
if matches!(shape, VectorShape::SvgShape { .. }) {
let c = colors;
button(text("Edit SVG").size(BODY))
.on_press(Message::SvgEditorOpen {
layer_id: active_layer_id,
shape_idx: idx,
})
.padding([4, 8])
.style(move |_theme, _status| iced::widget::button::Style {
background: Some(iced::Background::Color(c.accent)),
text_color: iced::Color::WHITE,
border: iced::Border::default().rounded(3),
..Default::default()
})
.into()
} else {
text("").into()
};
column![
text("Shape Properties").size(SECTION),
row![text("Shape").size(BODY), text(label).size(BODY)].spacing(4),
row![text("Type").size(BODY), text(shape_type).size(BODY)].spacing(4),
edit_svg_btn,
plain_slider(
"Stroke",
stroke,
0.0..=50.0,
0.5,
"px",
1,
colors,
move |v| { Message::VectorStrokeChanged(v) },
),
plain_slider(
"Opacity",
opacity,
0.0..=1.0,
0.01,
"%",
0,
colors,
move |v| { Message::VectorOpacityChanged(v) },
),
plain_slider(
"Hardness",
hardness,
0.0..=1.0,
0.01,
"%",
0,
colors,
move |v| { Message::GeometryHardnessChanged(v) },
),
plain_slider(
"Angle",
vector_angle,
-180.0..=180.0,
1.0,
"°",
0,
colors,
move |v| Message::VectorAngleChanged(v),
),
row![
text("Fill").size(BODY),
checkbox("", filled)
.on_toggle(Message::VectorFillToggled)
.size(11),
]
.spacing(4)
.align_y(iced::Alignment::Center),
// Fill color picker (only if fill is enabled)
if filled {
color_picker::color_swatch(
fill_color,
"Fill Color",
ColorPickerTarget::GeometryFill,
)
} else {
text("").into()
},
// Stroke color picker
color_picker::color_swatch(
stroke_color,
"Stroke Color",
ColorPickerTarget::GeometryStroke,
),
]
.spacing(4)
.into()
}
None => column![
text("Vector Select").size(SECTION),
text("Click a shape on canvas").size(BODY),
]
.spacing(4)
.into(),
}
}
/// Vector shape tool settings — shown when drawing a new vector shape.
fn vector_tool_properties<'a>(
tool: &Tool,
stroke: f32,
opacity: f32,
fill: bool,
radius: f32,
points: u32,
sides: u32,
vector_draw: Option<((f32, f32), (f32, f32))>,
colors: ThemeColors,
foreground_color: [u8; 4],
background_color: [u8; 4],
) -> Element<'a, Message> {
let mut col = column![
text("Vector Shape").size(11),
plain_slider("Stroke", stroke, 0.0..=50.0, 0.5, "px", 1, colors, |v| {
Message::VectorStrokeChanged(v)
},),
plain_slider("Opacity", opacity, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
Message::VectorOpacityChanged(v)
},),
row![
text("Fill").size(10),
checkbox("", fill)
.on_toggle(Message::VectorFillToggled)
.size(11),
]
.spacing(4)
.align_y(iced::Alignment::Center),
// Fill color picker (only if fill is enabled)
if fill {
color_picker::color_swatch(
background_color,
"Fill Color",
ColorPickerTarget::Background,
)
} else {
text("").into()
},
// Stroke color picker
color_picker::color_swatch(
foreground_color,
"Stroke Color",
ColorPickerTarget::Foreground,
),
]
.spacing(4);
// Shape-specific controls
match tool {
Tool::VectorRect => {
col = col.push(plain_slider(
"Radius",
radius,
0.0..=100.0,
1.0,
"px",
1,
colors,
|v| Message::VectorRadiusChanged(v),
));
}
Tool::VectorStar => {
col = col.push(plain_slider(
"Points",
points as f32,
3.0..=20.0,
1.0,
"",
0,
colors,
|v| Message::VectorPointsChanged(v as u32),
));
}
Tool::VectorPolygon => {
col = col.push(plain_slider(
"Sides",
sides as f32,
3.0..=12.0,
1.0,
"",
0,
colors,
|v| Message::VectorSidesChanged(v as u32),
));
}
_ => {}
}
// Show size if currently drawing
if let Some(((x0, y0), (x1, y1))) = vector_draw {
let w = (x1 - x0).abs() as u32;
let h = (y1 - y0).abs() as u32;
col = col.push(horizontal_rule(1));
col =
col.push(row![text("Size").size(10), text(format!("{}×{}", w, h)).size(10)].spacing(4));
}
col.into()
}
/// Brush tool properties (Pen, Brush, Eraser).
fn brush_properties<'a>(
size: f32,
opacity: f32,
hardness: f32,
colors: ThemeColors,
) -> Element<'a, Message> {
column![
text("Brush Properties").size(11),
plain_slider("Size", size, 1.0..=200.0, 1.0, "px", 0, colors, |v| {
Message::BrushSizeChanged(v)
},),
plain_slider("Opacity", opacity, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
Message::BrushOpacityChanged(v)
},),
plain_slider("Hardness", hardness, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
Message::BrushHardnessChanged(v)
},),
]
.spacing(4)
.into()
}
/// Spray tool properties with particle size and density controls.
fn spray_properties<'a>(
size: f32,
opacity: f32,
hardness: f32,
particle_size: f32,
density: u32,
colors: ThemeColors,
) -> Element<'a, Message> {
column![
text("Spray Properties").size(11),
plain_slider("Size", size, 1.0..=200.0, 1.0, "px", 0, colors, |v| {
Message::BrushSizeChanged(v)
},),
plain_slider(
"Particle Size",
particle_size,
1.0..=20.0,
0.1,
"px",
1,
colors,
Message::SprayParticleSizeChanged,
),
plain_slider(
"Density",
density as f32,
10.0..=1000.0,
1.0,
"",
0,
colors,
|v| { Message::SprayDensityChanged(v as u32) },
),
plain_slider("Opacity", opacity, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
Message::BrushOpacityChanged(v)
},),
plain_slider("Hardness", hardness, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
Message::BrushHardnessChanged(v)
},),
]
.spacing(4)
.into()
}
/// Selection tool properties.
fn selection_properties<'a>(
rect: Option<(f32, f32, f32, f32)>,
_colors: ThemeColors,
) -> Element<'a, Message> {
match rect {
Some((x0, y0, x1, y1)) => {
let w = (x1 - x0).abs() as u32;
let h = (y1 - y0).abs() as u32;
column![
text("Selection").size(11),
row![text("Size").size(10), text(format!("{}×{}", w, h)).size(10)].spacing(4),
]
.spacing(4)
.into()
}
None => column![text("Selection").size(11), text("Drag on canvas").size(10)]
.spacing(4)
.into(),
}
}
/// Text tool properties.
fn text_properties<'a>(_colors: ThemeColors) -> Element<'a, Message> {
column![
text("Text Tool").size(11),
text("Click on canvas to add text").size(10),
horizontal_rule(1),
row![
button(text("Left").size(10))
.on_press(Message::TextAlignChanged("Left".to_string()))
.padding([4, 8]),
button(text("Center").size(10))
.on_press(Message::TextAlignChanged("Center".to_string()))
.padding([4, 8]),
button(text("Right").size(10))
.on_press(Message::TextAlignChanged("Right".to_string()))
.padding([4, 8]),
]
.spacing(4),
]
.spacing(4)
.into()
}