209 lines
8.0 KiB
Rust
209 lines
8.0 KiB
Rust
//! Right-click context menu — gpicview-inspired, with SVG icons.
|
|
//!
|
|
//! Items (in order):
|
|
//! Open With… → opens with system default app (xdg-open)
|
|
//! ─────────────
|
|
//! Copy Path → clipboard: file path
|
|
//! Copy Image → clipboard: image pixels
|
|
//! ─────────────
|
|
//! Rotate 90° CW
|
|
//! Rotate 90° CCW
|
|
//! ─────────────
|
|
//! Set as Wallpaper
|
|
//! Move to Trash → deletes file, loads next
|
|
//! ─────────────
|
|
//! Properties → shows info modal
|
|
|
|
use iced::widget::{button, column, container, text};
|
|
use iced::{Element, Length};
|
|
|
|
use super::icons::Icon;
|
|
use super::theme;
|
|
|
|
const MENU_WIDTH: f32 = 200.0;
|
|
const ITEM_HEIGHT: f32 = 28.0;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ContextMenuItem {
|
|
OpenWith,
|
|
CopyPath,
|
|
CopyImage,
|
|
CopyToPictures,
|
|
RotateCw,
|
|
RotateCcw,
|
|
SetAsWallpaper,
|
|
MoveToTrash,
|
|
DeletePermanently,
|
|
Properties,
|
|
About,
|
|
ExportToVideo,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum ContextMenuMessage {
|
|
Show { x: f32, y: f32 },
|
|
Selected(ContextMenuItem),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ContextMenu {
|
|
pub visible: bool,
|
|
pub x: f32,
|
|
pub y: f32,
|
|
}
|
|
|
|
impl ContextMenu {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn show(&mut self, x: f32, y: f32, window_w: f32, window_h: f32) {
|
|
// Clamp so the menu stays on screen.
|
|
let approx_menu_h = 14.0 * ITEM_HEIGHT + 16.0; // 12 items + padding
|
|
self.x = x.min(window_w - MENU_WIDTH - 8.0).max(8.0);
|
|
self.y = y.min(window_h - approx_menu_h - 8.0).max(8.0);
|
|
self.visible = true;
|
|
}
|
|
|
|
pub fn dismiss(&mut self) {
|
|
self.visible = false;
|
|
}
|
|
|
|
/// Render the menu content (without positioning). The app layer handles
|
|
/// the overlay backdrop.
|
|
pub fn content_view(&self) -> Element<'static, ContextMenuMessage> {
|
|
let icon_color = theme::text_secondary_hex();
|
|
let danger_color = theme::danger_hex();
|
|
|
|
let items: Vec<(Option<Icon>, &'static str, ContextMenuItem, bool)> = vec![
|
|
(Some(Icon::OpenWith), "Open With…", ContextMenuItem::OpenWith, false),
|
|
(None, "", ContextMenuItem::OpenWith, true), // separator
|
|
(Some(Icon::Copy), "Copy Path", ContextMenuItem::CopyPath, false),
|
|
(Some(Icon::Copy), "Copy Image", ContextMenuItem::CopyImage, false),
|
|
(Some(Icon::FolderOpen), "Copy to Pictures", ContextMenuItem::CopyToPictures, false),
|
|
(None, "", ContextMenuItem::CopyPath, true), // separator
|
|
(Some(Icon::RotateCw), "Rotate 90° CW", ContextMenuItem::RotateCw, false),
|
|
(Some(Icon::RotateCcw), "Rotate 90° CCW", ContextMenuItem::RotateCcw, false),
|
|
(None, "", ContextMenuItem::RotateCcw, true), // separator
|
|
(Some(Icon::Wallpaper), "Set as Wallpaper", ContextMenuItem::SetAsWallpaper, false),
|
|
(Some(Icon::Trash), "Move to Trash", ContextMenuItem::MoveToTrash, true), // danger
|
|
(Some(Icon::Trash), "Delete Permanently", ContextMenuItem::DeletePermanently, true), // danger
|
|
(None, "", ContextMenuItem::DeletePermanently, true), // separator
|
|
(Some(Icon::Properties), "Properties", ContextMenuItem::Properties, false),
|
|
(Some(Icon::Info), "About marten", ContextMenuItem::About, false),
|
|
(None, "", ContextMenuItem::About, true),
|
|
(Some(Icon::Film), "Export folder as video…", ContextMenuItem::ExportToVideo, false),
|
|
];
|
|
|
|
let children: Vec<Element<'_, ContextMenuMessage>> = items
|
|
.iter()
|
|
.map(|(icon, label, msg, is_separator_or_danger)| {
|
|
if *is_separator_or_danger && label.is_empty() {
|
|
// Separator line
|
|
container(text("").height(Length::Fixed(1.0)))
|
|
.width(Length::Fill)
|
|
.height(Length::Fixed(1.0))
|
|
.style(|_| container::Style {
|
|
background: Some(theme::BG_CHROME_BORDER.into()),
|
|
..Default::default()
|
|
})
|
|
.into()
|
|
} else {
|
|
let danger = matches!(
|
|
*msg,
|
|
ContextMenuItem::MoveToTrash | ContextMenuItem::DeletePermanently
|
|
);
|
|
let color_hex = if danger {
|
|
danger_color.clone()
|
|
} else {
|
|
icon_color.clone()
|
|
};
|
|
let label_color = if danger { theme::DANGER } else { theme::TEXT_PRIMARY };
|
|
|
|
let icon_el: Element<'static, ContextMenuMessage> = match icon {
|
|
Some(ic) => ic.widget(14.0, &color_hex).into(),
|
|
None => text("").width(Length::Fixed(14.0)).into(),
|
|
};
|
|
|
|
let row = iced::widget::row![
|
|
container(icon_el).width(Length::Fixed(20.0)),
|
|
text(*label).color(label_color).size(12),
|
|
]
|
|
.align_y(iced::Alignment::Center)
|
|
.spacing(6.0);
|
|
|
|
button(row)
|
|
.on_press(ContextMenuMessage::Selected(*msg))
|
|
.width(Length::Fill)
|
|
.height(Length::Fixed(ITEM_HEIGHT))
|
|
.padding(iced::Padding {
|
|
top: 0.0,
|
|
right: 12.0,
|
|
bottom: 0.0,
|
|
left: 10.0,
|
|
})
|
|
.style(move |_theme, status| {
|
|
let bg = match status {
|
|
button::Status::Hovered if danger => Some(theme::DANGER_DIM),
|
|
button::Status::Hovered => Some(theme::BG_CHROME_HOVER),
|
|
button::Status::Pressed if danger => Some(theme::DANGER_DIM),
|
|
button::Status::Pressed => Some(theme::ACCENT_DIM),
|
|
_ => None,
|
|
};
|
|
button::Style {
|
|
background: bg.map(iced::Background::Color),
|
|
..Default::default()
|
|
}
|
|
})
|
|
.into()
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
column(children)
|
|
.padding(iced::Padding {
|
|
top: 4.0,
|
|
right: 0.0,
|
|
bottom: 4.0,
|
|
left: 0.0,
|
|
})
|
|
.into()
|
|
}
|
|
|
|
/// Render the menu as a positioned overlay element.
|
|
/// This is meant to be placed inside a stack/overlay by the app.
|
|
pub fn overlay_view(&self) -> Element<'_, ContextMenuMessage> {
|
|
let menu = container(self.content_view())
|
|
.width(Length::Fixed(MENU_WIDTH))
|
|
.style(|_| container::Style {
|
|
background: Some(theme::BG_CHROME.into()),
|
|
border: iced::Border {
|
|
color: theme::BG_CHROME_BORDER,
|
|
width: 1.0,
|
|
radius: 6.0.into(),
|
|
},
|
|
shadow: iced::Shadow {
|
|
color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.6),
|
|
offset: iced::Vector::new(0.0, 4.0),
|
|
blur_radius: 16.0,
|
|
},
|
|
..Default::default()
|
|
});
|
|
|
|
container(menu)
|
|
.width(Length::Fill)
|
|
.height(Length::Fill)
|
|
.padding(iced::Padding {
|
|
top: self.y,
|
|
right: 0.0,
|
|
bottom: 0.0,
|
|
left: self.x,
|
|
})
|
|
.style(|_| container::Style {
|
|
background: Some(iced::Color::TRANSPARENT.into()),
|
|
..Default::default()
|
|
})
|
|
.into()
|
|
}
|
|
}
|