feat(iced): implement VectorSelect click-to-select with shape hit-testing
This commit is contained in:
@@ -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<usize>,
|
||||
/// Snapshot of vector shapes before editing (for undo/Cancel).
|
||||
pub vector_shapes_snapshot: Option<Vec<hcie_engine_api::VectorShape>>,
|
||||
/// 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<usize>),
|
||||
|
||||
// ── 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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<VectorShape>, colors: ThemeColors, selected_shape: Option<usize>) -> 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))
|
||||
|
||||
Reference in New Issue
Block a user