feat(iced): draw transform handles in selection overlay

This commit is contained in:
2026-07-15 03:07:33 +03:00
parent 7bff36d006
commit a8c1772807
2 changed files with 124 additions and 17 deletions
@@ -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<SelectionTransform>,
/// 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<Message> for OverlayProgram {
@@ -94,28 +169,34 @@ impl canvas::Program<Message> 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<Message> 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)
@@ -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<u8>,