feat(iced): update Properties panel with layer info and vector shape properties

This commit is contained in:
2026-07-16 11:14:01 +03:00
parent 418f4adc15
commit 634592e26d
2 changed files with 392 additions and 50 deletions
@@ -73,14 +73,42 @@ pub fn dock_view<'a>(
crate::color_picker::view(&app.fg_color, &app.bg_color, &app.recent_colors, app.color_tab)
}
PaneType::Properties => {
let doc = &app.documents[app.active_doc];
let active_id = doc.engine.active_layer_id();
let layer_info = doc.cached_layers.iter().find(|l| l.id == active_id);
let layer_name = layer_info.map(|l| l.name.as_str()).unwrap_or("");
let layer_opacity = layer_info.map(|l| l.opacity).unwrap_or(1.0);
let layer_visible = layer_info.map(|l| l.visible).unwrap_or(true);
let layer_blend_mode = layer_info.map(|l| l.blend_mode).unwrap_or(hcie_engine_api::BlendMode::Normal);
let layer_type = layer_info.map(|l| l.layer_type).unwrap_or(hcie_engine_api::LayerType::Raster);
let layer_width = layer_info.map(|l| l.width).unwrap_or(0);
let layer_height = layer_info.map(|l| l.height).unwrap_or(0);
let vector_shapes = doc.engine.active_vector_shapes().unwrap_or_default();
let ts2 = app.settings.tool_settings.clone();
crate::panels::properties::view(
&app.tool_state.active_tool,
app.tool_state.brush_size,
app.tool_state.brush_opacity,
app.tool_state.brush_hardness,
app.documents[app.active_doc].selection_rect,
app.documents[app.active_doc].vector_draw,
doc.selection_rect,
doc.vector_draw,
colors,
layer_name,
layer_opacity,
layer_visible,
layer_blend_mode,
layer_type,
layer_width,
layer_height,
doc.selected_vector_shape,
&vector_shapes,
ts2.vector_stroke_width,
ts2.vector_opacity,
ts2.vector_fill,
ts2.vector_radius,
ts2.vector_points,
ts2.vector_sides,
active_id,
)
}
PaneType::Script => {
@@ -1,13 +1,83 @@
//! Properties panel — active object/tool properties.
//! Uses ThemeColors for consistent styling.
//! 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::panels::styles;
use crate::theme::ThemeColors;
use hcie_engine_api::Tool;
use iced::widget::{button, column, container, horizontal_rule, row, slider, text};
use hcie_engine_api::{BlendMode, LayerType, Tool, VectorShape};
use iced::widget::{button, checkbox, column, container, horizontal_rule, row, slider, text};
use iced::{Element, Length};
/// Human-readable label for a shape at a given index.
fn shape_label(shapes: &[VectorShape], i: usize) -> String {
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::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,
@@ -17,46 +87,90 @@ pub fn view<'a>(
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,
active_layer_id: u64,
) -> Element<'a, Message> {
let content: Element<'a, Message> = match active_tool {
// 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 | Tool::Spray => {
brush_properties(brush_size, brush_opacity, brush_hardness, colors)
}
Tool::Select => {
selection_properties(selection_rect, colors)
Tool::Select => selection_properties(selection_rect, colors),
Tool::VectorSelect => {
vector_select_properties(selected_vector_shape, vector_shapes, colors)
}
Tool::VectorRect | Tool::VectorCircle | Tool::VectorLine => {
vector_properties(vector_draw, 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,
)
}
Tool::Text => {
text_properties(colors)
}
Tool::Eyedropper => {
column![
Tool::Text => text_properties(colors),
Tool::Eyedropper => column![
text("Eyedropper").size(11),
text("Click on canvas to pick color").size(10),
]
.spacing(4)
.into()
}
Tool::FloodFill => {
column![
.into(),
Tool::FloodFill => column![
text("Flood Fill").size(11),
text("Click on canvas to fill area").size(10),
]
.spacing(4)
.into()
}
_ => {
text(format!("{:?}", active_tool)).size(11).into()
}
.into(),
_ => text(format!("{:?}", active_tool)).size(11).into(),
};
// Panel title is in the dock tab — no duplicate inside the panel.
let panel = column![
content,
]
.spacing(0)
let panel = column![layer_section, horizontal_rule(1), tool_section,]
.spacing(4)
.padding(4);
container(panel)
@@ -66,6 +180,215 @@ pub fn view<'a>(
.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 = slider(0.0..=1.0, opacity, move |v| Message::LayerSetOpacity(layer_id, v))
.step(0.01)
.width(Length::Fill);
column![
text("Layer Info").size(11),
row![
text("Name").size(10).width(Length::Fixed(50.0)),
text(name).size(10),
]
.spacing(4),
row![
text("Opacity").size(10).width(Length::Fixed(50.0)),
text(format!("{:.0}%", opacity * 100.0)).size(10).width(Length::Fixed(36.0)),
]
.spacing(4),
opacity_slider,
row![
text("Visible").size(10).width(Length::Fixed(50.0)),
checkbox("", visible)
.on_toggle(move |v| Message::LayerToggleVisibility(layer_id))
.size(11),
]
.spacing(4)
.align_y(iced::Alignment::Center),
row![
text("Blend").size(10).width(Length::Fixed(50.0)),
text(blend_mode_label(blend_mode)).size(10),
]
.spacing(4),
row![
text("Type").size(10).width(Length::Fixed(50.0)),
text(layer_type_label(layer_type)).size(10),
]
.spacing(4),
row![
text("Size").size(10).width(Length::Fixed(50.0)),
text(format!("{}×{}", width, height)).size(10),
]
.spacing(4),
]
.spacing(4)
.into()
}
/// VectorSelect shape properties — shown when a shape is selected.
fn vector_select_properties<'a>(
selected: Option<usize>,
shapes: &[VectorShape],
_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 shape_type = match shape {
VectorShape::Line { .. } => "Line",
VectorShape::Rect { .. } => "Rect",
VectorShape::Circle { .. } => "Circle",
VectorShape::Arrow { .. } => "Arrow",
VectorShape::Star { .. } => "Star",
VectorShape::Polygon { .. } => "Polygon",
VectorShape::Rhombus { .. } => "Rhombus",
VectorShape::Cylinder { .. } => "Cylinder",
VectorShape::Heart { .. } => "Heart",
VectorShape::Bubble { .. } => "Bubble",
VectorShape::Gear { .. } => "Gear",
VectorShape::Cross { .. } => "Cross",
VectorShape::Crescent { .. } => "Crescent",
VectorShape::Bolt { .. } => "Bolt",
VectorShape::Arrow4 { .. } => "4-way Arrow",
VectorShape::FreePath { .. } => "Free Path",
};
column![
text("Shape Properties").size(11),
row![text("Shape").size(10), text(label).size(10)].spacing(4),
row![text("Type").size(10), text(shape_type).size(10)].spacing(4),
row![text("Stroke").size(10), text(format!("{:.1}", stroke)).size(10)].spacing(4),
slider(0.0..=50.0, stroke, move |v| Message::VectorStrokeChanged(v))
.step(0.5)
.width(Length::Fill),
row![text("Opacity").size(10), text(format!("{:.0}%", opacity * 100.0)).size(10)].spacing(4),
slider(0.0..=1.0, opacity, move |v| Message::VectorOpacityChanged(v))
.step(0.01)
.width(Length::Fill),
row![text("Hardness").size(10), text(format!("{:.0}%", hardness * 100.0)).size(10)].spacing(4),
slider(0.0..=1.0, hardness, move |v| Message::GeometryHardnessChanged(v))
.step(0.01)
.width(Length::Fill),
row![
text("Fill").size(10),
checkbox("", filled)
.on_toggle(Message::VectorFillToggled)
.size(11),
]
.spacing(4)
.align_y(iced::Alignment::Center),
]
.spacing(4)
.into()
}
None => column![
text("Vector Select").size(11),
text("Click a shape on canvas").size(10),
]
.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,
) -> Element<'a, Message> {
let mut col = column![
text("Vector Shape").size(11),
row![text("Stroke").size(10), text(format!("{:.1}", stroke)).size(10)].spacing(4),
slider(0.0..=50.0, stroke, |v| Message::VectorStrokeChanged(v))
.step(0.5)
.width(Length::Fill),
row![text("Opacity").size(10), text(format!("{:.0}%", opacity * 100.0)).size(10)].spacing(4),
slider(0.0..=1.0, opacity, |v| Message::VectorOpacityChanged(v))
.step(0.01)
.width(Length::Fill),
row![
text("Fill").size(10),
checkbox("", fill)
.on_toggle(Message::VectorFillToggled)
.size(11),
]
.spacing(4)
.align_y(iced::Alignment::Center),
]
.spacing(4);
// Shape-specific controls
match tool {
Tool::VectorRect => {
col = col.push(
row![text("Radius").size(10), text(format!("{:.1}", radius)).size(10)].spacing(4),
);
col = col.push(
slider(0.0..=100.0, radius, |v| Message::VectorRadiusChanged(v))
.step(1.0)
.width(Length::Fill),
);
}
Tool::VectorStar => {
col = col.push(
row![text("Points").size(10), text(format!("{}", points)).size(10)].spacing(4),
);
col = col.push(
slider(3.0..=20.0, points as f32, |v| Message::VectorPointsChanged(v as u32))
.step(1.0)
.width(Length::Fill),
);
}
Tool::VectorPolygon => {
col = col.push(
row![text("Sides").size(10), text(format!("{}", sides)).size(10)].spacing(4),
);
col = col.push(
slider(3.0..=12.0, sides as f32, |v| Message::VectorSidesChanged(v as u32))
.step(1.0)
.width(Length::Fill),
);
}
_ => {}
}
// 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, Spray).
fn brush_properties<'a>(size: f32, opacity: f32, hardness: f32, _colors: ThemeColors) -> Element<'a, Message> {
let size_slider = slider(1.0..=200.0, size, |v| Message::BrushSizeChanged(v))
.step(1.0)
@@ -90,6 +413,7 @@ fn brush_properties<'a>(size: f32, opacity: f32, hardness: f32, _colors: ThemeCo
.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)) => {
@@ -106,17 +430,7 @@ fn selection_properties<'a>(rect: Option<(f32, f32, f32, f32)>, _colors: ThemeCo
}
}
fn vector_properties<'a>(draw: Option<((f32, f32), (f32, f32))>, _colors: ThemeColors) -> Element<'a, Message> {
match draw {
Some(((x0, y0), (x1, y1))) => {
let w = (x1 - x0).abs() as u32;
let h = (y1 - y0).abs() as u32;
column![text("Vector Shape").size(11), row![text("Size").size(10), text(format!("{}×{}", w, h)).size(10)].spacing(4)].spacing(4).into()
}
None => column![text("Vector Shape").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),