feat(iced): implement full geometry panel with all vector shape controls
- Shape list with selection and delete button (B1) - Boolean operations: Union/Intersect/Subtract/Xor (B7) - Bounds editing with X1/Y1/X2/Y2 sliders and size display (B2) - Fill toggle + fill color with RGB sliders (B3) - Stroke color with RGB sliders (B4) - Opacity and Hardness sliders (B5) - Line Cap controls for Line shapes (B6)
This commit is contained in:
@@ -260,6 +260,10 @@ pub struct HcieIcedApp {
|
|||||||
pub vector_shapes_snapshot: Option<Vec<hcie_engine_api::VectorShape>>,
|
pub vector_shapes_snapshot: Option<Vec<hcie_engine_api::VectorShape>>,
|
||||||
/// Last drag position during vector shape editing (canvas coordinates).
|
/// Last drag position during vector shape editing (canvas coordinates).
|
||||||
pub vector_drag_last: Option<(f32, f32)>,
|
pub vector_drag_last: Option<(f32, f32)>,
|
||||||
|
/// Boolean operation shape A selector index (geometry panel).
|
||||||
|
pub bool_shape_a: Option<usize>,
|
||||||
|
/// Boolean operation shape B selector index (geometry panel).
|
||||||
|
pub bool_shape_b: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A recently opened file entry.
|
/// A recently opened file entry.
|
||||||
@@ -633,6 +637,22 @@ pub enum Message {
|
|||||||
VectorShapeFlipH,
|
VectorShapeFlipH,
|
||||||
VectorShapeFlipV,
|
VectorShapeFlipV,
|
||||||
|
|
||||||
|
// ── Geometry panel controls ──────────────────────────
|
||||||
|
GeometryBoundsX1(f32),
|
||||||
|
GeometryBoundsY1(f32),
|
||||||
|
GeometryBoundsX2(f32),
|
||||||
|
GeometryBoundsY2(f32),
|
||||||
|
GeometryFillToggled(bool),
|
||||||
|
GeometryFillColorChanged([u8; 4]),
|
||||||
|
GeometryStrokeColorChanged([u8; 4]),
|
||||||
|
GeometryOpacityChanged(f32),
|
||||||
|
GeometryHardnessChanged(f32),
|
||||||
|
GeometryBoolShapeA(Option<usize>),
|
||||||
|
GeometryBoolShapeB(Option<usize>),
|
||||||
|
GeometryBooleanOp(hcie_engine_api::VectorBooleanOp),
|
||||||
|
GeometryLineCapStart(hcie_engine_api::LineCap),
|
||||||
|
GeometryLineCapEnd(hcie_engine_api::LineCap),
|
||||||
|
|
||||||
// ── Lasso / polygon selection ───────────────────────
|
// ── Lasso / polygon selection ───────────────────────
|
||||||
LassoDragMove(f32, f32),
|
LassoDragMove(f32, f32),
|
||||||
LassoDragEnd,
|
LassoDragEnd,
|
||||||
@@ -1086,6 +1106,8 @@ impl HcieIcedApp {
|
|||||||
colors_dirty: false,
|
colors_dirty: false,
|
||||||
vector_shapes_snapshot: None,
|
vector_shapes_snapshot: None,
|
||||||
vector_drag_last: None,
|
vector_drag_last: None,
|
||||||
|
bool_shape_a: None,
|
||||||
|
bool_shape_b: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply saved settings to tool state
|
// Apply saved settings to tool state
|
||||||
@@ -3616,6 +3638,140 @@ impl HcieIcedApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Geometry panel controls ───────────────────
|
||||||
|
Message::GeometryBoundsX1(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
let shapes = self.documents[self.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
|
if let Some(s) = shapes.get(idx) {
|
||||||
|
let (_, y1, x2, y2) = s.bounds();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_bounds(layer_id, idx, v, y1, x2, y2);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryBoundsY1(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
let shapes = self.documents[self.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
|
if let Some(s) = shapes.get(idx) {
|
||||||
|
let (x1, _, x2, y2) = s.bounds();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_bounds(layer_id, idx, x1, v, x2, y2);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryBoundsX2(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
let shapes = self.documents[self.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
|
if let Some(s) = shapes.get(idx) {
|
||||||
|
let (x1, y1, _, y2) = s.bounds();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_bounds(layer_id, idx, x1, y1, v, y2);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryBoundsY2(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
let shapes = self.documents[self.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
|
if let Some(s) = shapes.get(idx) {
|
||||||
|
let (x1, y1, x2, _) = s.bounds();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_bounds(layer_id, idx, x1, y1, x2, v);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryFillToggled(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_fill(layer_id, idx, v);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryFillColorChanged(c) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_fill_color(layer_id, idx, c);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryStrokeColorChanged(c) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_color(layer_id, idx, c);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryOpacityChanged(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_opacity(layer_id, idx, v);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryHardnessChanged(v) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_hardness(layer_id, idx, v);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryBoolShapeA(idx) => {
|
||||||
|
self.bool_shape_a = idx;
|
||||||
|
}
|
||||||
|
Message::GeometryBoolShapeB(idx) => {
|
||||||
|
self.bool_shape_b = idx;
|
||||||
|
}
|
||||||
|
Message::GeometryBooleanOp(op) => {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
if let (Some(a), Some(b)) = (self.bool_shape_a, self.bool_shape_b) {
|
||||||
|
self.documents[self.active_doc].engine.boolean_vector_shapes(layer_id, op, a, b);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryLineCapStart(cap) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
let shapes = self.documents[self.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
|
if let Some(s) = shapes.get(idx) {
|
||||||
|
let ce = match s {
|
||||||
|
hcie_engine_api::VectorShape::Line { cap_end, .. } => *cap_end,
|
||||||
|
_ => hcie_engine_api::LineCap::Round,
|
||||||
|
};
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_line_caps(layer_id, idx, cap, ce);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GeometryLineCapEnd(cap) => {
|
||||||
|
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||||
|
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||||
|
let shapes = self.documents[self.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
|
if let Some(s) = shapes.get(idx) {
|
||||||
|
let cs = match s {
|
||||||
|
hcie_engine_api::VectorShape::Line { cap_start, .. } => *cap_start,
|
||||||
|
_ => hcie_engine_api::LineCap::Round,
|
||||||
|
};
|
||||||
|
self.documents[self.active_doc].engine.set_vector_shape_line_caps(layer_id, idx, cs, cap);
|
||||||
|
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||||
|
self.refresh_composite_if_needed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Lasso / polygon selection ─────────────────
|
// ── Lasso / polygon selection ─────────────────
|
||||||
Message::LassoDragMove(x, y) => {
|
Message::LassoDragMove(x, y) => {
|
||||||
let cx = x.round().max(0.0) as u32;
|
let cx = x.round().max(0.0) as u32;
|
||||||
|
|||||||
@@ -104,7 +104,13 @@ pub fn dock_view<'a>(
|
|||||||
}
|
}
|
||||||
PaneType::Geometry => {
|
PaneType::Geometry => {
|
||||||
let shapes = app.documents[app.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
let shapes = app.documents[app.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||||
crate::panels::geometry::view(shapes, colors, app.documents[app.active_doc].selected_vector_shape)
|
crate::panels::geometry::view(
|
||||||
|
shapes,
|
||||||
|
colors,
|
||||||
|
app.documents[app.active_doc].selected_vector_shape,
|
||||||
|
app.bool_shape_a,
|
||||||
|
app.bool_shape_b,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
PaneType::ToolSettings => {
|
PaneType::ToolSettings => {
|
||||||
let fg = app.fg_color;
|
let fg = app.fg_color;
|
||||||
|
|||||||
@@ -1,85 +1,496 @@
|
|||||||
//! Vector geometry panel — shape properties for vector layers.
|
//! Vector geometry panel — full shape properties editor.
|
||||||
//! Uses ThemeColors for consistent styling.
|
//!
|
||||||
|
//! Renders the shape list with selection, boolean operations, bounds editing,
|
||||||
|
//! fill/stroke controls, opacity/hardness sliders, and line cap controls
|
||||||
|
//! for the active vector layer's shapes.
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::panels::styles;
|
use crate::panels::styles;
|
||||||
use crate::theme::ThemeColors;
|
use crate::theme::ThemeColors;
|
||||||
use hcie_engine_api::VectorShape;
|
use hcie_engine_api::{LineCap, VectorBooleanOp, VectorShape};
|
||||||
use iced::widget::{column, horizontal_rule, scrollable, text};
|
use iced::widget::{
|
||||||
|
button, checkbox, column, container, row, scrollable, slider, text, Space,
|
||||||
|
};
|
||||||
use iced::{Element, Length};
|
use iced::{Element, Length};
|
||||||
|
|
||||||
/// Build the geometry panel for vector shapes.
|
/// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a section header bar with the given label.
|
||||||
|
fn section_header(label: String, colors: ThemeColors) -> Element<'static, Message> {
|
||||||
|
container(
|
||||||
|
row![text(label).size(11)]
|
||||||
|
.spacing(4),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([3, 6])
|
||||||
|
.style(move |_theme| iced::widget::container::Style {
|
||||||
|
background: Some(iced::Background::Color(colors.bg_hover)),
|
||||||
|
border: iced::Border::default().color(colors.border_high).width(1),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a labeled row with a slider.
|
||||||
|
fn labeled_slider<'a>(
|
||||||
|
label: &'a str,
|
||||||
|
value: f32,
|
||||||
|
range: std::ops::RangeInclusive<f32>,
|
||||||
|
on_change: fn(f32) -> Message,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
row![
|
||||||
|
text(label).size(10).width(30),
|
||||||
|
slider(range, value, on_change).step(0.5).width(Length::Fill),
|
||||||
|
text(format!("{:.0}", value)).size(9).width(32),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the geometry panel.
|
||||||
|
///
|
||||||
/// Panel title is in the dock tab — no duplicate inside the panel.
|
/// Panel title is in the dock tab — no duplicate inside the panel.
|
||||||
pub fn view(shapes: Vec<VectorShape>, colors: ThemeColors, selected_shape: Option<usize>) -> Element<'static, Message> {
|
pub fn view(
|
||||||
let panel = if shapes.is_empty() {
|
shapes: Vec<VectorShape>,
|
||||||
column![
|
colors: ThemeColors,
|
||||||
text("No vector shapes").size(11),
|
selected_shape: Option<usize>,
|
||||||
text("Use vector tools to create shapes").size(10),
|
bool_shape_a: Option<usize>,
|
||||||
]
|
bool_shape_b: Option<usize>,
|
||||||
} else {
|
) -> Element<'static, Message> {
|
||||||
let mut shape_list = column![].spacing(2);
|
let has_shapes = !shapes.is_empty();
|
||||||
let shape_count = shapes.len();
|
let shape_count = shapes.len();
|
||||||
|
|
||||||
for (i, shape) in shapes.into_iter().enumerate() {
|
let mut content = column![].spacing(0).padding(4);
|
||||||
let (shape_name, shape_info) = match &shape {
|
|
||||||
VectorShape::Line { x1, y1, x2, y2, stroke, .. } => ("Line", format!("({:.0},{:.0})→({:.0},{:.0}) w:{:.1}", x1, y1, x2, y2, stroke)),
|
|
||||||
VectorShape::Rect { x1, y1, x2, y2, fill, stroke, .. } => ("Rect", format!("{}×{} fill:{} stroke:{:.1}", (x2-x1).abs() as u32, (y2-y1).abs() as u32, fill, stroke)),
|
|
||||||
VectorShape::Circle { x1, y1, x2, y2, fill, stroke, .. } => ("Circle", format!("r:{:.0} fill:{} stroke:{:.1}", ((x2-x1).abs() + (y2-y1).abs()) / 4.0, fill, stroke)),
|
|
||||||
_ => ("Shape", "Custom".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// ── SHAPES section ──────────────────────────────────────
|
||||||
|
content = content.push(section_header("SHAPES".to_string(), colors));
|
||||||
|
|
||||||
|
if has_shapes {
|
||||||
|
// Delete button in header row (right-aligned)
|
||||||
|
if selected_shape.is_some() {
|
||||||
|
content = content.push(
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
button(text("X").size(10))
|
||||||
|
.on_press(Message::VectorShapeDelete)
|
||||||
|
.padding([2, 6]),
|
||||||
|
]
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
|
.padding([2, 0]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape list
|
||||||
|
let mut shape_list = column![].spacing(1);
|
||||||
|
for i in 0..shape_count {
|
||||||
|
let label = shape_label(&shapes, i);
|
||||||
let is_selected = selected_shape == Some(i);
|
let is_selected = selected_shape == Some(i);
|
||||||
let c = colors;
|
let c = colors;
|
||||||
|
|
||||||
let item = iced::widget::button(
|
let item = button(text(label).size(11))
|
||||||
column![text(format!("{}. {}", i + 1, shape_name)).size(11), text(shape_info).size(9)].spacing(2)
|
.on_press(Message::VectorShapeSelect(if is_selected { None } else { Some(i) }))
|
||||||
)
|
.width(Length::Fill)
|
||||||
.on_press(Message::VectorShapeSelect(Some(i)))
|
.padding([4, 6])
|
||||||
.width(Length::Fill)
|
.style(move |_theme: &iced::Theme, status: iced::widget::button::Status| {
|
||||||
.padding([4, 6])
|
match status {
|
||||||
.style(move |_theme: &iced::Theme, status: iced::widget::button::Status| {
|
iced::widget::button::Status::Active => {
|
||||||
match status {
|
if is_selected {
|
||||||
iced::widget::button::Status::Active => {
|
iced::widget::button::Style {
|
||||||
if is_selected {
|
background: Some(iced::Background::Color(c.accent)),
|
||||||
|
text_color: iced::Color::WHITE,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iced::widget::button::Style {
|
||||||
|
background: Some(iced::Background::Color(c.bg_hover)),
|
||||||
|
text_color: c.text_primary,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iced::widget::button::Status::Hovered => {
|
||||||
iced::widget::button::Style {
|
iced::widget::button::Style {
|
||||||
background: Some(iced::Background::Color(c.accent)),
|
background: Some(iced::Background::Color(if is_selected { c.accent } else { c.bg_active })),
|
||||||
text_color: iced::Color::WHITE,
|
text_color: iced::Color::WHITE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
iced::widget::button::Style {
|
|
||||||
background: Some(iced::Background::Color(c.bg_hover)),
|
|
||||||
text_color: c.text_primary,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
_ => iced::widget::button::Style {
|
||||||
iced::widget::button::Status::Hovered => {
|
background: Some(iced::Background::Color(c.bg_hover)),
|
||||||
iced::widget::button::Style {
|
text_color: c.text_primary,
|
||||||
background: Some(iced::Background::Color(if is_selected { c.accent } else { c.bg_active })),
|
|
||||||
text_color: iced::Color::WHITE,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
_ => iced::widget::button::Style {
|
});
|
||||||
background: Some(iced::Background::Color(c.bg_hover)),
|
|
||||||
text_color: c.text_primary,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
shape_list = shape_list.push(item);
|
shape_list = shape_list.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
column![
|
content = content.push(
|
||||||
text(format!("{} shapes", shape_count)).size(10),
|
scrollable(shape_list).height(Length::Fill).width(Length::Fill),
|
||||||
horizontal_rule(1),
|
);
|
||||||
scrollable(shape_list).height(Length::Fill),
|
} else {
|
||||||
]
|
content = content.push(
|
||||||
};
|
column![
|
||||||
|
text("No vector shapes").size(11),
|
||||||
|
text("Use vector tools to create shapes").size(10),
|
||||||
|
]
|
||||||
|
.spacing(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
iced::widget::container(panel.spacing(0).padding(4))
|
// ── PATH BOOLEAN section (when 2+ shapes) ───────────────
|
||||||
|
if shape_count >= 2 {
|
||||||
|
content = content.push(Space::with_height(8));
|
||||||
|
content = content.push(section_header("PATH BOOLEAN".to_string(), colors));
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
|
||||||
|
// Shape A selector
|
||||||
|
let a_label = match bool_shape_a {
|
||||||
|
Some(i) if i < shape_count => shape_label(&shapes, i),
|
||||||
|
_ => "Shape A".to_string(),
|
||||||
|
};
|
||||||
|
let mut a_menu = column![].spacing(1);
|
||||||
|
for i in 0..shape_count {
|
||||||
|
let lbl = shape_label(&shapes, i);
|
||||||
|
let idx = i;
|
||||||
|
a_menu = a_menu.push(
|
||||||
|
button(text(lbl).size(10))
|
||||||
|
.on_press(Message::GeometryBoolShapeA(Some(idx)))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([3, 6]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape B selector
|
||||||
|
let b_label = match bool_shape_b {
|
||||||
|
Some(i) if i < shape_count => shape_label(&shapes, i),
|
||||||
|
_ => "Shape B".to_string(),
|
||||||
|
};
|
||||||
|
let mut b_menu = column![].spacing(1);
|
||||||
|
for i in 0..shape_count {
|
||||||
|
let lbl = shape_label(&shapes, i);
|
||||||
|
let idx = i;
|
||||||
|
b_menu = b_menu.push(
|
||||||
|
button(text(lbl).size(10))
|
||||||
|
.on_press(Message::GeometryBoolShapeB(Some(idx)))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([3, 6]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show selectors inline
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
container(
|
||||||
|
column![
|
||||||
|
button(text(a_label).size(10))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([4, 6]),
|
||||||
|
a_menu,
|
||||||
|
]
|
||||||
|
.spacing(2)
|
||||||
|
).width(Length::Fill).style(move |_theme| iced::widget::container::Style {
|
||||||
|
background: Some(iced::Background::Color(colors.bg_active)),
|
||||||
|
border: iced::Border::default().color(colors.border_high).width(1).rounded(2),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
text(" + ").size(10),
|
||||||
|
container(
|
||||||
|
column![
|
||||||
|
button(text(b_label).size(10))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([4, 6]),
|
||||||
|
b_menu,
|
||||||
|
]
|
||||||
|
.spacing(2)
|
||||||
|
).width(Length::Fill).style(move |_theme| iced::widget::container::Style {
|
||||||
|
background: Some(iced::Background::Color(colors.bg_active)),
|
||||||
|
border: iced::Border::default().color(colors.border_high).width(1).rounded(2),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.width(Length::Fill),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Boolean operation buttons
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
button(text("Union").size(10)).on_press(Message::GeometryBooleanOp(VectorBooleanOp::Union)).padding([3, 8]),
|
||||||
|
button(text("Intersect").size(10)).on_press(Message::GeometryBooleanOp(VectorBooleanOp::Intersect)).padding([3, 8]),
|
||||||
|
button(text("Subtract").size(10)).on_press(Message::GeometryBooleanOp(VectorBooleanOp::Subtract)).padding([3, 8]),
|
||||||
|
button(text("Xor").size(10)).on_press(Message::GeometryBooleanOp(VectorBooleanOp::Xor)).padding([3, 8]),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.width(Length::Fill),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GEOMETRY section (selected shape only) ──────────────
|
||||||
|
if let Some(idx) = selected_shape {
|
||||||
|
if idx < shape_count {
|
||||||
|
content = content.push(Space::with_height(8));
|
||||||
|
content = content.push(section_header(format!("GEOMETRY [#{}]", idx + 1), colors));
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
|
||||||
|
let (x1, y1, x2, y2) = shapes[idx].bounds();
|
||||||
|
let w = (x2 - x1).abs();
|
||||||
|
let h = (y2 - y1).abs();
|
||||||
|
|
||||||
|
// Bounds sliders
|
||||||
|
content = content.push(labeled_slider("X1", x1, -2000.0..=5000.0, Message::GeometryBoundsX1));
|
||||||
|
content = content.push(labeled_slider("Y1", y1, -2000.0..=5000.0, Message::GeometryBoundsY1));
|
||||||
|
content = content.push(labeled_slider("X2", x2, -2000.0..=5000.0, Message::GeometryBoundsX2));
|
||||||
|
content = content.push(labeled_slider("Y2", y2, -2000.0..=5000.0, Message::GeometryBoundsY2));
|
||||||
|
|
||||||
|
// Size display
|
||||||
|
content = content.push(
|
||||||
|
text(format!("Size: {:.0} x {:.0}", w, h))
|
||||||
|
.size(10)
|
||||||
|
.style(move |_theme| iced::widget::text::Style {
|
||||||
|
color: Some(colors.text_secondary),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fill controls (not for Line shapes)
|
||||||
|
let is_line = matches!(&shapes[idx], VectorShape::Line { .. });
|
||||||
|
if !is_line {
|
||||||
|
if let Some(fc) = shapes[idx].fill_color() {
|
||||||
|
let cur_fill = shapes[idx].fill().unwrap_or(false);
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
checkbox("", cur_fill).on_toggle(Message::GeometryFillToggled),
|
||||||
|
text("Fill").size(10),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
|
if cur_fill {
|
||||||
|
// Fill color: inline RGB sliders
|
||||||
|
let r = fc[0];
|
||||||
|
let g = fc[1];
|
||||||
|
let b_val = fc[2];
|
||||||
|
let a = fc[3];
|
||||||
|
|
||||||
|
content = content.push(Space::with_height(2));
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("R").size(9).width(14),
|
||||||
|
slider(0.0..=255.0, r as f32, move |v| Message::GeometryFillColorChanged([v as u8, g, b_val, a])).step(1.0).width(Length::Fill),
|
||||||
|
text(format!("{}", r)).size(9).width(24),
|
||||||
|
]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("G").size(9).width(14),
|
||||||
|
slider(0.0..=255.0, g as f32, move |v| Message::GeometryFillColorChanged([r, v as u8, b_val, a])).step(1.0).width(Length::Fill),
|
||||||
|
text(format!("{}", g)).size(9).width(24),
|
||||||
|
]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("B").size(9).width(14),
|
||||||
|
slider(0.0..=255.0, b_val as f32, move |v| Message::GeometryFillColorChanged([r, g, v as u8, a])).step(1.0).width(Length::Fill),
|
||||||
|
text(format!("{}", b_val)).size(9).width(24),
|
||||||
|
]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Color preview swatch
|
||||||
|
content = content.push(
|
||||||
|
container(Space::with_width(Length::Fill))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(14)
|
||||||
|
.style(move |_theme| iced::widget::container::Style {
|
||||||
|
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b_val as f32 / 255.0,
|
||||||
|
a as f32 / 255.0,
|
||||||
|
))),
|
||||||
|
border: iced::Border::default().rounded(2).color(colors.border_high).width(1),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke color
|
||||||
|
let stroke_color = shapes[idx].color();
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
content = content.push(
|
||||||
|
row![text("Stroke").size(10)].spacing(4),
|
||||||
|
);
|
||||||
|
|
||||||
|
let sr = stroke_color[0];
|
||||||
|
let sg = stroke_color[1];
|
||||||
|
let sb = stroke_color[2];
|
||||||
|
let sa = stroke_color[3];
|
||||||
|
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("R").size(9).width(14),
|
||||||
|
slider(0.0..=255.0, sr as f32, move |v| Message::GeometryStrokeColorChanged([v as u8, sg, sb, sa])).step(1.0).width(Length::Fill),
|
||||||
|
text(format!("{}", sr)).size(9).width(24),
|
||||||
|
]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("G").size(9).width(14),
|
||||||
|
slider(0.0..=255.0, sg as f32, move |v| Message::GeometryStrokeColorChanged([sr, v as u8, sb, sa])).step(1.0).width(Length::Fill),
|
||||||
|
text(format!("{}", sg)).size(9).width(24),
|
||||||
|
]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("B").size(9).width(14),
|
||||||
|
slider(0.0..=255.0, sb as f32, move |v| Message::GeometryStrokeColorChanged([sr, sg, v as u8, sa])).step(1.0).width(Length::Fill),
|
||||||
|
text(format!("{}", sb)).size(9).width(24),
|
||||||
|
]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stroke color preview swatch
|
||||||
|
content = content.push(
|
||||||
|
container(Space::with_width(Length::Fill))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(14)
|
||||||
|
.style(move |_theme| iced::widget::container::Style {
|
||||||
|
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||||
|
sr as f32 / 255.0,
|
||||||
|
sg as f32 / 255.0,
|
||||||
|
sb as f32 / 255.0,
|
||||||
|
sa as f32 / 255.0,
|
||||||
|
))),
|
||||||
|
border: iced::Border::default().rounded(2).color(colors.border_high).width(1),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Opacity slider
|
||||||
|
let cur_opacity = shapes[idx].shape_opacity();
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
content = content.push(labeled_slider("Opacity", cur_opacity, 0.0..=1.0, Message::GeometryOpacityChanged));
|
||||||
|
|
||||||
|
// Hardness slider
|
||||||
|
let cur_hardness = shapes[idx].hardness();
|
||||||
|
content = content.push(labeled_slider("Hardness", cur_hardness, 0.0..=1.0, Message::GeometryHardnessChanged));
|
||||||
|
|
||||||
|
// Line caps (for Line shapes only)
|
||||||
|
if is_line {
|
||||||
|
let (cs, ce) = match &shapes[idx] {
|
||||||
|
VectorShape::Line { cap_start, cap_end, .. } => (*cap_start, *cap_end),
|
||||||
|
_ => (LineCap::Round, LineCap::Round),
|
||||||
|
};
|
||||||
|
|
||||||
|
content = content.push(Space::with_height(4));
|
||||||
|
content = content.push(section_header("LINE CAPS".to_string(), colors));
|
||||||
|
content = content.push(Space::with_height(2));
|
||||||
|
|
||||||
|
// Start cap combo
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("Start").size(10).width(40),
|
||||||
|
button(text(cs.label()).size(10)).padding([3, 6]).width(Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show cap options when clicked (inline list)
|
||||||
|
let mut cap_start_list = column![].spacing(1);
|
||||||
|
for &cap in LineCap::ALL {
|
||||||
|
let cap_val = cap;
|
||||||
|
let is_active = cs == cap;
|
||||||
|
cap_start_list = cap_start_list.push(
|
||||||
|
button(
|
||||||
|
row![
|
||||||
|
if is_active { text(">").size(10) } else { text(" ").size(10) },
|
||||||
|
text(cap.label()).size(10),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
)
|
||||||
|
.on_press(Message::GeometryLineCapStart(cap_val))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([2, 6]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
content = content.push(cap_start_list);
|
||||||
|
|
||||||
|
content = content.push(Space::with_height(2));
|
||||||
|
|
||||||
|
// End cap combo
|
||||||
|
content = content.push(
|
||||||
|
row![
|
||||||
|
text("End").size(10).width(40),
|
||||||
|
button(text(ce.label()).size(10)).padding([3, 6]).width(Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cap_end_list = column![].spacing(1);
|
||||||
|
for &cap in LineCap::ALL {
|
||||||
|
let cap_val = cap;
|
||||||
|
let is_active = ce == cap;
|
||||||
|
cap_end_list = cap_end_list.push(
|
||||||
|
button(
|
||||||
|
row![
|
||||||
|
if is_active { text(">").size(10) } else { text(" ").size(10) },
|
||||||
|
text(cap.label()).size(10),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
)
|
||||||
|
.on_press(Message::GeometryLineCapEnd(cap_val))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([2, 6]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
content = content.push(cap_end_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container(content.spacing(0))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(move |_theme| styles::panel_background(colors))
|
.style(move |_theme| styles::panel_background(colors))
|
||||||
|
|||||||
Reference in New Issue
Block a user