feat(iced): implement selection menu operations
- Add Inverse selection (Shift+Ctrl+I) with engine.invert_selection() - Add Shrink, Feather, Border, Smooth menu items with parameter dialogs - Implement selection_border() and selection_smooth() in engine API - Implement border_mask() and smooth_mask() in hcie-selection crate - Update SelectionOpApply handler for all selection operations
This commit is contained in:
@@ -1941,6 +1941,8 @@ impl HcieIcedApp {
|
||||
"Grow" => self.documents[self.active_doc].engine.selection_grow(v),
|
||||
"Shrink" => self.documents[self.active_doc].engine.selection_shrink(v),
|
||||
"Feather" => self.documents[self.active_doc].engine.selection_feather(v as f32),
|
||||
"Border" => self.documents[self.active_doc].engine.selection_border(v),
|
||||
"Smooth" => self.documents[self.active_doc].engine.selection_smooth(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -2685,7 +2687,11 @@ impl HcieIcedApp {
|
||||
self.documents[self.active_doc].selection_rect = Some((0.0, 0.0, w, h));
|
||||
}
|
||||
(4, 1) => { self.documents[self.active_doc].selection_rect = None; } // Deselect
|
||||
// (4, 2) = Inverse
|
||||
(4, 2) => { // Inverse
|
||||
self.documents[self.active_doc].engine.selection_invert();
|
||||
self.refresh_composite_if_needed();
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
// (4, 3) = separator
|
||||
// (4, 4) = Remove BG
|
||||
// (4, 5) = Color Range...
|
||||
@@ -2695,13 +2701,18 @@ impl HcieIcedApp {
|
||||
// (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, 12) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Shrink"); } // Shrink
|
||||
(4, 13) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Feather"); } // Feather
|
||||
(4, 14) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Border"); } // Border
|
||||
(4, 15) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Smooth"); } // Smooth
|
||||
// (4, 16) = separator
|
||||
// (4, 17) = Load Selection
|
||||
// (4, 18) = Save Selection
|
||||
// (4, 17) = Similar
|
||||
// (4, 18) = separator
|
||||
// (4, 19) = Transform Selection
|
||||
// (4, 20) = Quick Mask Mode
|
||||
// (4, 21) = separator
|
||||
// (4, 22) = Load Selection
|
||||
// (4, 23) = Save Selection
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// Filter menu (5) — Photopea-style
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! **Purpose:** Renders menu items as a floating dropdown matching Photopea's design.
|
||||
//! Photopea uses light gray backgrounds (#e8e8e8) with dark text, keyboard shortcuts
|
||||
//! on the right, separator lines, and blue hover highlights.
|
||||
//! on the right, separator lines, and blue hover highlights. Items with ">" have sub-menus.
|
||||
//!
|
||||
//! **Design reference:** Photopea image editor (93 screenshots analyzed).
|
||||
//! - Menu background: #e8e8e8 (light gray)
|
||||
@@ -10,6 +10,7 @@
|
||||
//! - Hover: #b3d9ff (light blue)
|
||||
//! - Shortcuts: #666666 (gray, right-aligned)
|
||||
//! - Separators: 1px #cccccc lines
|
||||
//! - Sub-menus: indicated by ">" arrow on the right
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::theme::ThemeColors;
|
||||
@@ -22,27 +23,32 @@ struct MenuItem {
|
||||
label: String,
|
||||
shortcut: String,
|
||||
enabled: bool,
|
||||
has_submenu: bool,
|
||||
}
|
||||
|
||||
impl MenuItem {
|
||||
fn new(label: &str) -> Self {
|
||||
Self { label: label.to_string(), shortcut: String::new(), enabled: true }
|
||||
Self { label: label.to_string(), shortcut: String::new(), enabled: true, has_submenu: false }
|
||||
}
|
||||
|
||||
fn with_shortcut(label: &str, shortcut: &str) -> Self {
|
||||
Self { label: label.to_string(), shortcut: shortcut.to_string(), enabled: true }
|
||||
Self { label: label.to_string(), shortcut: shortcut.to_string(), enabled: true, has_submenu: false }
|
||||
}
|
||||
|
||||
fn submenu(label: &str) -> Self {
|
||||
Self { label: label.to_string(), shortcut: String::new(), enabled: true, has_submenu: true }
|
||||
}
|
||||
|
||||
fn separator() -> Self {
|
||||
Self { label: "─".to_string(), shortcut: String::new(), enabled: false }
|
||||
Self { label: "─".to_string(), shortcut: String::new(), enabled: false, has_submenu: false }
|
||||
}
|
||||
|
||||
fn disabled(label: &str) -> Self {
|
||||
Self { label: label.to_string(), shortcut: String::new(), enabled: false }
|
||||
Self { label: label.to_string(), shortcut: String::new(), enabled: false, has_submenu: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// All menu definitions matching Photopea's menu structure.
|
||||
/// All menu definitions matching Photopea's menu structure exactly.
|
||||
#[derive(Clone)]
|
||||
struct MenuDef {
|
||||
label: String,
|
||||
@@ -55,21 +61,21 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::with_shortcut("New...", "Alt+Ctrl+N"),
|
||||
MenuItem::with_shortcut("Open...", "Alt+Ctrl+O"),
|
||||
MenuItem::new("Open & Place..."),
|
||||
MenuItem::new("Open More"),
|
||||
MenuItem::submenu("Open More"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Share"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::with_shortcut("Save", "Ctrl+S"),
|
||||
MenuItem::new("Save as PSD"),
|
||||
MenuItem::new("Save More"),
|
||||
MenuItem::new("Export as"),
|
||||
MenuItem::submenu("Save More"),
|
||||
MenuItem::submenu("Export as"),
|
||||
MenuItem::with_shortcut("Print...", "Ctrl+P"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Export Layers..."),
|
||||
MenuItem::new("Export Color Lookup..."),
|
||||
MenuItem::new("File Info..."),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Automate"),
|
||||
MenuItem::submenu("Automate"),
|
||||
MenuItem::new("Script..."),
|
||||
];
|
||||
|
||||
@@ -108,14 +114,14 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::new("Puppet Warp"),
|
||||
MenuItem::new("Perspective Warp"),
|
||||
MenuItem::with_shortcut("Free Transform", "Alt+Ctrl+T"),
|
||||
MenuItem::new("Transform"),
|
||||
MenuItem::submenu("Transform"),
|
||||
MenuItem::new("Auto-Align"),
|
||||
MenuItem::new("Auto-Blend"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Assign Profile"),
|
||||
MenuItem::new("Convert to Profile"),
|
||||
MenuItem::submenu("Assign Profile"),
|
||||
MenuItem::submenu("Convert to Profile"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Define New"),
|
||||
MenuItem::submenu("Define New"),
|
||||
MenuItem::new("Preset Manager..."),
|
||||
MenuItem::with_shortcut("Preferences...", "Ctrl+K"),
|
||||
MenuItem::new("Local Storage..."),
|
||||
@@ -125,8 +131,8 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuDef {
|
||||
label: "Image".to_string(),
|
||||
items: vec![
|
||||
MenuItem::new("Mode"),
|
||||
MenuItem::new("Adjustments"),
|
||||
MenuItem::submenu("Mode"),
|
||||
MenuItem::submenu("Adjustments"),
|
||||
MenuItem::new("Auto Tone"),
|
||||
MenuItem::new("Auto Contrast"),
|
||||
MenuItem::new("Auto Color"),
|
||||
@@ -137,9 +143,9 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::separator(),
|
||||
MenuItem::with_shortcut("Canvas Size...", "Alt+Ctrl+C"),
|
||||
MenuItem::with_shortcut("Image Size...", "Alt+Ctrl+I"),
|
||||
MenuItem::new("Transform"),
|
||||
MenuItem::submenu("Transform"),
|
||||
MenuItem::new("Crop"),
|
||||
MenuItem::new("Trim..."),
|
||||
MenuItem::with_shortcut("Trim...", "Ctrl+."),
|
||||
MenuItem::new("Reveal All"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Duplicate"),
|
||||
@@ -152,29 +158,29 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuDef {
|
||||
label: "Layer".to_string(),
|
||||
items: vec![
|
||||
MenuItem::new("New"),
|
||||
MenuItem::submenu("New"),
|
||||
MenuItem::new("Duplicate Layer"),
|
||||
MenuItem::new("Duplicate Into ..."),
|
||||
MenuItem::new("Delete"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Text"),
|
||||
MenuItem::new("Layer Style"),
|
||||
MenuItem::submenu("Text"),
|
||||
MenuItem::submenu("Layer Style"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("New Fill Layer"),
|
||||
MenuItem::new("New Adjustment Layer"),
|
||||
MenuItem::submenu("New Fill Layer"),
|
||||
MenuItem::submenu("New Adjustment Layer"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Raster Mask"),
|
||||
MenuItem::new("Vector Mask"),
|
||||
MenuItem::new("Clipping Mask"),
|
||||
MenuItem::submenu("Raster Mask"),
|
||||
MenuItem::submenu("Vector Mask"),
|
||||
MenuItem::disabled("Clipping Mask"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Smart Object"),
|
||||
MenuItem::submenu("Smart Object"),
|
||||
MenuItem::new("Rasterize"),
|
||||
MenuItem::new("Rasterize Layer Style"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::with_shortcut("Group Layers", "Ctrl+G"),
|
||||
MenuItem::new("Arrange"),
|
||||
MenuItem::new("Combine Shapes"),
|
||||
MenuItem::new("Animation"),
|
||||
MenuItem::submenu("Arrange"),
|
||||
MenuItem::submenu("Combine Shapes"),
|
||||
MenuItem::submenu("Animation"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::with_shortcut("Merge Down", "Ctrl+E"),
|
||||
MenuItem::new("Flatten Image"),
|
||||
@@ -195,12 +201,17 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::new("Subject"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Refine Edge..."),
|
||||
MenuItem::new("Modify"),
|
||||
MenuItem::new("Grow"),
|
||||
MenuItem::submenu("Modify"),
|
||||
MenuItem::with_shortcut("Grow...", "Alt+Ctrl+G"),
|
||||
MenuItem::with_shortcut("Shrink...", "Alt+Ctrl+S"),
|
||||
MenuItem::with_shortcut("Feather...", "Alt+Ctrl+F"),
|
||||
MenuItem::new("Border..."),
|
||||
MenuItem::new("Smooth..."),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Similar"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Transform Selection"),
|
||||
MenuItem::new("Quick Mask Mode"),
|
||||
MenuItem::with_shortcut("Quick Mask Mode", "Q"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Load Selection"),
|
||||
MenuItem::new("Save Selection"),
|
||||
@@ -218,34 +229,47 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::new("Liquify..."),
|
||||
MenuItem::new("Vanishing Point..."),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("3D"),
|
||||
MenuItem::new("Blur"),
|
||||
MenuItem::new("Blur Gallery"),
|
||||
MenuItem::new("Distort"),
|
||||
MenuItem::new("Noise"),
|
||||
MenuItem::new("Pixelate"),
|
||||
MenuItem::new("Render"),
|
||||
MenuItem::new("Sharpen"),
|
||||
MenuItem::new("Stylize"),
|
||||
MenuItem::new("Other"),
|
||||
MenuItem::new("Fourier"),
|
||||
MenuItem::submenu("3D"),
|
||||
MenuItem::submenu("Blur"),
|
||||
MenuItem::submenu("Blur Gallery"),
|
||||
MenuItem::submenu("Distort"),
|
||||
MenuItem::submenu("Noise"),
|
||||
MenuItem::submenu("Pixelate"),
|
||||
MenuItem::submenu("Render"),
|
||||
MenuItem::submenu("Sharpen"),
|
||||
MenuItem::submenu("Stylize"),
|
||||
MenuItem::submenu("Other"),
|
||||
MenuItem::submenu("Fourier"),
|
||||
],
|
||||
},
|
||||
// ── View menu ──
|
||||
// ── View menu (Photopea-style) ──
|
||||
MenuDef {
|
||||
label: "View".to_string(),
|
||||
items: vec![
|
||||
MenuItem::new("Zoom In"),
|
||||
MenuItem::new("Zoom Out"),
|
||||
MenuItem::new("Fit on Screen"),
|
||||
MenuItem::new("Actual Pixels"),
|
||||
MenuItem::with_shortcut("Zoom In", "Ctrl++"),
|
||||
MenuItem::with_shortcut("Zoom Out", "Ctrl+-"),
|
||||
MenuItem::with_shortcut("Fit The Area", "Ctrl+0"),
|
||||
MenuItem::with_shortcut("Pixel to Pixel", "Ctrl+1"),
|
||||
MenuItem::new("Pattern Preview"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Rulers"),
|
||||
MenuItem::new("Grid"),
|
||||
MenuItem::new("Guides"),
|
||||
MenuItem::submenu("Mode"),
|
||||
MenuItem::with_shortcut("Extras", "Ctrl+H"),
|
||||
MenuItem::submenu("Show"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::with_shortcut("Rulers", "Ctrl+R"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Snap"),
|
||||
MenuItem::submenu("Snap To"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Lock Guides"),
|
||||
MenuItem::new("Clear Guides"),
|
||||
MenuItem::new("Add Guides..."),
|
||||
MenuItem::new("Guides from Layer"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Clear Slices"),
|
||||
],
|
||||
},
|
||||
// ── Window menu ──
|
||||
// ── Window menu (Photopea-style) ──
|
||||
MenuDef {
|
||||
label: "Window".to_string(),
|
||||
items: vec![
|
||||
@@ -261,10 +285,12 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::new("Layer Details"),
|
||||
],
|
||||
},
|
||||
// ── More menu ──
|
||||
// ── More menu (Photopea-style) ──
|
||||
MenuDef {
|
||||
label: "More".to_string(),
|
||||
items: vec![
|
||||
MenuItem::submenu("More"),
|
||||
MenuItem::separator(),
|
||||
MenuItem::new("Plugins"),
|
||||
MenuItem::new("Actions"),
|
||||
MenuItem::new("Adjustments"),
|
||||
@@ -381,16 +407,19 @@ pub fn dropdown_overlay(
|
||||
} else {
|
||||
let lbl = item.label.clone();
|
||||
let sht = item.shortcut.clone();
|
||||
let label_row: Element<'static, Message> = if sht.is_empty() {
|
||||
row![text(lbl).size(11)].into()
|
||||
} else {
|
||||
// Photopea-style: label left, shortcut right in gray
|
||||
row![
|
||||
text(lbl).size(11).width(Length::Fill),
|
||||
text(sht).size(10),
|
||||
]
|
||||
.spacing(16)
|
||||
.into()
|
||||
let has_sub = item.has_submenu;
|
||||
|
||||
// Photopea-style: label left, shortcut right in gray, submenu arrow ">" if needed
|
||||
let label_row: Element<'static, Message> = {
|
||||
let mut row_items = vec![];
|
||||
row_items.push(text(lbl).size(11).width(Length::Fill).into());
|
||||
if !sht.is_empty() {
|
||||
row_items.push(text(sht).size(10).color(iced::Color::from_rgba(0.4, 0.4, 0.4, 1.0)).into());
|
||||
}
|
||||
if has_sub {
|
||||
row_items.push(text(">").size(10).color(iced::Color::from_rgba(0.4, 0.4, 0.4, 1.0)).into());
|
||||
}
|
||||
row(row_items).spacing(16).into()
|
||||
};
|
||||
|
||||
let enabled = item.enabled;
|
||||
@@ -428,7 +457,8 @@ pub fn dropdown_overlay(
|
||||
.width(Length::Fill)
|
||||
.padding([4, 16])
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
background: Some(iced::Background::Color(c.menu_bg)),
|
||||
background: Some(iced::Background::Color(colors.menu_bg)),
|
||||
text_color: Some(iced::Color::from_rgba(0.6, 0.6, 0.6, 1.0)),
|
||||
..Default::default()
|
||||
});
|
||||
items.push(disabled_item.into());
|
||||
|
||||
Reference in New Issue
Block a user