feat(iced): draw transform handles in selection overlay
This commit is contained in:
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user