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:
@@ -412,6 +412,36 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a border from the current selection.
|
||||||
|
///
|
||||||
|
/// **Purpose:** Extracts only the edge pixels of the selection, keeping
|
||||||
|
/// pixels that are selected but have at least one unselected neighbor.
|
||||||
|
///
|
||||||
|
/// **Arguments:**
|
||||||
|
/// - `px`: Border thickness in pixels.
|
||||||
|
pub fn selection_border(&mut self, px: u32) {
|
||||||
|
if let Some(ref mut mask) = self.document.selection_mask {
|
||||||
|
let w = self.document.canvas_width;
|
||||||
|
let h = self.document.canvas_height;
|
||||||
|
hcie_selection::border_mask(mask, w, h, px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Smooth the current selection edges.
|
||||||
|
///
|
||||||
|
/// **Purpose:** Softens jagged selection edges by applying a box blur
|
||||||
|
/// within the specified radius.
|
||||||
|
///
|
||||||
|
/// **Arguments:**
|
||||||
|
/// - `px`: Blur radius in pixels.
|
||||||
|
pub fn selection_smooth(&mut self, px: u32) {
|
||||||
|
if let Some(ref mut mask) = self.document.selection_mask {
|
||||||
|
let w = self.document.canvas_width;
|
||||||
|
let h = self.document.canvas_height;
|
||||||
|
hcie_selection::smooth_mask(mask, w, h, px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selection_invert(&mut self) {
|
pub fn selection_invert(&mut self) {
|
||||||
if let Some(ref mut mask) = self.document.selection_mask {
|
if let Some(ref mut mask) = self.document.selection_mask {
|
||||||
hcie_selection::invert_mask(mask);
|
hcie_selection::invert_mask(mask);
|
||||||
|
|||||||
@@ -1941,6 +1941,8 @@ impl HcieIcedApp {
|
|||||||
"Grow" => self.documents[self.active_doc].engine.selection_grow(v),
|
"Grow" => self.documents[self.active_doc].engine.selection_grow(v),
|
||||||
"Shrink" => self.documents[self.active_doc].engine.selection_shrink(v),
|
"Shrink" => self.documents[self.active_doc].engine.selection_shrink(v),
|
||||||
"Feather" => self.documents[self.active_doc].engine.selection_feather(v as f32),
|
"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));
|
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, 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, 3) = separator
|
||||||
// (4, 4) = Remove BG
|
// (4, 4) = Remove BG
|
||||||
// (4, 5) = Color Range...
|
// (4, 5) = Color Range...
|
||||||
@@ -2695,13 +2701,18 @@ impl HcieIcedApp {
|
|||||||
// (4, 9) = Refine Edge...
|
// (4, 9) = Refine Edge...
|
||||||
// (4, 10) = Modify (submenu)
|
// (4, 10) = Modify (submenu)
|
||||||
(4, 11) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Grow"); } // Grow
|
(4, 11) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Grow"); } // Grow
|
||||||
// (4, 12) = Similar
|
(4, 12) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Shrink"); } // Shrink
|
||||||
// (4, 13) = separator
|
(4, 13) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Feather"); } // Feather
|
||||||
// (4, 14) = Transform Selection
|
(4, 14) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Border"); } // Border
|
||||||
// (4, 15) = Quick Mask Mode
|
(4, 15) => { self.dialog_selection_value = 5.0; self.active_dialog = ActiveDialog::SelectionOp("Smooth"); } // Smooth
|
||||||
// (4, 16) = separator
|
// (4, 16) = separator
|
||||||
// (4, 17) = Load Selection
|
// (4, 17) = Similar
|
||||||
// (4, 18) = Save Selection
|
// (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
|
// Filter menu (5) — Photopea-style
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! **Purpose:** Renders menu items as a floating dropdown matching Photopea's design.
|
//! **Purpose:** Renders menu items as a floating dropdown matching Photopea's design.
|
||||||
//! Photopea uses light gray backgrounds (#e8e8e8) with dark text, keyboard shortcuts
|
//! 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).
|
//! **Design reference:** Photopea image editor (93 screenshots analyzed).
|
||||||
//! - Menu background: #e8e8e8 (light gray)
|
//! - Menu background: #e8e8e8 (light gray)
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
//! - Hover: #b3d9ff (light blue)
|
//! - Hover: #b3d9ff (light blue)
|
||||||
//! - Shortcuts: #666666 (gray, right-aligned)
|
//! - Shortcuts: #666666 (gray, right-aligned)
|
||||||
//! - Separators: 1px #cccccc lines
|
//! - Separators: 1px #cccccc lines
|
||||||
|
//! - Sub-menus: indicated by ">" arrow on the right
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::theme::ThemeColors;
|
use crate::theme::ThemeColors;
|
||||||
@@ -22,27 +23,32 @@ struct MenuItem {
|
|||||||
label: String,
|
label: String,
|
||||||
shortcut: String,
|
shortcut: String,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
has_submenu: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MenuItem {
|
impl MenuItem {
|
||||||
fn new(label: &str) -> Self {
|
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 {
|
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 {
|
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 {
|
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)]
|
#[derive(Clone)]
|
||||||
struct MenuDef {
|
struct MenuDef {
|
||||||
label: String,
|
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("New...", "Alt+Ctrl+N"),
|
||||||
MenuItem::with_shortcut("Open...", "Alt+Ctrl+O"),
|
MenuItem::with_shortcut("Open...", "Alt+Ctrl+O"),
|
||||||
MenuItem::new("Open & Place..."),
|
MenuItem::new("Open & Place..."),
|
||||||
MenuItem::new("Open More"),
|
MenuItem::submenu("Open More"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Share"),
|
MenuItem::new("Share"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::with_shortcut("Save", "Ctrl+S"),
|
MenuItem::with_shortcut("Save", "Ctrl+S"),
|
||||||
MenuItem::new("Save as PSD"),
|
MenuItem::new("Save as PSD"),
|
||||||
MenuItem::new("Save More"),
|
MenuItem::submenu("Save More"),
|
||||||
MenuItem::new("Export as"),
|
MenuItem::submenu("Export as"),
|
||||||
MenuItem::with_shortcut("Print...", "Ctrl+P"),
|
MenuItem::with_shortcut("Print...", "Ctrl+P"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Export Layers..."),
|
MenuItem::new("Export Layers..."),
|
||||||
MenuItem::new("Export Color Lookup..."),
|
MenuItem::new("Export Color Lookup..."),
|
||||||
MenuItem::new("File Info..."),
|
MenuItem::new("File Info..."),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Automate"),
|
MenuItem::submenu("Automate"),
|
||||||
MenuItem::new("Script..."),
|
MenuItem::new("Script..."),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -108,14 +114,14 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuItem::new("Puppet Warp"),
|
MenuItem::new("Puppet Warp"),
|
||||||
MenuItem::new("Perspective Warp"),
|
MenuItem::new("Perspective Warp"),
|
||||||
MenuItem::with_shortcut("Free Transform", "Alt+Ctrl+T"),
|
MenuItem::with_shortcut("Free Transform", "Alt+Ctrl+T"),
|
||||||
MenuItem::new("Transform"),
|
MenuItem::submenu("Transform"),
|
||||||
MenuItem::new("Auto-Align"),
|
MenuItem::new("Auto-Align"),
|
||||||
MenuItem::new("Auto-Blend"),
|
MenuItem::new("Auto-Blend"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Assign Profile"),
|
MenuItem::submenu("Assign Profile"),
|
||||||
MenuItem::new("Convert to Profile"),
|
MenuItem::submenu("Convert to Profile"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Define New"),
|
MenuItem::submenu("Define New"),
|
||||||
MenuItem::new("Preset Manager..."),
|
MenuItem::new("Preset Manager..."),
|
||||||
MenuItem::with_shortcut("Preferences...", "Ctrl+K"),
|
MenuItem::with_shortcut("Preferences...", "Ctrl+K"),
|
||||||
MenuItem::new("Local Storage..."),
|
MenuItem::new("Local Storage..."),
|
||||||
@@ -125,8 +131,8 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuDef {
|
MenuDef {
|
||||||
label: "Image".to_string(),
|
label: "Image".to_string(),
|
||||||
items: vec![
|
items: vec![
|
||||||
MenuItem::new("Mode"),
|
MenuItem::submenu("Mode"),
|
||||||
MenuItem::new("Adjustments"),
|
MenuItem::submenu("Adjustments"),
|
||||||
MenuItem::new("Auto Tone"),
|
MenuItem::new("Auto Tone"),
|
||||||
MenuItem::new("Auto Contrast"),
|
MenuItem::new("Auto Contrast"),
|
||||||
MenuItem::new("Auto Color"),
|
MenuItem::new("Auto Color"),
|
||||||
@@ -137,9 +143,9 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::with_shortcut("Canvas Size...", "Alt+Ctrl+C"),
|
MenuItem::with_shortcut("Canvas Size...", "Alt+Ctrl+C"),
|
||||||
MenuItem::with_shortcut("Image Size...", "Alt+Ctrl+I"),
|
MenuItem::with_shortcut("Image Size...", "Alt+Ctrl+I"),
|
||||||
MenuItem::new("Transform"),
|
MenuItem::submenu("Transform"),
|
||||||
MenuItem::new("Crop"),
|
MenuItem::new("Crop"),
|
||||||
MenuItem::new("Trim..."),
|
MenuItem::with_shortcut("Trim...", "Ctrl+."),
|
||||||
MenuItem::new("Reveal All"),
|
MenuItem::new("Reveal All"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Duplicate"),
|
MenuItem::new("Duplicate"),
|
||||||
@@ -152,29 +158,29 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuDef {
|
MenuDef {
|
||||||
label: "Layer".to_string(),
|
label: "Layer".to_string(),
|
||||||
items: vec![
|
items: vec![
|
||||||
MenuItem::new("New"),
|
MenuItem::submenu("New"),
|
||||||
MenuItem::new("Duplicate Layer"),
|
MenuItem::new("Duplicate Layer"),
|
||||||
MenuItem::new("Duplicate Into ..."),
|
MenuItem::new("Duplicate Into ..."),
|
||||||
MenuItem::new("Delete"),
|
MenuItem::new("Delete"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Text"),
|
MenuItem::submenu("Text"),
|
||||||
MenuItem::new("Layer Style"),
|
MenuItem::submenu("Layer Style"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("New Fill Layer"),
|
MenuItem::submenu("New Fill Layer"),
|
||||||
MenuItem::new("New Adjustment Layer"),
|
MenuItem::submenu("New Adjustment Layer"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Raster Mask"),
|
MenuItem::submenu("Raster Mask"),
|
||||||
MenuItem::new("Vector Mask"),
|
MenuItem::submenu("Vector Mask"),
|
||||||
MenuItem::new("Clipping Mask"),
|
MenuItem::disabled("Clipping Mask"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Smart Object"),
|
MenuItem::submenu("Smart Object"),
|
||||||
MenuItem::new("Rasterize"),
|
MenuItem::new("Rasterize"),
|
||||||
MenuItem::new("Rasterize Layer Style"),
|
MenuItem::new("Rasterize Layer Style"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::with_shortcut("Group Layers", "Ctrl+G"),
|
MenuItem::with_shortcut("Group Layers", "Ctrl+G"),
|
||||||
MenuItem::new("Arrange"),
|
MenuItem::submenu("Arrange"),
|
||||||
MenuItem::new("Combine Shapes"),
|
MenuItem::submenu("Combine Shapes"),
|
||||||
MenuItem::new("Animation"),
|
MenuItem::submenu("Animation"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::with_shortcut("Merge Down", "Ctrl+E"),
|
MenuItem::with_shortcut("Merge Down", "Ctrl+E"),
|
||||||
MenuItem::new("Flatten Image"),
|
MenuItem::new("Flatten Image"),
|
||||||
@@ -195,12 +201,17 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuItem::new("Subject"),
|
MenuItem::new("Subject"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Refine Edge..."),
|
MenuItem::new("Refine Edge..."),
|
||||||
MenuItem::new("Modify"),
|
MenuItem::submenu("Modify"),
|
||||||
MenuItem::new("Grow"),
|
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::new("Similar"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Transform Selection"),
|
MenuItem::new("Transform Selection"),
|
||||||
MenuItem::new("Quick Mask Mode"),
|
MenuItem::with_shortcut("Quick Mask Mode", "Q"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Load Selection"),
|
MenuItem::new("Load Selection"),
|
||||||
MenuItem::new("Save Selection"),
|
MenuItem::new("Save Selection"),
|
||||||
@@ -218,34 +229,47 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuItem::new("Liquify..."),
|
MenuItem::new("Liquify..."),
|
||||||
MenuItem::new("Vanishing Point..."),
|
MenuItem::new("Vanishing Point..."),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("3D"),
|
MenuItem::submenu("3D"),
|
||||||
MenuItem::new("Blur"),
|
MenuItem::submenu("Blur"),
|
||||||
MenuItem::new("Blur Gallery"),
|
MenuItem::submenu("Blur Gallery"),
|
||||||
MenuItem::new("Distort"),
|
MenuItem::submenu("Distort"),
|
||||||
MenuItem::new("Noise"),
|
MenuItem::submenu("Noise"),
|
||||||
MenuItem::new("Pixelate"),
|
MenuItem::submenu("Pixelate"),
|
||||||
MenuItem::new("Render"),
|
MenuItem::submenu("Render"),
|
||||||
MenuItem::new("Sharpen"),
|
MenuItem::submenu("Sharpen"),
|
||||||
MenuItem::new("Stylize"),
|
MenuItem::submenu("Stylize"),
|
||||||
MenuItem::new("Other"),
|
MenuItem::submenu("Other"),
|
||||||
MenuItem::new("Fourier"),
|
MenuItem::submenu("Fourier"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// ── View menu ──
|
// ── View menu (Photopea-style) ──
|
||||||
MenuDef {
|
MenuDef {
|
||||||
label: "View".to_string(),
|
label: "View".to_string(),
|
||||||
items: vec![
|
items: vec![
|
||||||
MenuItem::new("Zoom In"),
|
MenuItem::with_shortcut("Zoom In", "Ctrl++"),
|
||||||
MenuItem::new("Zoom Out"),
|
MenuItem::with_shortcut("Zoom Out", "Ctrl+-"),
|
||||||
MenuItem::new("Fit on Screen"),
|
MenuItem::with_shortcut("Fit The Area", "Ctrl+0"),
|
||||||
MenuItem::new("Actual Pixels"),
|
MenuItem::with_shortcut("Pixel to Pixel", "Ctrl+1"),
|
||||||
|
MenuItem::new("Pattern Preview"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Rulers"),
|
MenuItem::submenu("Mode"),
|
||||||
MenuItem::new("Grid"),
|
MenuItem::with_shortcut("Extras", "Ctrl+H"),
|
||||||
MenuItem::new("Guides"),
|
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 {
|
MenuDef {
|
||||||
label: "Window".to_string(),
|
label: "Window".to_string(),
|
||||||
items: vec![
|
items: vec![
|
||||||
@@ -261,10 +285,12 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
|||||||
MenuItem::new("Layer Details"),
|
MenuItem::new("Layer Details"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// ── More menu ──
|
// ── More menu (Photopea-style) ──
|
||||||
MenuDef {
|
MenuDef {
|
||||||
label: "More".to_string(),
|
label: "More".to_string(),
|
||||||
items: vec![
|
items: vec![
|
||||||
|
MenuItem::submenu("More"),
|
||||||
|
MenuItem::separator(),
|
||||||
MenuItem::new("Plugins"),
|
MenuItem::new("Plugins"),
|
||||||
MenuItem::new("Actions"),
|
MenuItem::new("Actions"),
|
||||||
MenuItem::new("Adjustments"),
|
MenuItem::new("Adjustments"),
|
||||||
@@ -381,16 +407,19 @@ pub fn dropdown_overlay(
|
|||||||
} else {
|
} else {
|
||||||
let lbl = item.label.clone();
|
let lbl = item.label.clone();
|
||||||
let sht = item.shortcut.clone();
|
let sht = item.shortcut.clone();
|
||||||
let label_row: Element<'static, Message> = if sht.is_empty() {
|
let has_sub = item.has_submenu;
|
||||||
row![text(lbl).size(11)].into()
|
|
||||||
} else {
|
// Photopea-style: label left, shortcut right in gray, submenu arrow ">" if needed
|
||||||
// Photopea-style: label left, shortcut right in gray
|
let label_row: Element<'static, Message> = {
|
||||||
row![
|
let mut row_items = vec![];
|
||||||
text(lbl).size(11).width(Length::Fill),
|
row_items.push(text(lbl).size(11).width(Length::Fill).into());
|
||||||
text(sht).size(10),
|
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());
|
||||||
.spacing(16)
|
}
|
||||||
.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;
|
let enabled = item.enabled;
|
||||||
@@ -428,7 +457,8 @@ pub fn dropdown_overlay(
|
|||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.padding([4, 16])
|
.padding([4, 16])
|
||||||
.style(move |_theme| iced::widget::container::Style {
|
.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()
|
..Default::default()
|
||||||
});
|
});
|
||||||
items.push(disabled_item.into());
|
items.push(disabled_item.into());
|
||||||
|
|||||||
@@ -282,4 +282,87 @@ pub fn lasso_fill_mask(mask: &mut [u8], w: u32, h: u32, points: &[(u32, u32)]) {
|
|||||||
k += 2;
|
k += 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a border from a selection mask by keeping only edge pixels.
|
||||||
|
///
|
||||||
|
/// **Purpose:** Extracts the outline/border of a selection by keeping pixels
|
||||||
|
/// that are selected but have at least one unselected neighbor.
|
||||||
|
///
|
||||||
|
/// **Arguments:**
|
||||||
|
/// - `mask`: The selection mask to process (modified in place).
|
||||||
|
/// - `w`, `h`: Canvas dimensions.
|
||||||
|
/// - `thickness`: Border thickness in pixels.
|
||||||
|
pub fn border_mask(mask: &mut [u8], w: u32, h: u32, thickness: u32) {
|
||||||
|
if thickness == 0 { return; }
|
||||||
|
let original = mask.to_vec();
|
||||||
|
|
||||||
|
// First, create the outer border by eroding
|
||||||
|
let mut eroded = original.clone();
|
||||||
|
for _ in 0..thickness {
|
||||||
|
let old = eroded.clone();
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let i = (y * w + x) as usize;
|
||||||
|
if old[i] == 0 { continue; }
|
||||||
|
let neighbors = [
|
||||||
|
(x.wrapping_sub(1), y), (x + 1, y),
|
||||||
|
(x, y.wrapping_sub(1)), (x, y + 1),
|
||||||
|
];
|
||||||
|
let mut has_zero = false;
|
||||||
|
for (nx, ny) in neighbors {
|
||||||
|
if nx >= w || ny >= h || old[(ny * w + nx) as usize] == 0 {
|
||||||
|
has_zero = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_zero {
|
||||||
|
eroded[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border = original - eroded (pixels in original but not in eroded)
|
||||||
|
for i in 0..mask.len() {
|
||||||
|
if original[i] > 0 && eroded[i] == 0 {
|
||||||
|
mask[i] = original[i];
|
||||||
|
} else {
|
||||||
|
mask[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Smooth a selection mask using a box blur.
|
||||||
|
///
|
||||||
|
/// **Purpose:** Softens jagged edges of a selection by applying a box blur
|
||||||
|
/// within the specified radius, creating smoother transitions.
|
||||||
|
///
|
||||||
|
/// **Arguments:**
|
||||||
|
/// - `mask`: The selection mask to process (modified in place).
|
||||||
|
/// - `w`, `h`: Canvas dimensions.
|
||||||
|
/// - `radius`: Blur radius in pixels.
|
||||||
|
pub fn smooth_mask(mask: &mut [u8], w: u32, h: u32, radius: u32) {
|
||||||
|
if radius == 0 { return; }
|
||||||
|
let r = radius as i32;
|
||||||
|
let size = (w * h) as usize;
|
||||||
|
let mut result = vec![0u8; size];
|
||||||
|
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let mut sum = 0u32;
|
||||||
|
let mut count = 0u32;
|
||||||
|
for dy in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as u32;
|
||||||
|
let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as u32;
|
||||||
|
sum += mask[(ny * w + nx) as usize] as u32;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[(y * w + x) as usize] = (sum / count) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mask.copy_from_slice(&result);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user