From 4f057452553911c3b0ada1aa019747756af6fca5 Mon Sep 17 00:00:00 2001 From: Halit Can Date: Thu, 16 Jul 2026 05:17:52 +0300 Subject: [PATCH] feat(iced): add Apply/Cancel overlay and shape deletion --- hcie-iced-app/crates/hcie-iced-gui/src/app.rs | 98 ++++++++++++++++--- .../crates/hcie-iced-gui/src/canvas/mod.rs | 68 +++++++++++++ 2 files changed, 153 insertions(+), 13 deletions(-) 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 ad9c92e..b55cb2c 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -628,6 +628,10 @@ pub enum Message { VectorSelectDragEnd, VectorShapeDelete, VectorShapeSelect(Option), + VectorShapeApply, + VectorShapeCancel, + VectorShapeFlipH, + VectorShapeFlipV, // ── Lasso / polygon selection ─────────────────────── LassoDragMove(f32, f32), @@ -3520,17 +3524,14 @@ impl HcieIcedApp { Message::VectorShapeDelete => { if let Some(idx) = self.documents[self.active_doc].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.documents[self.active_doc].engine.delete_vector_shape(layer_id, idx); self.documents[self.active_doc].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); + } else { + return self.update(Message::Deselect); } } Message::VectorShapeSelect(idx) => { @@ -3542,6 +3543,78 @@ impl HcieIcedApp { self.vector_shapes_snapshot = None; } } + // ── Vector shape apply / cancel / flip ────── + Message::VectorShapeApply => { + // Commit edits: clear snapshot and deselect. + let doc = &mut self.documents[self.active_doc]; + doc.selected_vector_shape = None; + doc.vector_edit_handle = hcie_engine_api::VectorEditHandle::None; + self.vector_shapes_snapshot = None; + self.vector_drag_last = None; + } + Message::VectorShapeCancel => { + // Restore snapshot: put original shapes back, then deselect. + if let Some(snapshot) = self.vector_shapes_snapshot.take() { + let doc = &mut self.documents[self.active_doc]; + let layer_id = doc.engine.active_layer_id(); + if let Some(shapes) = doc.engine.get_layer_shapes_direct(layer_id) { + *shapes = snapshot; + } + doc.selected_vector_shape = None; + doc.vector_edit_handle = hcie_engine_api::VectorEditHandle::None; + self.vector_drag_last = None; + doc.engine.mark_composite_dirty(); + self.refresh_composite_if_needed(); + } + } + Message::VectorShapeFlipH => { + if let Some(idx) = self.documents[self.active_doc].selected_vector_shape { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if let Some(shapes) = self.documents[self.active_doc].engine.get_layer_shapes_direct(layer_id) { + if let Some(shape) = shapes.get_mut(idx) { + match shape { + hcie_engine_api::VectorShape::FreePath { pts, .. } => { + let cx = pts.iter().map(|p| p[0]).sum::() / pts.len() as f32; + for p in pts.iter_mut() { + p[0] = 2.0 * cx - p[0]; + } + } + _ => { + let (x1, y1, x2, y2) = shape.bounds(); + // Swap x1/x2 to flip horizontally. + shape.set_bounds(x2, y1, x1, y2); + } + } + self.documents[self.active_doc].engine.mark_composite_dirty(); + self.refresh_composite_if_needed(); + } + } + } + } + Message::VectorShapeFlipV => { + if let Some(idx) = self.documents[self.active_doc].selected_vector_shape { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if let Some(shapes) = self.documents[self.active_doc].engine.get_layer_shapes_direct(layer_id) { + if let Some(shape) = shapes.get_mut(idx) { + match shape { + hcie_engine_api::VectorShape::FreePath { pts, .. } => { + let cy = pts.iter().map(|p| p[1]).sum::() / pts.len() as f32; + for p in pts.iter_mut() { + p[1] = 2.0 * cy - p[1]; + } + } + _ => { + let (x1, y1, x2, y2) = shape.bounds(); + // Swap y1/y2 to flip vertically. + shape.set_bounds(x1, y2, x2, y1); + } + } + self.documents[self.active_doc].engine.mark_composite_dirty(); + self.refresh_composite_if_needed(); + } + } + } + } // ── Lasso / polygon selection ───────────────── Message::LassoDragMove(x, y) => { @@ -4877,10 +4950,7 @@ impl HcieIcedApp { } else if self.documents[self.active_doc].selection_transform.is_some() { return self.update(Message::TransformCancel); } else if self.documents[self.active_doc].selected_vector_shape.is_some() { - self.documents[self.active_doc].selected_vector_shape = None; - self.vector_shapes_snapshot = None; - self.documents[self.active_doc].vector_edit_handle = hcie_engine_api::VectorEditHandle::None; - self.vector_drag_last = None; + return self.update(Message::VectorShapeCancel); } else if self.documents[self.active_doc].selection_bounds.is_some() || self.documents[self.active_doc].selection_rect.is_some() { @@ -4892,13 +4962,15 @@ impl HcieIcedApp { } } Message::EnterPressed => { - // Mirrors egui's Enter: crop > transform > text-commit > viewer. + // Mirrors egui's Enter: crop > transform > text-commit > vector-apply > viewer. if self.documents[self.active_doc].crop_state.active { return self.update(Message::CropConfirm); } else if self.documents[self.active_doc].selection_transform.is_some() { return self.update(Message::TransformApply); } else if self.active_document_mut().text_draft.is_some() { return self.update(Message::TextCommit); + } else if self.documents[self.active_doc].selected_vector_shape.is_some() { + return self.update(Message::VectorShapeApply); } else { return self.update(Message::ViewerEnter); } @@ -5290,9 +5362,9 @@ impl HcieIcedApp { // reserved — egui maps Enter to crop confirm). // ── Named keys ─────────────────────────────── - // Delete = Clear selection / deselect + // Delete = Delete selected vector shape, or clear selection iced::keyboard::Key::Named(iced::keyboard::key::Named::Delete) => { - Some(Message::Deselect) + Some(Message::VectorShapeDelete) } // F11 = Fullscreen toggle iced::keyboard::Key::Named(iced::keyboard::key::Named::F11) => { diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs index 44eab9c..36acc01 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs @@ -1038,6 +1038,74 @@ pub fn view<'a>( stacked = stacked.push(positioned_editor); } + // ── Vector shape Apply / Cancel / Flip overlay ────────────────────── + // Floating buttons shown below the selected vector shape for quick + // commit, cancel, or flip operations. + if let Some((bx1, by1, bx2, by2)) = sel_vec_bounds { + let (pane_w, pane_h) = doc.pane_size; + let display_w = engine_w as f32 * zoom; + let display_h = engine_h as f32 * zoom; + let origin_x = (pane_w - display_w) / 2.0 + pan_offset.x; + let origin_y = (pane_h - display_h) / 2.0 + pan_offset.y; + + let cx = (bx1 + bx2) / 2.0; + let cy = (by2).max(by1) + 10.0; // below shape + let screen_x = (origin_x + cx * zoom).max(4.0); + let screen_y = (origin_y + cy * zoom).max(4.0); + + let btn_style = |_theme: &iced::Theme, _status: iced::widget::button::Status| iced::widget::button::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.15, 0.15, 0.18, 0.9))), + border: iced::Border::default() + .color(iced::Color::from_rgba(0.35, 0.35, 0.4, 1.0)) + .width(1) + .rounded(4), + text_color: iced::Color::from_rgb(0.9, 0.9, 0.9), + ..Default::default() + }; + + let apply_btn = iced::widget::button( + iced::widget::text("Apply").size(10), + ) + .on_press(Message::VectorShapeApply) + .padding([3, 8]) + .style(btn_style); + + let cancel_btn = iced::widget::button( + iced::widget::text("Cancel").size(10), + ) + .on_press(Message::VectorShapeCancel) + .padding([3, 8]) + .style(btn_style); + + let flip_h_btn = iced::widget::button( + iced::widget::text("Flip H").size(10), + ) + .on_press(Message::VectorShapeFlipH) + .padding([3, 8]) + .style(btn_style); + + let flip_v_btn = iced::widget::button( + iced::widget::text("Flip V").size(10), + ) + .on_press(Message::VectorShapeFlipV) + .padding([3, 8]) + .style(btn_style); + + let overlay_row = row![apply_btn, cancel_btn, flip_h_btn, flip_v_btn] + .spacing(4); + + let overlay_container = container(overlay_row) + .width(Length::Shrink) + .padding(iced::Padding { + top: screen_y, + bottom: 0.0, + left: screen_x, + right: 0.0, + }); + + stacked = stacked.push(overlay_container); + } + // ── Tool info strip (bottom of canvas area) ───────────────────────── let overlay_info = if let Some((x0, y0, x1, y1)) = doc.selection_rect { let w = (x1 - x0).abs() as u32;