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:
2026-07-16 05:26:54 +03:00
parent 4f05745255
commit e53d144626
3 changed files with 632 additions and 59 deletions
@@ -260,6 +260,10 @@ pub struct HcieIcedApp {
pub vector_shapes_snapshot: Option<Vec<hcie_engine_api::VectorShape>>,
/// Last drag position during vector shape editing (canvas coordinates).
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.
@@ -633,6 +637,22 @@ pub enum Message {
VectorShapeFlipH,
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 ───────────────────────
LassoDragMove(f32, f32),
LassoDragEnd,
@@ -1086,6 +1106,8 @@ impl HcieIcedApp {
colors_dirty: false,
vector_shapes_snapshot: None,
vector_drag_last: None,
bool_shape_a: None,
bool_shape_b: None,
};
// 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 ─────────────────
Message::LassoDragMove(x, y) => {
let cx = x.round().max(0.0) as u32;
@@ -104,7 +104,13 @@ pub fn dock_view<'a>(
}
PaneType::Geometry => {
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 => {
let fg = app.fg_color;
@@ -1,85 +1,496 @@
//! Vector geometry panel — shape properties for vector layers.
//! Uses ThemeColors for consistent styling.
//! Vector geometry panel — full shape properties editor.
//!
//! 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::panels::styles;
use crate::theme::ThemeColors;
use hcie_engine_api::VectorShape;
use iced::widget::{column, horizontal_rule, scrollable, text};
use hcie_engine_api::{LineCap, VectorBooleanOp, VectorShape};
use iced::widget::{
button, checkbox, column, container, row, scrollable, slider, text, Space,
};
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.
pub fn view(shapes: Vec<VectorShape>, colors: ThemeColors, selected_shape: Option<usize>) -> Element<'static, Message> {
let panel = if shapes.is_empty() {
column![
text("No vector shapes").size(11),
text("Use vector tools to create shapes").size(10),
]
} else {
let mut shape_list = column![].spacing(2);
let shape_count = shapes.len();
pub fn view(
shapes: Vec<VectorShape>,
colors: ThemeColors,
selected_shape: Option<usize>,
bool_shape_a: Option<usize>,
bool_shape_b: Option<usize>,
) -> Element<'static, Message> {
let has_shapes = !shapes.is_empty();
let shape_count = shapes.len();
for (i, shape) in shapes.into_iter().enumerate() {
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()),
};
let mut content = column![].spacing(0).padding(4);
// ── 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 c = colors;
let item = iced::widget::button(
column![text(format!("{}. {}", i + 1, shape_name)).size(11), text(shape_info).size(9)].spacing(2)
)
.on_press(Message::VectorShapeSelect(Some(i)))
.width(Length::Fill)
.padding([4, 6])
.style(move |_theme: &iced::Theme, status: iced::widget::button::Status| {
match status {
iced::widget::button::Status::Active => {
if is_selected {
let item = button(text(label).size(11))
.on_press(Message::VectorShapeSelect(if is_selected { None } else { Some(i) }))
.width(Length::Fill)
.padding([4, 6])
.style(move |_theme: &iced::Theme, status: iced::widget::button::Status| {
match status {
iced::widget::button::Status::Active => {
if is_selected {
iced::widget::button::Style {
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 {
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,
..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 {
background: Some(iced::Background::Color(if is_selected { c.accent } else { c.bg_active })),
text_color: iced::Color::WHITE,
_ => iced::widget::button::Style {
background: Some(iced::Background::Color(c.bg_hover)),
text_color: c.text_primary,
..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);
}
column![
text(format!("{} shapes", shape_count)).size(10),
horizontal_rule(1),
scrollable(shape_list).height(Length::Fill),
]
};
content = content.push(
scrollable(shape_list).height(Length::Fill).width(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)
.height(Length::Fill)
.style(move |_theme| styles::panel_background(colors))