use std::io;
use windows::Win32::Foundation::{LPARAM, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
AppendMenuW, CreatePopupMenu, DestroyMenu, HMENU, MF_GRAYED, MF_SEPARATOR, MF_STRING,
PostMessageW, SetForegroundWindow, TPM_NONOTIFY, TPM_RETURNCMD, TPM_RIGHTBUTTON,
TrackPopupMenu, WM_NULL,
};
use windows::core::PCWSTR;
use super::config::{allocation_error, windows_error};
use super::native::NativeWindow;
pub const MAX_POPUP_MENU_ITEMS: usize = 32;
pub const MAX_POPUP_MENU_LABEL_UNITS: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PopupMenuItem {
label: Option<Vec<u16>>,
enabled: bool,
}
impl PopupMenuItem {
pub fn action(label: &str, enabled: bool) -> io::Result<Self> {
Ok(Self {
label: Some(encode_label(label)?),
enabled,
})
}
#[must_use]
pub const fn separator() -> Self {
Self {
label: None,
enabled: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PopupMenu {
items: Vec<PopupMenuItem>,
}
impl PopupMenu {
pub fn new(items: Vec<PopupMenuItem>) -> io::Result<Self> {
if items.len() > MAX_POPUP_MENU_ITEMS {
return Err(invalid("popup menu has too many items"));
}
if !items.iter().any(|item| item.label.is_some()) {
return Err(invalid("popup menu needs a selectable item"));
}
Ok(Self { items })
}
#[must_use]
pub fn len(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub(super) fn build(&self, first_id: usize) -> io::Result<OwnedMenu> {
let menu = OwnedMenu(unsafe { CreatePopupMenu() }.map_err(windows_error)?);
for (index, item) in self.items.iter().enumerate() {
unsafe {
match &item.label {
Some(label) => {
let flags = if item.enabled {
MF_STRING
} else {
MF_STRING | MF_GRAYED
};
AppendMenuW(menu.0, flags, first_id + index, PCWSTR(label.as_ptr()))
}
None => AppendMenuW(menu.0, MF_SEPARATOR, 0, PCWSTR::null()),
}
}
.map_err(windows_error)?;
}
Ok(menu)
}
}
pub(super) fn encode_label(label: &str) -> io::Result<Vec<u16>> {
let units = label.encode_utf16().count();
if label.is_empty() || label.contains('\0') || units > MAX_POPUP_MENU_LABEL_UNITS {
return Err(invalid("menu label must be 1 to 64 NUL-free units"));
}
let mut encoded = Vec::new();
encoded
.try_reserve_exact(units + 1)
.map_err(|_| allocation_error())?;
encoded.extend(label.encode_utf16());
encoded.push(0);
Ok(encoded)
}
pub(super) struct OwnedMenu(pub(super) HMENU);
impl OwnedMenu {
pub(super) fn into_raw(self) -> HMENU {
let handle = self.0;
std::mem::forget(self);
handle
}
}
impl Drop for OwnedMenu {
fn drop(&mut self) {
let _ = unsafe { DestroyMenu(self.0) };
}
}
impl NativeWindow {
pub fn show_popup_menu(
&mut self,
menu: &PopupMenu,
x: i32,
y: i32,
) -> io::Result<Option<usize>> {
if self.is_destroyed() {
return Err(invalid("cannot show a menu for a destroyed native window"));
}
let native = menu.build(1)?;
let chosen = unsafe {
let _ = SetForegroundWindow(self.hwnd);
let chosen = TrackPopupMenu(
native.0,
TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON,
x,
y,
None,
self.hwnd,
None,
);
let _ = PostMessageW(Some(self.hwnd), WM_NULL, WPARAM(0), LPARAM(0));
chosen
};
Ok(usize::try_from(chosen.0)
.ok()
.and_then(|id| id.checked_sub(1))
.filter(|index| *index < menu.len()))
}
}
pub(super) fn invalid(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message)
}