use std::cell::RefCell;
use std::io::ErrorKind;
use std::marker::PhantomData;
use std::rc::Rc;
use std::{
io,
mem,
};
pub(crate) use private::MenuHandle;
use windows::Win32::UI::WindowsAndMessaging::{
CreateMenu,
CreatePopupMenu,
DestroyMenu,
GetMenuItemCount,
GetMenuItemID,
HMENU,
InsertMenuItemW,
IsMenu,
MENUINFO,
MENUITEMINFOW,
MF_BYPOSITION,
MFS_CHECKED,
MFS_DISABLED,
MFT_RADIOCHECK,
MFT_SEPARATOR,
MFT_STRING,
MIIM_FTYPE,
MIIM_ID,
MIIM_STATE,
MIIM_STRING,
MIIM_SUBMENU,
MIM_STYLE,
MNS_NOTIFYBYPOS,
RemoveMenu,
SetMenuInfo,
SetMenuItemInfoW,
TrackPopupMenu,
};
#[expect(clippy::wildcard_imports)]
use self::private::*;
use crate::internal::{
ResultExt,
ReturnValue,
};
use crate::string::ZeroTerminatedWideString;
use crate::ui::{
Point,
WindowHandle,
};
mod private {
#[expect(clippy::wildcard_imports)]
use super::*;
#[cfg(test)]
static_assertions::assert_not_impl_any!(MenuHandle: Send, Sync);
#[derive(Eq, PartialEq, Debug)]
pub struct MenuHandle {
pub(super) raw_handle: HMENU,
pub(super) marker: PhantomData<*mut ()>,
}
pub trait MenuKindPrivate {
type MenuItem: MenuItemKind;
fn new_handle() -> io::Result<MenuHandle>;
}
pub trait MenuItemKind: Clone {
fn call_with_raw_menu_info<O>(&self, call: impl FnOnce(MENUITEMINFOW) -> O) -> O;
}
}
impl MenuHandle {
fn new_menu() -> io::Result<Self> {
let handle = unsafe { CreateMenu()?.if_null_get_last_error()? };
let result = Self {
raw_handle: handle,
marker: PhantomData,
};
result.set_notify_by_pos()?;
Ok(result)
}
fn new_submenu() -> io::Result<Self> {
let handle = unsafe { CreatePopupMenu()?.if_null_get_last_error()? };
let result = Self {
raw_handle: handle,
marker: PhantomData,
};
result.set_notify_by_pos()?;
Ok(result)
}
#[expect(dead_code)]
pub(crate) fn from_non_null(raw_handle: HMENU) -> Self {
Self {
raw_handle,
marker: PhantomData,
}
}
pub(crate) fn from_maybe_null(handle: HMENU) -> Option<Self> {
if handle.is_null() {
None
} else {
Some(Self {
raw_handle: handle,
marker: PhantomData,
})
}
}
pub(crate) fn as_raw_handle(&self) -> HMENU {
self.raw_handle
}
fn set_notify_by_pos(&self) -> io::Result<()> {
let raw_menu_info = MENUINFO {
cbSize: mem::size_of::<MENUINFO>()
.try_into()
.unwrap_or_else(|_| unreachable!()),
fMask: MIM_STYLE,
dwStyle: MNS_NOTIFYBYPOS,
cyMax: 0,
hbrBack: Default::default(),
dwContextHelpID: 0,
dwMenuData: 0,
};
unsafe {
SetMenuInfo(self.raw_handle, &raw const raw_menu_info)?;
}
Ok(())
}
fn insert_menu_item<MI: MenuItemKind>(&self, item: &MI, idx: u32) -> io::Result<()> {
let insert_call = |raw_item_info| {
unsafe {
InsertMenuItemW(self.raw_handle, idx, true, &raw const raw_item_info)?;
}
Ok(())
};
item.call_with_raw_menu_info(insert_call)
}
fn modify_menu_item<MI: MenuItemKind>(&self, item: &MI, idx: u32) -> io::Result<()> {
let insert_call = |raw_item_info| {
unsafe {
SetMenuItemInfoW(self.raw_handle, idx, true, &raw const raw_item_info)?;
}
Ok(())
};
item.call_with_raw_menu_info(insert_call)
}
fn remove_item(&self, idx: u32) -> io::Result<()> {
unsafe {
RemoveMenu(self.raw_handle, idx, MF_BYPOSITION)?;
}
Ok(())
}
pub(crate) fn get_item_id(&self, item_idx: u32) -> io::Result<u32> {
let id = unsafe { GetMenuItemID(self.raw_handle, item_idx.cast_signed()) };
id.if_eq_to_error((-1i32).cast_unsigned(), || ErrorKind::Other.into())?;
Ok(id)
}
fn get_item_count(&self) -> io::Result<i32> {
let count = unsafe { GetMenuItemCount(Some(self.raw_handle)) };
count.if_eq_to_error(-1, io::Error::last_os_error)?;
Ok(count)
}
#[expect(dead_code)]
fn is_menu(&self) -> bool {
unsafe { IsMenu(self.raw_handle).as_bool() }
}
fn destroy(&self) -> io::Result<()> {
unsafe {
DestroyMenu(self.raw_handle)?;
}
Ok(())
}
}
impl From<MenuHandle> for HMENU {
fn from(value: MenuHandle) -> Self {
value.raw_handle
}
}
impl From<&MenuHandle> for HMENU {
fn from(value: &MenuHandle) -> Self {
value.raw_handle
}
}
pub trait MenuKind: MenuKindPrivate {}
#[derive(Debug)]
pub enum MenuBarKind {}
impl MenuKindPrivate for MenuBarKind {
type MenuItem = TextMenuItem;
fn new_handle() -> io::Result<MenuHandle> {
MenuHandle::new_menu()
}
}
impl MenuKind for MenuBarKind {}
#[derive(Debug)]
pub enum SubMenuKind {}
impl MenuKindPrivate for SubMenuKind {
type MenuItem = SubMenuItem;
fn new_handle() -> io::Result<MenuHandle> {
MenuHandle::new_submenu()
}
}
impl MenuKind for SubMenuKind {}
#[cfg(test)]
static_assertions::assert_not_impl_any!(Menu<MenuBarKind>: Send, Sync);
#[cfg(test)]
static_assertions::assert_not_impl_any!(Menu<SubMenuKind>: Send, Sync);
#[derive(Debug)]
pub struct Menu<MK: MenuKind> {
handle: MenuHandle,
items: Vec<MK::MenuItem>,
}
impl<MK: MenuKind> Menu<MK> {
pub fn new() -> io::Result<Self> {
Ok(Self {
handle: MK::new_handle()?,
items: Vec::new(),
})
}
pub fn new_from_items<I>(items: I) -> io::Result<Self>
where
I: IntoIterator<Item = MK::MenuItem>,
{
let mut result = Self::new()?;
result.insert_menu_items(items)?;
Ok(result)
}
pub fn as_handle(&self) -> &MenuHandle {
&self.handle
}
pub fn insert_menu_item(&mut self, item: MK::MenuItem, index: Option<u32>) -> io::Result<()> {
let handle_item_count: u32 = self
.handle
.get_item_count()?
.try_into()
.unwrap_or_else(|_| unreachable!());
assert_eq!(handle_item_count, self.items.len().try_into().unwrap());
let idx = match index {
Some(idx) => idx,
None => handle_item_count,
};
self.handle.insert_menu_item(&item, idx)?;
self.items.insert(idx.try_into().unwrap(), item);
Ok(())
}
pub fn insert_menu_items<I>(&mut self, items: I) -> io::Result<()>
where
I: IntoIterator<Item = MK::MenuItem>,
{
for item in items {
self.insert_menu_item(item, None)?;
}
Ok(())
}
pub fn modify_menu_item_by_index(
&mut self,
index: u32,
modify_fn: impl FnOnce(&mut MK::MenuItem) -> io::Result<()>,
) -> io::Result<()> {
let item = &mut self.items[usize::try_from(index).unwrap()];
let mut modified_item = item.clone();
modify_fn(&mut modified_item)?;
self.handle.modify_menu_item(&modified_item, index)?;
*item = modified_item;
Ok(())
}
pub fn remove_menu_item(&mut self, index: u32) -> io::Result<()> {
let index_usize = usize::try_from(index).unwrap();
assert!(index_usize < self.items.len());
self.handle.remove_item(index)?;
let _ = self.items.remove(index_usize);
Ok(())
}
}
impl Menu<SubMenuKind> {
pub fn modify_text_menu_items_by_id(
&mut self,
id: u32,
mut modify_fn: impl FnMut(&mut TextMenuItem) -> io::Result<()>,
) -> io::Result<()> {
let indexes: Vec<_> = (0..)
.zip(&self.items)
.filter_map(|(index, item)| match item {
SubMenuItem::Text(text_menu_item) => {
if text_menu_item.id == id {
Some(index)
} else {
None
}
}
SubMenuItem::Separator => None,
})
.collect();
let mut internal_modify_fn = |item: &mut SubMenuItem| {
if let SubMenuItem::Text(item) = item {
modify_fn(item)?;
} else {
unreachable!()
}
Ok(())
};
for index in indexes {
self.modify_menu_item_by_index(index, &mut internal_modify_fn)?;
}
Ok(())
}
pub fn show_menu(&self, window: WindowHandle, coords: Point) -> io::Result<()> {
unsafe {
TrackPopupMenu(
self.handle.raw_handle,
Default::default(),
coords.x,
coords.y,
None,
window.into(),
None,
)
.if_null_get_last_error_else_drop()?;
}
Ok(())
}
}
impl<MK: MenuKind> Drop for Menu<MK> {
fn drop(&mut self) {
let size_u32 = u32::try_from(self.items.len()).unwrap();
for index in (0..size_u32).rev() {
self.remove_menu_item(index)
.unwrap_or_default_and_print_error();
}
self.handle.destroy().unwrap_or_default_and_print_error();
}
}
pub type MenuBar = Menu<MenuBarKind>;
pub type SubMenu = Menu<SubMenuKind>;
#[derive(Clone, Debug)]
pub enum SubMenuItem {
Text(TextMenuItem),
Separator,
}
impl MenuItemKind for SubMenuItem {
fn call_with_raw_menu_info<O>(&self, call: impl FnOnce(MENUITEMINFOW) -> O) -> O {
match self {
SubMenuItem::Text(text_item) => text_item.call_with_raw_menu_info(call),
SubMenuItem::Separator => {
let mut item_info = default_raw_item_info();
item_info.fMask |= MIIM_FTYPE;
item_info.fType |= MFT_SEPARATOR;
call(item_info)
}
}
}
}
#[derive(Clone, Default, Debug)]
pub struct TextMenuItem {
pub id: u32,
pub text: String,
pub disabled: bool,
pub item_symbol: Option<ItemSymbol>,
pub sub_menu: Option<Rc<RefCell<SubMenu>>>,
}
impl TextMenuItem {
pub fn default_with_text(id: u32, text: impl Into<String>) -> Self {
Self {
id,
text: text.into(),
disabled: false,
item_symbol: None,
sub_menu: None,
}
}
}
impl MenuItemKind for TextMenuItem {
fn call_with_raw_menu_info<O>(&self, call: impl FnOnce(MENUITEMINFOW) -> O) -> O {
let mut text_wide_string = ZeroTerminatedWideString::from_os_str(&self.text);
let mut item_info = default_raw_item_info();
item_info.fMask |= MIIM_FTYPE | MIIM_STATE | MIIM_ID | MIIM_SUBMENU | MIIM_STRING;
item_info.fType |= MFT_STRING;
item_info.cch = text_wide_string.as_ref().len().try_into().unwrap();
item_info.dwTypeData = text_wide_string.as_raw_pwstr();
if self.disabled {
item_info.fState |= MFS_DISABLED;
}
if let Some(checkmark) = self.item_symbol {
item_info.fState |= MFS_CHECKED;
match checkmark {
ItemSymbol::CheckMark => (),
ItemSymbol::RadioButton => item_info.fType |= MFT_RADIOCHECK,
}
}
item_info.wID = self.id;
if let Some(submenu) = &self.sub_menu {
item_info.hSubMenu = submenu.borrow().handle.raw_handle;
}
call(item_info)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum ItemSymbol {
#[default]
CheckMark,
RadioButton,
}
fn default_raw_item_info() -> MENUITEMINFOW {
MENUITEMINFOW {
cbSize: mem::size_of::<MENUITEMINFOW>()
.try_into()
.unwrap_or_else(|_| unreachable!()),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_test_menu() -> io::Result<()> {
let mut menu = SubMenu::new()?;
const TEST_ID: u32 = 42;
const TEST_ID2: u32 = 43;
menu.insert_menu_items([
SubMenuItem::Text(TextMenuItem::default_with_text(TEST_ID, "text")),
SubMenuItem::Separator,
])?;
menu.modify_menu_item_by_index(0, |item| {
if let SubMenuItem::Text(item) = item {
item.disabled = true;
Ok(())
} else {
panic!()
}
})?;
menu.modify_menu_item_by_index(1, |item| {
*item = SubMenuItem::Text(TextMenuItem::default_with_text(TEST_ID2, "text2"));
Ok(())
})?;
let submenu2: Rc<RefCell<_>> = {
let submenu2 = SubMenu::new_from_items([SubMenuItem::Separator])?;
Rc::new(RefCell::new(submenu2))
};
{
let mut menu2 = SubMenu::new()?;
menu2.insert_menu_item(
SubMenuItem::Text(TextMenuItem {
sub_menu: Some(submenu2.clone()),
..TextMenuItem::default_with_text(0, "")
}),
None,
)?;
}
menu.insert_menu_item(
SubMenuItem::Text(TextMenuItem {
sub_menu: Some(submenu2),
..TextMenuItem::default_with_text(0, "Submenu")
}),
None,
)?;
assert_eq!(menu.handle.get_item_count()?, 3);
assert_eq!(menu.handle.get_item_id(0)?, TEST_ID);
assert_eq!(menu.handle.get_item_id(1)?, TEST_ID2);
let menu = Rc::new(RefCell::new(menu));
let menu_bar = MenuBar::new_from_items([TextMenuItem {
sub_menu: Some(menu),
..TextMenuItem::default_with_text(0, "File")
}])?;
assert_eq!(menu_bar.handle.get_item_count()?, 1);
Ok(())
}
}