feat(iced): add selection state types for transform, crop
This commit is contained in:
@@ -16,6 +16,7 @@ use crate::dialogs;
|
||||
use crate::dock::state::{DockState, PaneType};
|
||||
use crate::io::tablet::TabletState;
|
||||
use crate::panels;
|
||||
use crate::selection::{CropState, SelectionTransform, TransformHandle};
|
||||
use crate::theme::ThemeState;
|
||||
use hcie_engine_api::{BlendMode, BrushTip, BrushStyle, Engine, FilterType, LayerStyle, Tool, ZOOM_MAX, ZOOM_MIN};
|
||||
use iced::widget::{column, container, text};
|
||||
@@ -196,6 +197,14 @@ pub struct IcedDocument {
|
||||
/// Whether a full texture upload is needed (first frame, resize, file load).
|
||||
/// Set to true after loading a file or changing canvas dimensions.
|
||||
pub full_upload: std::cell::RefCell<bool>,
|
||||
/// Active selection transform (floating pixels after cut/copy-paste)
|
||||
pub selection_transform: Option<SelectionTransform>,
|
||||
/// Transform handle being dragged
|
||||
pub transform_drag_handle: TransformHandle,
|
||||
/// Internal clipboard for copy/paste operations
|
||||
pub internal_clipboard: Option<SelectionTransform>,
|
||||
/// Crop tool state
|
||||
pub crop_state: CropState,
|
||||
}
|
||||
|
||||
/// Drawing tool state.
|
||||
@@ -512,6 +521,10 @@ impl HcieIcedApp {
|
||||
render_generation: 0,
|
||||
dirty_region: std::cell::RefCell::new(None),
|
||||
full_upload: std::cell::RefCell::new(true),
|
||||
selection_transform: None,
|
||||
transform_drag_handle: TransformHandle::None,
|
||||
internal_clipboard: None,
|
||||
crop_state: CropState::default(),
|
||||
};
|
||||
|
||||
// Load saved settings
|
||||
@@ -1788,6 +1801,10 @@ impl HcieIcedApp {
|
||||
render_generation: 0,
|
||||
dirty_region: std::cell::RefCell::new(None),
|
||||
full_upload: std::cell::RefCell::new(true),
|
||||
selection_transform: None,
|
||||
transform_drag_handle: TransformHandle::None,
|
||||
internal_clipboard: None,
|
||||
crop_state: CropState::default(),
|
||||
});
|
||||
self.active_doc = self.documents.len() - 1;
|
||||
self.active_dialog = ActiveDialog::None;
|
||||
@@ -2203,6 +2220,10 @@ impl HcieIcedApp {
|
||||
render_generation: 0,
|
||||
dirty_region: std::cell::RefCell::new(None),
|
||||
full_upload: std::cell::RefCell::new(true),
|
||||
selection_transform: None,
|
||||
transform_drag_handle: TransformHandle::None,
|
||||
internal_clipboard: None,
|
||||
crop_state: CropState::default(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2338,169 +2359,241 @@ impl HcieIcedApp {
|
||||
Message::MenuAction(menu_idx, item_idx) => {
|
||||
self.active_menu = None;
|
||||
match (menu_idx, item_idx) {
|
||||
(0, 0) => return Task::perform(async {}, |_| Message::DialogOpen(ActiveDialog::NewImage)), // New
|
||||
(0, 1) => return Task::perform(async {}, |_| Message::OpenFileRfd), // Open
|
||||
(0, _) if item_idx >= 2 => {
|
||||
let recent_count = self.recent_files.len().min(10);
|
||||
let static_base = if recent_count > 0 { 4 + recent_count + 3 } else { 2 };
|
||||
if item_idx >= static_base {
|
||||
match item_idx - static_base {
|
||||
0 => return Task::perform(async {}, |_| Message::SaveFileAs),
|
||||
1 => return Task::perform(async {}, |_| Message::SaveFileAs),
|
||||
6 => return Task::perform(async {}, |_| Message::SaveFileAs), // Export PNG
|
||||
7 => return Task::perform(async {}, |_| Message::SaveFileAs), // Export JPEG
|
||||
9 => return Task::perform(async {}, |_| Message::DialogOpen(ActiveDialog::CloseConfirm)),
|
||||
10 => {
|
||||
if let Some(id) = self.window_id {
|
||||
return iced::window::close(id);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════
|
||||
// File menu (0) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
(0, 0) => return Task::perform(async {}, |_| Message::DialogOpen(ActiveDialog::NewImage)), // New...
|
||||
(0, 1) => return Task::perform(async {}, |_| Message::OpenFileRfd), // Open...
|
||||
// (0, 2) = Open & Place...
|
||||
// (0, 3) = Open More (submenu)
|
||||
// (0, 4) = separator
|
||||
// (0, 5) = Share
|
||||
// (0, 6) = separator
|
||||
(0, 7) => return Task::perform(async {}, |_| Message::SaveFile), // Save
|
||||
(0, 8) => return Task::perform(async {}, |_| Message::SaveFileAs), // Save as PSD
|
||||
// (0, 9) = Save More (submenu)
|
||||
// (0, 10) = Export as (submenu)
|
||||
(0, 11) => return Task::perform(async {}, |_| Message::SaveFileAs), // Print...
|
||||
// (0, 12) = separator
|
||||
// (0, 13) = Export Layers...
|
||||
// (0, 14) = Export Color Lookup...
|
||||
// (0, 15) = File Info...
|
||||
// (0, 16) = separator
|
||||
// (0, 17) = Automate (submenu)
|
||||
// (0, 18) = Script...
|
||||
|
||||
// ── Edit menu (1) ──
|
||||
(1, 0) => return Task::perform(async {}, |_| Message::Undo), // Undo
|
||||
(1, 1) => return Task::perform(async {}, |_| Message::Redo), // Redo
|
||||
// (1, 2) = separator
|
||||
(1, 3) => return Task::perform(async {}, |_| Message::CopyImage), // Cut (copy for now)
|
||||
(1, 4) => return Task::perform(async {}, |_| Message::CopyImage), // Copy
|
||||
(1, 5) => return Task::perform(async {}, |_| Message::PasteImage), // Paste
|
||||
// (1, 6) = separator
|
||||
(1, 7) => { // Select All
|
||||
let w = self.documents[self.active_doc].engine.canvas_width() as f32;
|
||||
let h = self.documents[self.active_doc].engine.canvas_height() as f32;
|
||||
self.documents[self.active_doc].selection_rect = Some((0.0, 0.0, w, h));
|
||||
}
|
||||
(1, 8) => { self.documents[self.active_doc].selection_rect = None; } // Deselect
|
||||
// (1, 9) = Invert Selection — TODO
|
||||
// (1, 10) = separator
|
||||
// (1, 11) = Fill — TODO
|
||||
// ══════════════════════════════════════════
|
||||
// Edit menu (1) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
(1, 0) => return Task::perform(async {}, |_| Message::Undo), // Undo / Redo
|
||||
(1, 1) => return Task::perform(async {}, |_| Message::Redo), // Step Forward
|
||||
(1, 2) => return Task::perform(async {}, |_| Message::Undo), // Step Backward
|
||||
// (1, 3) = separator
|
||||
// (1, 4) = Fade...
|
||||
// (1, 5) = separator
|
||||
(1, 6) => return Task::perform(async {}, |_| Message::CopyImage), // Cut
|
||||
(1, 7) => return Task::perform(async {}, |_| Message::CopyImage), // Copy
|
||||
(1, 8) => return Task::perform(async {}, |_| Message::CopyImage), // Copy Merged
|
||||
(1, 9) => return Task::perform(async {}, |_| Message::PasteImage), // Paste
|
||||
// (1, 10) = Clear
|
||||
// (1, 11) = separator
|
||||
// (1, 12) = Fill...
|
||||
// (1, 13) = Stroke...
|
||||
// (1, 14) = separator
|
||||
// (1, 15) = Content-Aware Scale
|
||||
// (1, 16) = Puppet Warp
|
||||
// (1, 17) = Perspective Warp
|
||||
// (1, 18) = Free Transform
|
||||
// (1, 19) = Transform (submenu)
|
||||
// (1, 20) = Auto-Align
|
||||
// (1, 21) = Auto-Blend
|
||||
// (1, 22) = separator
|
||||
// (1, 23) = Assign Profile (submenu)
|
||||
// (1, 24) = Convert to Profile (submenu)
|
||||
// (1, 25) = separator
|
||||
// (1, 26) = Define New (submenu)
|
||||
// (1, 27) = Preset Manager...
|
||||
// (1, 28) = Preferences...
|
||||
// (1, 29) = Local Storage...
|
||||
|
||||
// ── Tools menu (2) ──
|
||||
(2, 0) => self.tool_state.active_tool = Tool::Move,
|
||||
(2, 1) => self.tool_state.active_tool = Tool::VectorSelect,
|
||||
// (2, 2) = separator
|
||||
(2, 3) => self.tool_state.active_tool = Tool::Select,
|
||||
(2, 4) => self.tool_state.active_tool = Tool::Lasso,
|
||||
(2, 5) => self.tool_state.active_tool = Tool::PolygonSelect,
|
||||
(2, 6) => self.tool_state.active_tool = Tool::MagicWand,
|
||||
// (2, 7) = separator
|
||||
(2, 8) => self.tool_state.active_tool = Tool::Crop,
|
||||
(2, 9) => self.tool_state.active_tool = Tool::Eyedropper,
|
||||
// (2, 10) = separator
|
||||
(2, 11) => self.tool_state.active_tool = Tool::Brush,
|
||||
(2, 12) => self.tool_state.active_tool = Tool::Pen,
|
||||
(2, 13) => self.tool_state.active_tool = Tool::Spray,
|
||||
(2, 14) => self.tool_state.active_tool = Tool::Eraser,
|
||||
// (2, 15) = separator
|
||||
(2, 16) => self.tool_state.active_tool = Tool::FloodFill,
|
||||
(2, 17) => self.tool_state.active_tool = Tool::Gradient,
|
||||
// (2, 18) = separator
|
||||
(2, 19) => self.tool_state.active_tool = Tool::Text,
|
||||
// (2, 20) = separator
|
||||
(2, 21) => self.tool_state.active_tool = Tool::VectorLine,
|
||||
(2, 22) => self.tool_state.active_tool = Tool::VectorRect,
|
||||
(2, 23) => self.tool_state.active_tool = Tool::VectorCircle,
|
||||
// (2, 24) = separator
|
||||
// (2, 25) = Reset Tool — TODO
|
||||
// ══════════════════════════════════════════
|
||||
// Image menu (2) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
// (2, 0) = Mode (submenu)
|
||||
// (2, 1) = Adjustments (submenu)
|
||||
// (2, 2) = Auto Tone
|
||||
// (2, 3) = Auto Contrast
|
||||
// (2, 4) = Auto Color
|
||||
// (2, 5) = separator
|
||||
// (2, 6) = Reduce Colors...
|
||||
// (2, 7) = Vectorize Bitmap...
|
||||
// (2, 8) = Wavelet Decompose...
|
||||
// (2, 9) = separator
|
||||
// (2, 10) = Canvas Size...
|
||||
// (2, 11) = Image Size...
|
||||
// (2, 12) = Transform (submenu)
|
||||
// (2, 13) = Crop
|
||||
// (2, 14) = Trim...
|
||||
// (2, 15) = Reveal All
|
||||
// (2, 16) = separator
|
||||
// (2, 17) = Duplicate
|
||||
// (2, 18) = Apply Image...
|
||||
// (2, 19) = separator
|
||||
// (2, 20) = Variables...
|
||||
|
||||
// ── Image menu (3) ──
|
||||
// (3, 0) = Canvas Size — TODO
|
||||
// (3, 1) = Image Size — TODO
|
||||
// (3, 2) = separator
|
||||
// (3, 5) = Rotate 180 — TODO
|
||||
// (3, 6) = separator
|
||||
// (3, 9) = separator
|
||||
// (3, 10) = Crop to Selection — TODO
|
||||
|
||||
// ── Layer menu (4) ──
|
||||
(4, 0) => return Task::perform(async {}, |_| Message::LayerAdd), // New Layer
|
||||
// (4, 1) = Duplicate Layer — TODO
|
||||
(4, 2) => { // Delete Layer
|
||||
// ══════════════════════════════════════════
|
||||
// Layer menu (3) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
(3, 0) => return Task::perform(async {}, |_| Message::LayerAdd), // New (submenu)
|
||||
// (3, 1) = Duplicate Layer — TODO: engine doesn't support yet
|
||||
// (3, 2) = Duplicate Into...
|
||||
(3, 3) => { // Delete
|
||||
let id = self.documents[self.active_doc].engine.active_layer_id();
|
||||
self.documents[self.active_doc].engine.delete_layer(id);
|
||||
}
|
||||
// (4, 3) = separator
|
||||
(4, 4) => return Task::perform(async {}, |_| Message::LayerMergeDown), // Merge Down
|
||||
(4, 5) => return Task::perform(async {}, |_| Message::LayerFlatten), // Flatten Image
|
||||
// (4, 6) = separator
|
||||
(4, 7) => return Task::perform(async {}, |_| Message::OpenLayerStyleDialog), // Layer Styles
|
||||
// (4, 10) = separator
|
||||
// (4, 11) = Align Left — TODO
|
||||
// (4, 12) = Align Center — TODO
|
||||
// (4, 13) = Align Right — TODO
|
||||
// (3, 4) = separator
|
||||
// (3, 5) = Text (submenu)
|
||||
(3, 6) => return Task::perform(async {}, |_| Message::OpenLayerStyleDialog), // Layer Style (submenu)
|
||||
// (3, 7) = separator
|
||||
// (3, 8) = New Fill Layer (submenu)
|
||||
// (3, 9) = New Adjustment Layer (submenu)
|
||||
// (3, 10) = separator
|
||||
// (3, 11) = Raster Mask (submenu)
|
||||
// (3, 12) = Vector Mask (submenu)
|
||||
// (3, 13) = Clipping Mask (disabled)
|
||||
// (3, 14) = separator
|
||||
// (3, 15) = Smart Object (submenu)
|
||||
// (3, 16) = Rasterize
|
||||
// (3, 17) = Rasterize Layer Style
|
||||
// (3, 18) = separator
|
||||
// (3, 19) = Group Layers
|
||||
// (3, 20) = Arrange (submenu)
|
||||
// (3, 21) = Combine Shapes (submenu)
|
||||
// (3, 22) = Animation (submenu)
|
||||
// (3, 23) = separator
|
||||
(3, 24) => return Task::perform(async {}, |_| Message::LayerMergeDown), // Merge Down
|
||||
(3, 25) => return Task::perform(async {}, |_| Message::LayerFlatten), // Flatten Image
|
||||
// (3, 26) = Defringe
|
||||
|
||||
// ── Filter menu (5) ──
|
||||
// (5, 0) = ─ Blur (disabled)
|
||||
(5, 1) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::BoxBlur)),
|
||||
(5, 2) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::GaussianBlur)),
|
||||
(5, 3) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::MotionBlur)),
|
||||
// (5, 4) = ─ Sharpen (disabled)
|
||||
(5, 5) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::UnsharpMask)),
|
||||
// (5, 6) = ─ Pixelate (disabled)
|
||||
(5, 7) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Mosaic)),
|
||||
(5, 8) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Crystallize)),
|
||||
// (5, 9) = ─ Distort (disabled)
|
||||
(5, 10) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Pinch)),
|
||||
(5, 11) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Twirl)),
|
||||
// (5, 12) = ─ Stylize (disabled)
|
||||
(5, 13) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::OilPaint)),
|
||||
// (5, 14) = ─ Color & Light (disabled)
|
||||
(5, 15) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Levels)),
|
||||
(5, 16) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Vibrance)),
|
||||
(5, 17) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Exposure)),
|
||||
(5, 18) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Posterize)),
|
||||
(5, 19) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Threshold)),
|
||||
// (5, 20) = ─ Restore (disabled)
|
||||
(5, 21) => return Task::perform(async {}, |_| Message::FilterSelect(FilterType::Dehaze)),
|
||||
|
||||
// ── Select menu (6) ──
|
||||
(6, 0) => { // Select All
|
||||
// ══════════════════════════════════════════
|
||||
// Select menu (4) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
(4, 0) => { // All
|
||||
let w = self.documents[self.active_doc].engine.canvas_width() as f32;
|
||||
let h = self.documents[self.active_doc].engine.canvas_height() as f32;
|
||||
self.documents[self.active_doc].selection_rect = Some((0.0, 0.0, w, h));
|
||||
}
|
||||
(6, 1) => { self.documents[self.active_doc].selection_rect = None; } // Deselect
|
||||
// (6, 2) = Invert Selection — TODO
|
||||
// (6, 3) = separator
|
||||
(6, 4) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Grow"); } // Grow
|
||||
(6, 5) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Shrink"); } // Shrink
|
||||
(6, 6) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Feather"); } // Feather
|
||||
(4, 1) => { self.documents[self.active_doc].selection_rect = None; } // Deselect
|
||||
// (4, 2) = Inverse
|
||||
// (4, 3) = separator
|
||||
// (4, 4) = Remove BG
|
||||
// (4, 5) = Color Range...
|
||||
// (4, 6) = Magic Cut...
|
||||
// (4, 7) = Subject
|
||||
// (4, 8) = separator
|
||||
// (4, 9) = Refine Edge...
|
||||
// (4, 10) = Modify (submenu)
|
||||
(4, 11) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Grow"); } // Grow
|
||||
// (4, 12) = Similar
|
||||
// (4, 13) = separator
|
||||
// (4, 14) = Transform Selection
|
||||
// (4, 15) = Quick Mask Mode
|
||||
// (4, 16) = separator
|
||||
// (4, 17) = Load Selection
|
||||
// (4, 18) = Save Selection
|
||||
|
||||
// ── View menu (7) ──
|
||||
(7, 0) => return Task::perform(async {}, |_| Message::CanvasZoomRelative(1.1)), // Zoom In
|
||||
(7, 1) => return Task::perform(async {}, |_| Message::CanvasZoomRelative(1.0 / 1.1)), // Zoom Out
|
||||
(7, 2) => return Task::perform(async {}, |_| Message::CanvasZoomSet(1.0)), // Zoom Reset
|
||||
// (7, 3) = separator
|
||||
(7, 4) => { // Fit to Window
|
||||
// Will be handled in view() with access to window size
|
||||
// ══════════════════════════════════════════
|
||||
// Filter menu (5) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
// (5, 0) = Last Filter
|
||||
// (5, 1) = separator
|
||||
// (5, 2) = Filter Gallery...
|
||||
// (5, 3) = Lens Correction...
|
||||
// (5, 4) = Camera Raw...
|
||||
// (5, 5) = Liquify...
|
||||
// (5, 6) = Vanishing Point...
|
||||
// (5, 7) = separator
|
||||
// (5, 8) = 3D (submenu)
|
||||
// (5, 9) = Blur (submenu)
|
||||
// (5, 10) = Blur Gallery (submenu)
|
||||
// (5, 11) = Distort (submenu)
|
||||
// (5, 12) = Noise (submenu)
|
||||
// (5, 13) = Pixelate (submenu)
|
||||
// (5, 14) = Render (submenu)
|
||||
// (5, 15) = Sharpen (submenu)
|
||||
// (5, 16) = Stylize (submenu)
|
||||
// (5, 17) = Other (submenu)
|
||||
// (5, 18) = Fourier (submenu)
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// View menu (6) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
(6, 0) => return Task::perform(async {}, |_| Message::CanvasZoomRelative(1.1)), // Zoom In
|
||||
(6, 1) => return Task::perform(async {}, |_| Message::CanvasZoomRelative(1.0 / 1.1)), // Zoom Out
|
||||
(6, 2) => { // Fit The Area
|
||||
self.documents[self.active_doc].zoom = 1.0;
|
||||
}
|
||||
(7, 5) => return Task::perform(async {}, |_| Message::CanvasZoomSet(1.0)), // 100%
|
||||
(7, 6) => return Task::perform(async {}, |_| Message::CanvasZoomSet(2.0)), // 200%
|
||||
// (7, 7) = separator
|
||||
// (7, 8) = Debug Mode — TODO
|
||||
(6, 3) => return Task::perform(async {}, |_| Message::CanvasZoomSet(1.0)), // Pixel to Pixel (100%)
|
||||
// (6, 4) = Pattern Preview
|
||||
// (6, 5) = separator
|
||||
// (6, 6) = Mode (submenu)
|
||||
// (6, 7) = Extras
|
||||
// (6, 8) = Show (submenu)
|
||||
// (6, 9) = separator
|
||||
// (6, 10) = Rulers
|
||||
// (6, 11) = separator
|
||||
// (6, 12) = Snap
|
||||
// (6, 13) = Snap To (submenu)
|
||||
// (6, 14) = separator
|
||||
// (6, 15) = Lock Guides
|
||||
// (6, 16) = Clear Guides
|
||||
// (6, 17) = Add Guides...
|
||||
// (6, 18) = Guides from Layer
|
||||
// (6, 19) = separator
|
||||
// (6, 20) = Clear Slices
|
||||
|
||||
// ── Window menu (8) ──
|
||||
(8, 0) => self.dock.reopen_pane(PaneType::Layers),
|
||||
(8, 1) => self.dock.reopen_pane(PaneType::History),
|
||||
(8, 2) => self.dock.reopen_pane(PaneType::Brushes),
|
||||
(8, 3) => self.dock.reopen_pane(PaneType::Filters),
|
||||
(8, 4) => self.dock.reopen_pane(PaneType::ColorPicker),
|
||||
(8, 5) => self.dock.reopen_pane(PaneType::Properties),
|
||||
(8, 6) => self.dock.reopen_pane(PaneType::AiChat),
|
||||
(8, 7) => self.dock.reopen_pane(PaneType::Geometry),
|
||||
(8, 8) => self.dock.reopen_pane(PaneType::Script),
|
||||
(8, 9) => self.dock.reopen_pane(PaneType::LayerDetails),
|
||||
// ══════════════════════════════════════════
|
||||
// Window menu (7) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
(7, 0) => self.dock.reopen_pane(PaneType::Layers),
|
||||
(7, 1) => self.dock.reopen_pane(PaneType::History),
|
||||
(7, 2) => self.dock.reopen_pane(PaneType::Brushes),
|
||||
(7, 3) => self.dock.reopen_pane(PaneType::Filters),
|
||||
(7, 4) => self.dock.reopen_pane(PaneType::ColorPicker),
|
||||
(7, 5) => self.dock.reopen_pane(PaneType::Properties),
|
||||
(7, 6) => self.dock.reopen_pane(PaneType::AiChat),
|
||||
(7, 7) => self.dock.reopen_pane(PaneType::Geometry),
|
||||
(7, 8) => self.dock.reopen_pane(PaneType::Script),
|
||||
(7, 9) => self.dock.reopen_pane(PaneType::LayerDetails),
|
||||
|
||||
// ── Help menu (9) ──
|
||||
// (9, 0) = About — TODO
|
||||
// (9, 1) = Documentation — TODO
|
||||
// (9, 2) = License — TODO
|
||||
// ══════════════════════════════════════════
|
||||
// More menu (8) — Photopea-style
|
||||
// ══════════════════════════════════════════
|
||||
// (8, 0) = More (submenu)
|
||||
// (8, 1) = separator
|
||||
// (8, 2) = Plugins
|
||||
// (8, 3) = Actions
|
||||
// (8, 4) = Adjustments
|
||||
// (8, 5) = Brush
|
||||
// (8, 6) = Channels
|
||||
// (8, 7) = Character
|
||||
// (8, 8) = Character Styles
|
||||
// (8, 9) = Color
|
||||
// (8, 10) = Glyphs
|
||||
// (8, 11) = Histogram
|
||||
// (8, 12) = History
|
||||
// (8, 13) = Info
|
||||
// (8, 14) = Navigator
|
||||
// (8, 15) = Notes
|
||||
// (8, 16) = Paragraph
|
||||
// (8, 17) = Paths
|
||||
// (8, 18) = Properties
|
||||
// (8, 19) = Style
|
||||
// (8, 20) = Swatches
|
||||
// (8, 21) = Tool Presets
|
||||
// (8, 22) = Vector Info
|
||||
|
||||
// Ignore separators and unimplemented items
|
||||
// Ignore separators, submenus, and unimplemented items
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod dialogs;
|
||||
mod dock;
|
||||
mod io;
|
||||
mod panels;
|
||||
mod selection;
|
||||
mod settings;
|
||||
mod sidebar;
|
||||
mod theme;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! State for the crop tool.
|
||||
//!
|
||||
//! `CropState` manages the lifecycle of a crop rectangle from initial drag
|
||||
//! through confirmation or cancellation.
|
||||
|
||||
/// State for the crop tool.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CropState {
|
||||
/// Crop rectangle in canvas coordinates (x, y, width, height)
|
||||
pub rect: Option<(f32, f32, f32, f32)>,
|
||||
/// Whether crop is being dragged
|
||||
pub is_dragging: bool,
|
||||
/// Drag start position in canvas coordinates
|
||||
pub drag_start: Option<(f32, f32)>,
|
||||
/// Whether crop overlay is active
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl CropState {
|
||||
/// Begin a new crop drag at the given canvas position.
|
||||
pub fn start_drag(&mut self, x: f32, y: f32) {
|
||||
self.is_dragging = true;
|
||||
self.drag_start = Some((x, y));
|
||||
self.rect = Some((x, y, 0.0, 0.0));
|
||||
self.active = true;
|
||||
}
|
||||
|
||||
/// Update the crop rectangle during a drag.
|
||||
pub fn update_drag(&mut self, x: f32, y: f32) {
|
||||
if let Some((start_x, start_y)) = self.drag_start {
|
||||
let min_x = start_x.min(x);
|
||||
let min_y = start_y.min(y);
|
||||
let w = (x - start_x).abs();
|
||||
let h = (y - start_y).abs();
|
||||
self.rect = Some((min_x, min_y, w, h));
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the drag (mouse released).
|
||||
pub fn end_drag(&mut self) {
|
||||
self.is_dragging = false;
|
||||
self.drag_start = None;
|
||||
}
|
||||
|
||||
/// Confirm the crop and return integer pixel bounds.
|
||||
///
|
||||
/// Returns `None` if the crop rectangle is too small.
|
||||
pub fn confirm(&mut self) -> Option<(u32, u32, u32, u32)> {
|
||||
if let Some((x, y, w, h)) = self.rect {
|
||||
if w > 1.0 && h > 1.0 {
|
||||
self.active = false;
|
||||
self.rect = None;
|
||||
return Some((x as u32, y as u32, w as u32, h as u32));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Cancel the crop and clear all state.
|
||||
pub fn cancel(&mut self) {
|
||||
self.active = false;
|
||||
self.rect = None;
|
||||
self.is_dragging = false;
|
||||
self.drag_start = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Selection state types for the iced GUI.
|
||||
//!
|
||||
//! Contains types for managing floating pixel selections (cut/copy-paste),
|
||||
//! transform handles, and the crop tool.
|
||||
|
||||
pub mod crop;
|
||||
pub mod transform;
|
||||
|
||||
pub use crop::CropState;
|
||||
pub use transform::{SelectionTransform, TransformHandle};
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Floating pixel selection that can be moved, rotated, and scaled.
|
||||
//!
|
||||
//! `SelectionTransform` represents a rectangular selection of pixels that has
|
||||
//! been cut or copied from the canvas and can be freely transformed before being
|
||||
//! pasted back. `TransformHandle` identifies which part of the selection's
|
||||
//! bounding box the user is interacting with.
|
||||
|
||||
use iced::Vector;
|
||||
|
||||
/// Floating pixel selection that can be moved, rotated, and scaled.
|
||||
#[derive(Clone)]
|
||||
pub struct SelectionTransform {
|
||||
/// RGBA pixel data for the selection
|
||||
pub pixels: Vec<u8>,
|
||||
/// Width of the selection in pixels
|
||||
pub width: u32,
|
||||
/// Height of the selection in pixels
|
||||
pub height: u32,
|
||||
/// Position in canvas coordinates
|
||||
pub pos: Vector,
|
||||
/// Display size (may differ from pixel dimensions when scaled)
|
||||
pub size: Vector,
|
||||
/// Rotation in radians
|
||||
pub rotation: f32,
|
||||
}
|
||||
|
||||
impl SelectionTransform {
|
||||
/// Create from engine selection mask.
|
||||
pub fn from_engine(
|
||||
engine: &mut hcie_engine_api::Engine,
|
||||
mask: &[u8],
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
) -> Self {
|
||||
let mut min_x = canvas_w;
|
||||
let mut min_y = canvas_h;
|
||||
let mut max_x = 0u32;
|
||||
let mut max_y = 0u32;
|
||||
|
||||
// Find bounding box of non-zero mask pixels
|
||||
for y in 0..canvas_h {
|
||||
for x in 0..canvas_w {
|
||||
let idx = ((y * canvas_w + x) * 4 + 3) as usize;
|
||||
if idx < mask.len() && mask[idx] > 0 {
|
||||
min_x = min_x.min(x);
|
||||
min_y = min_y.min(y);
|
||||
max_x = max_x.max(x);
|
||||
max_y = max_y.max(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if min_x > max_x || min_y > max_y {
|
||||
return Self::empty();
|
||||
}
|
||||
|
||||
let width = max_x - min_x + 1;
|
||||
let height = max_y - min_y + 1;
|
||||
|
||||
// Extract RGBA pixels within the bounding box
|
||||
let mut pixels = vec![0u8; (width * height * 4) as usize];
|
||||
let comp = engine.get_composite_pixels();
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let src_x = min_x + x;
|
||||
let src_y = min_y + y;
|
||||
let src_idx = ((src_y * canvas_w + src_x) * 4) as usize;
|
||||
let dst_idx = ((y * width + x) * 4) as usize;
|
||||
|
||||
if src_idx + 3 < comp.len() && dst_idx + 3 < pixels.len() {
|
||||
let mask_idx = ((src_y * canvas_w + src_x) * 4 + 3) as usize;
|
||||
let alpha = if mask_idx < mask.len() { mask[mask_idx] } else { 0 };
|
||||
|
||||
pixels[dst_idx] = comp[src_idx];
|
||||
pixels[dst_idx + 1] = comp[src_idx + 1];
|
||||
pixels[dst_idx + 2] = comp[src_idx + 2];
|
||||
pixels[dst_idx + 3] = alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
pos: Vector::new(min_x as f32, min_y as f32),
|
||||
size: Vector::new(width as f32, height as f32),
|
||||
rotation: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty transform with no pixel data.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
pixels: Vec::new(),
|
||||
width: 0,
|
||||
height: 0,
|
||||
pos: Vector::ZERO,
|
||||
size: Vector::ZERO,
|
||||
rotation: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this transform has valid pixel data.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pixels.is_empty() || self.width == 0 || self.height == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for interacting with a selection transform.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum TransformHandle {
|
||||
#[default]
|
||||
None,
|
||||
Move,
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
Rotate,
|
||||
}
|
||||
|
||||
impl TransformHandle {
|
||||
/// Size of handle hit area in canvas pixels (at zoom=1).
|
||||
pub const HANDLE_SIZE: f32 = 8.0;
|
||||
|
||||
/// Size of rotation handle offset from top center.
|
||||
pub const ROTATE_OFFSET: f32 = 20.0;
|
||||
|
||||
/// Get cursor icon hint for this handle.
|
||||
pub fn cursor_hint(&self) -> &'static str {
|
||||
match self {
|
||||
TransformHandle::None => "default",
|
||||
TransformHandle::Move => "move",
|
||||
TransformHandle::TopLeft | TransformHandle::BottomRight => "nwse-resize",
|
||||
TransformHandle::TopRight | TransformHandle::BottomLeft => "nesw-resize",
|
||||
TransformHandle::Top | TransformHandle::Bottom => "ns-resize",
|
||||
TransformHandle::Left | TransformHandle::Right => "ew-resize",
|
||||
TransformHandle::Rotate => "crosshair",
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user