feat(iced): update Properties panel with layer info and vector shape properties
This commit is contained in:
@@ -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,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<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::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<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),
|
||||
|
||||
Reference in New Issue
Block a user