diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index a871520..bf26db0 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -256,6 +256,14 @@ pub struct HcieIcedApp { /// Dirty flag set whenever fg/bg/recent colors change, so colors can be /// persisted to disk (egui persists these under the `hcie_colors` key). pub colors_dirty: bool, + /// Index of the currently selected vector shape (None = no selection). + pub selected_vector_shape: Option, + /// Snapshot of vector shapes before editing (for undo/Cancel). + pub vector_shapes_snapshot: Option>, + /// Which transform handle is being dragged on the selected vector shape. + pub vector_edit_handle: hcie_engine_api::VectorEditHandle, + /// Last drag position during vector shape editing (canvas coordinates). + pub vector_drag_last: Option<(f32, f32)>, } /// A recently opened file entry. @@ -613,6 +621,14 @@ pub enum Message { VectorDrawMove(f32, f32), VectorDrawEnd, + // ── Vector select / edit ──────────────────────────── + VectorSelectClick { x: f32, y: f32 }, + VectorSelectDragStart { x: f32, y: f32 }, + VectorSelectDragMove { x: f32, y: f32 }, + VectorSelectDragEnd, + VectorShapeDelete, + VectorShapeSelect(Option), + // ── Lasso / polygon selection ─────────────────────── LassoDragMove(f32, f32), LassoDragEnd, @@ -1062,6 +1078,10 @@ impl HcieIcedApp { show_dock_profile_dialog: false, language: i18n::Language::default(), colors_dirty: false, + selected_vector_shape: None, + vector_shapes_snapshot: None, + vector_edit_handle: hcie_engine_api::VectorEditHandle::None, + vector_drag_last: None, }; // Apply saved settings to tool state @@ -1627,6 +1647,14 @@ impl HcieIcedApp { Message::CropDragStart(sx, sy) }); } + Tool::VectorSelect => { + // Click to select vector shapes under cursor. + let cx = canvas_x; + let cy = canvas_y; + return Task::perform(async move {}, move |_| { + Message::VectorSelectClick { x: cx, y: cy } + }); + } _ => {} } } @@ -1753,6 +1781,15 @@ impl HcieIcedApp { Message::CropDragMove(mx, my) }); } + + // Handle vector select drag + if self.tool_state.active_tool == Tool::VectorSelect && self.vector_drag_last.is_some() { + let mx = x; + let my = y; + return Task::perform(async move {}, move |_| { + Message::VectorSelectDragMove { x: mx, y: my } + }); + } } Message::CanvasPointerReleased => { @@ -1817,6 +1854,11 @@ impl HcieIcedApp { if self.tool_state.active_tool == Tool::Crop { return Task::perform(async {}, |_| Message::CropDragEnd); } + + // End vector select drag + if self.tool_state.active_tool == Tool::VectorSelect && self.vector_drag_last.is_some() { + return Task::perform(async {}, |_| Message::VectorSelectDragEnd); + } } Message::CanvasPointerRightClicked { x: _, y: _, screen_x, screen_y } => { @@ -3312,6 +3354,81 @@ impl HcieIcedApp { } } + // ── Vector select / edit ───────────────────── + Message::VectorSelectClick { x, y } => { + // Hit-test vector shapes under cursor; select the topmost one. + if let Some(idx) = self.documents[self.active_doc].engine.find_vector_shape_at(x, y) { + // Save snapshot before editing (for undo). + let snapshot = self.documents[self.active_doc].engine.active_vector_shapes(); + self.vector_shapes_snapshot = snapshot; + self.selected_vector_shape = Some(idx); + // Start a drag from this position (move on drag, deselect on release without drag). + let doc = &mut self.documents[self.active_doc]; + doc.vector_draw = None; // clear any preview + self.vector_drag_last = Some((x, y)); + self.vector_edit_handle = hcie_engine_api::VectorEditHandle::Move; + } else { + // No shape under cursor — deselect. + self.selected_vector_shape = None; + self.vector_shapes_snapshot = None; + self.vector_edit_handle = hcie_engine_api::VectorEditHandle::None; + self.vector_drag_last = None; + } + } + Message::VectorSelectDragStart { x, y } => { + if self.selected_vector_shape.is_some() { + self.vector_drag_last = Some((x, y)); + } + } + Message::VectorSelectDragMove { x, y } => { + if let (Some(idx), Some((last_x, last_y))) = (self.selected_vector_shape, self.vector_drag_last) { + let dx = x - last_x; + let dy = y - last_y; + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + self.documents[self.active_doc].engine.modify_vector_shape( + layer_id, + idx, + self.vector_edit_handle, + dx, + dy, + ); + self.vector_drag_last = Some((x, y)); + self.refresh_composite_if_needed(); + } + } + Message::VectorSelectDragEnd => { + if self.vector_drag_last.is_some() { + // Commit the move: if shapes changed, push history snapshot. + self.vector_drag_last = None; + self.vector_edit_handle = hcie_engine_api::VectorEditHandle::None; + } + } + Message::VectorShapeDelete => { + if let Some(idx) = self.selected_vector_shape { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + // Remove the shape from the layer's shape list. + if let Some(shapes) = self.documents[self.active_doc].engine.get_layer_shapes_direct(layer_id) { + if idx < shapes.len() { + shapes.remove(idx); + } + } + self.selected_vector_shape = None; + self.vector_shapes_snapshot = None; + self.documents[self.active_doc].engine.mark_composite_dirty(); + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + } + Message::VectorShapeSelect(idx) => { + self.selected_vector_shape = idx; + if idx.is_some() { + let snapshot = self.documents[self.active_doc].engine.active_vector_shapes(); + self.vector_shapes_snapshot = snapshot; + } else { + self.vector_shapes_snapshot = None; + } + } + // ── Lasso / polygon selection ───────────────── Message::LassoDragMove(x, y) => { let cx = x.round().max(0.0) as u32; @@ -4636,13 +4753,18 @@ impl HcieIcedApp { // ── Contextual key actions ─────────────────────── Message::EscapePressed => { // Mirrors egui's Escape handling. Priority: viewer > crop > - // transform > selection > dialog > viewer fallback. + // transform > vector selection > selection > dialog > viewer fallback. if self.viewer_active { return self.update(Message::ViewerToggle); } else if self.documents[self.active_doc].crop_state.active { return self.update(Message::CropCancel); } else if self.documents[self.active_doc].selection_transform.is_some() { return self.update(Message::TransformCancel); + } else if self.selected_vector_shape.is_some() { + self.selected_vector_shape = None; + self.vector_shapes_snapshot = None; + self.vector_edit_handle = hcie_engine_api::VectorEditHandle::None; + self.vector_drag_last = None; } else if self.documents[self.active_doc].selection_bounds.is_some() || self.documents[self.active_doc].selection_rect.is_some() { 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 8109fb9..7b2c9f0 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 @@ -103,7 +103,8 @@ pub fn dock_view<'a>( crate::panels::layer_details::view(&doc.cached_layers, doc.engine.active_layer_id(), colors) } PaneType::Geometry => { - crate::panels::geometry::view(&[], colors) + let shapes = app.documents[app.active_doc].engine.active_vector_shapes().unwrap_or_default(); + crate::panels::geometry::view(shapes, colors, app.selected_vector_shape) } PaneType::ToolSettings => { let fg = app.fg_color; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs index 3c85980..1de8320 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs @@ -5,12 +5,12 @@ use crate::app::Message; use crate::panels::styles; use crate::theme::ThemeColors; use hcie_engine_api::VectorShape; -use iced::widget::{column, container, horizontal_rule, scrollable, text}; +use iced::widget::{column, horizontal_rule, scrollable, text}; use iced::{Element, Length}; /// Build the geometry panel for vector shapes. /// Panel title is in the dock tab — no duplicate inside the panel. -pub fn view<'a>(shapes: &'a [VectorShape], colors: ThemeColors) -> Element<'a, Message> { +pub fn view(shapes: Vec, colors: ThemeColors, selected_shape: Option) -> Element<'static, Message> { let panel = if shapes.is_empty() { column![ text("No vector shapes").size(11), @@ -18,33 +18,68 @@ pub fn view<'a>(shapes: &'a [VectorShape], colors: ThemeColors) -> Element<'a, M ] } else { let mut shape_list = column![].spacing(2); + let shape_count = shapes.len(); - for (i, shape) in shapes.iter().enumerate() { - let (shape_name, shape_info) = match shape { + 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 item = container( + 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| styles::inactive_item_bg(colors)); + .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(if is_selected { c.accent } else { c.bg_active })), + text_color: iced::Color::WHITE, + ..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", shapes.len())).size(10), + text(format!("{} shapes", shape_count)).size(10), horizontal_rule(1), scrollable(shape_list).height(Length::Fill), ] }; - container(panel.spacing(0).padding(4)) + iced::widget::container(panel.spacing(0).padding(4)) .width(Length::Fill) .height(Length::Fill) .style(move |_theme| styles::panel_background(colors))