From a8c1772807aa88f546fd2c3a5613d5785e863bba Mon Sep 17 00:00:00 2001 From: Halit Can Date: Wed, 15 Jul 2026 03:07:33 +0300 Subject: [PATCH] feat(iced): draw transform handles in selection overlay --- .../crates/hcie-iced-gui/src/canvas/mod.rs | 139 ++++++++++++++++-- .../hcie-iced-gui/src/selection/transform.rs | 2 +- 2 files changed, 124 insertions(+), 17 deletions(-) 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 14f31ee..b726e16 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 @@ -24,6 +24,7 @@ pub mod shader_canvas; // pub mod render; use crate::app::Message; +use crate::selection::{SelectionTransform, TransformHandle}; use iced::widget::{canvas, column, container, row, text, Stack}; use iced::widget::canvas::{Frame, Path, Stroke}; use iced::widget::Shader; @@ -52,6 +53,10 @@ struct OverlayProgram { selection_rect: Option<(f32, f32, f32, f32)>, /// Vector draw preview in canvas-space ((x0, y0), (x1, y1)). vector_draw: Option<((f32, f32), (f32, f32))>, + /// Active selection transform (for drawing handles). + selection_transform: Option, + /// Current transform handle being hovered/dragged. + active_handle: TransformHandle, } /// Overlay state — tracks hover and cursor position for crosshair. @@ -78,6 +83,76 @@ impl OverlayProgram { let oy = raw_y.clamp(-(display_h - min_vis_h).max(0.0), (pane.height - min_vis_h).max(0.0)); (ox, oy, display_w, display_h) } + + /// Draw transform handles for selection. + fn draw_transform_handles( + &self, + frame: &mut Frame, + tr: &SelectionTransform, + origin_x: f32, + origin_y: f32, + ) { + let handle_size = TransformHandle::HANDLE_SIZE / self.zoom.max(1.0); + let half = handle_size / 2.0; + + // Calculate handle positions in viewport coordinates + let x = origin_x + tr.pos.x * self.zoom; + let y = origin_y + tr.pos.y * self.zoom; + let w = tr.size.x * self.zoom; + let h = tr.size.y * self.zoom; + + let handles = [ + (TransformHandle::TopLeft, x, y), + (TransformHandle::TopRight, x + w, y), + (TransformHandle::BottomLeft, x, y + h), + (TransformHandle::BottomRight, x + w, y + h), + (TransformHandle::Top, x + w / 2.0, y), + (TransformHandle::Bottom, x + w / 2.0, y + h), + (TransformHandle::Left, x, y + h / 2.0), + (TransformHandle::Right, x + w, y + h / 2.0), + ]; + + let handle_color = iced::Color::from_rgb(1.0, 1.0, 1.0); + let handle_stroke_color = iced::Color::from_rgb(0.0, 0.0, 0.0); + + for (handle, hx, hy) in &handles { + let rect = Path::rectangle( + Point::new(hx - half, hy - half), + Size::new(handle_size, handle_size), + ); + // White fill + frame.fill(&rect, handle_color); + // Black stroke + frame.stroke(&rect, Stroke { + style: canvas::stroke::Style::Solid(handle_stroke_color), + width: 1.0 / self.zoom.max(1.0), + ..Default::default() + }); + } + + // Draw rotation handle (circle above top center) + let rotate_y = y - TransformHandle::ROTATE_OFFSET / self.zoom.max(1.0); + let rotate_x = x + w / 2.0; + let rotate_radius = 4.0 / self.zoom.max(1.0); + let rotate_circle = Path::circle(Point::new(rotate_x, rotate_y), rotate_radius); + frame.fill(&rotate_circle, handle_color); + frame.stroke(&rotate_circle, Stroke { + style: canvas::stroke::Style::Solid(handle_stroke_color), + width: 1.0 / self.zoom.max(1.0), + ..Default::default() + }); + + // Draw line from top center to rotation handle + let line = Path::line( + Point::new(x + w / 2.0, y), + Point::new(rotate_x, rotate_y), + ); + frame.stroke(&line, Stroke { + style: canvas::stroke::Style::Solid(handle_stroke_color), + width: 1.0 / self.zoom.max(1.0), + ..Default::default() + }); + } } impl canvas::Program for OverlayProgram { @@ -94,28 +169,34 @@ impl canvas::Program for OverlayProgram { let (origin_x, origin_y, _display_w, _display_h) = self.canvas_origin(bounds.size()); let mut geometries = Vec::new(); - // ── Selection rectangle ────────────────────────────────────────── - // ── Vector shape preview ───────────────────────────────────────── - let has_overlays = self.selection_rect.is_some() || self.vector_draw.is_some(); + // ── Selection rectangle or transform handles ─────────────────────── + let has_overlays = self.selection_rect.is_some() + || self.vector_draw.is_some() + || self.selection_transform.is_some(); + if has_overlays { let mut frame = Frame::new(renderer, bounds.size()); + // Draw selection rectangle (when not in transform mode) if let Some((x0, y0, x1, y1)) = self.selection_rect { - let sel_x = origin_x + x0.min(x1) * self.zoom; - let sel_y = origin_y + y0.min(y1) * self.zoom; - let sel_w = (x1 - x0).abs() * self.zoom; - let sel_h = (y1 - y0).abs() * self.zoom; - let sel_path = Path::rectangle( - Point::new(sel_x, sel_y), - Size::new(sel_w, sel_h), - ); - frame.stroke(&sel_path, Stroke { - style: canvas::stroke::Style::Solid(iced::Color::from_rgb(0.0, 0.6, 1.0)), - width: 2.0 / self.zoom.max(1.0), - ..Default::default() - }); + if self.selection_transform.is_none() { + let sel_x = origin_x + x0.min(x1) * self.zoom; + let sel_y = origin_y + y0.min(y1) * self.zoom; + let sel_w = (x1 - x0).abs() * self.zoom; + let sel_h = (y1 - y0).abs() * self.zoom; + let sel_path = Path::rectangle( + Point::new(sel_x, sel_y), + Size::new(sel_w, sel_h), + ); + frame.stroke(&sel_path, Stroke { + style: canvas::stroke::Style::Solid(iced::Color::from_rgb(0.0, 0.6, 1.0)), + width: 2.0 / self.zoom.max(1.0), + ..Default::default() + }); + } } + // Draw vector shape preview if let Some(((x0, y0), (x1, y1))) = self.vector_draw { let v_x = origin_x + x0.min(x1) * self.zoom; let v_y = origin_y + y0.min(y1) * self.zoom; @@ -136,6 +217,30 @@ impl canvas::Program for OverlayProgram { }); } + // Draw transform handles (when in transform mode) + if let Some(ref tr) = self.selection_transform { + if !tr.is_empty() { + // Draw dotted bounding box + let x = origin_x + tr.pos.x * self.zoom; + let y = origin_y + tr.pos.y * self.zoom; + let w = tr.size.x * self.zoom; + let h = tr.size.y * self.zoom; + let bbox = Path::rectangle(Point::new(x, y), Size::new(w, h)); + frame.stroke(&bbox, Stroke { + style: canvas::stroke::Style::Solid(iced::Color::from_rgb(0.5, 0.5, 1.0)), + width: 1.0 / self.zoom.max(1.0), + line_dash: canvas::LineDash { + segments: &[4.0, 4.0], + offset: 0, + }, + ..Default::default() + }); + + // Draw handles + self.draw_transform_handles(&mut frame, tr, origin_x, origin_y); + } + } + geometries.push(frame.into_geometry()); } @@ -245,6 +350,8 @@ pub fn view<'a>( pan_offset, selection_rect: doc.selection_rect, vector_draw: doc.vector_draw, + selection_transform: doc.selection_transform.clone(), + active_handle: TransformHandle::None, // TODO: track hover state }; let overlay_canvas = canvas(overlay_program) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/selection/transform.rs b/hcie-iced-app/crates/hcie-iced-gui/src/selection/transform.rs index 2a72a4e..ec47a8c 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/selection/transform.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/selection/transform.rs @@ -8,7 +8,7 @@ use iced::Vector; /// Floating pixel selection that can be moved, rotated, and scaled. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct SelectionTransform { /// RGBA pixel data for the selection pub pixels: Vec,