From 634592e26d6b7c9a9fd7bb43176268828ec78e47 Mon Sep 17 00:00:00 2001 From: Halit Can Date: Thu, 16 Jul 2026 11:14:01 +0300 Subject: [PATCH] feat(iced): update Properties panel with layer info and vector shape properties --- .../crates/hcie-iced-gui/src/dock/view.rs | 32 +- .../hcie-iced-gui/src/panels/properties.rs | 410 ++++++++++++++++-- 2 files changed, 392 insertions(+), 50 deletions(-) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs index b7e2a4e..17dfd71 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs @@ -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 => { diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs index cea743e..37cd7e8 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs @@ -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,47 +87,91 @@ 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, + 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::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![ - text("Flood Fill").size(11), - text("Click on canvas to fill area").size(10), - ] - .spacing(4) - .into() - } - _ => { - text(format!("{:?}", active_tool)).size(11).into() + 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![ + text("Eyedropper").size(11), + text("Click on canvas to pick color").size(10), + ] + .spacing(4) + .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(), }; - // Panel title is in the dock tab — no duplicate inside the panel. - let panel = column![ - content, - ] - .spacing(0) - .padding(4); + let panel = column![layer_section, horizontal_rule(1), tool_section,] + .spacing(4) + .padding(4); container(panel) .width(Length::Fill) @@ -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, + 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),