1use std::cell::RefCell;
4use std::io::ErrorKind;
5use std::marker::PhantomData;
6use std::rc::Rc;
7use std::{
8 io,
9 mem,
10};
11
12pub(crate) use private::MenuHandle;
13use windows::Win32::UI::WindowsAndMessaging::{
14 CreateMenu,
15 CreatePopupMenu,
16 DestroyMenu,
17 GetMenuItemCount,
18 GetMenuItemID,
19 HMENU,
20 InsertMenuItemW,
21 IsMenu,
22 MENUINFO,
23 MENUITEMINFOW,
24 MF_BYPOSITION,
25 MFS_CHECKED,
26 MFS_DISABLED,
27 MFT_RADIOCHECK,
28 MFT_SEPARATOR,
29 MFT_STRING,
30 MIIM_FTYPE,
31 MIIM_ID,
32 MIIM_STATE,
33 MIIM_STRING,
34 MIIM_SUBMENU,
35 MIM_STYLE,
36 MNS_NOTIFYBYPOS,
37 RemoveMenu,
38 SetMenuInfo,
39 SetMenuItemInfoW,
40 TrackPopupMenu,
41};
42
43#[expect(clippy::wildcard_imports)]
44use self::private::*;
45use crate::internal::{
46 ResultExt,
47 ReturnValue,
48};
49use crate::string::ZeroTerminatedWideString;
50use crate::ui::{
51 Point,
52 WindowHandle,
53};
54
55mod private {
56 #[expect(clippy::wildcard_imports)]
57 use super::*;
58
59 #[cfg(test)]
60 static_assertions::assert_not_impl_any!(MenuHandle: Send, Sync);
61
62 #[derive(Eq, PartialEq, Debug)]
63 pub struct MenuHandle {
64 pub(super) raw_handle: HMENU,
65 pub(super) marker: PhantomData<*mut ()>,
66 }
67
68 pub trait MenuKindPrivate {
69 type MenuItem: MenuItemKind;
70 fn new_handle() -> io::Result<MenuHandle>;
71 }
72
73 pub trait MenuItemKind: Clone {
74 fn call_with_raw_menu_info<O>(&self, call: impl FnOnce(MENUITEMINFOW) -> O) -> O;
75 }
76}
77
78impl MenuHandle {
79 fn new_menu() -> io::Result<Self> {
80 let handle = unsafe { CreateMenu()?.if_null_get_last_error()? };
81 let result = Self {
82 raw_handle: handle,
83 marker: PhantomData,
84 };
85 result.set_notify_by_pos()?;
86 Ok(result)
87 }
88
89 fn new_submenu() -> io::Result<Self> {
90 let handle = unsafe { CreatePopupMenu()?.if_null_get_last_error()? };
91 let result = Self {
92 raw_handle: handle,
93 marker: PhantomData,
94 };
95 result.set_notify_by_pos()?;
96 Ok(result)
97 }
98
99 #[expect(dead_code)]
100 pub(crate) fn from_non_null(raw_handle: HMENU) -> Self {
101 Self {
102 raw_handle,
103 marker: PhantomData,
104 }
105 }
106
107 pub(crate) fn from_maybe_null(handle: HMENU) -> Option<Self> {
108 if handle.is_null() {
109 None
110 } else {
111 Some(Self {
112 raw_handle: handle,
113 marker: PhantomData,
114 })
115 }
116 }
117
118 pub(crate) fn as_raw_handle(&self) -> HMENU {
119 self.raw_handle
120 }
121
122 fn set_notify_by_pos(&self) -> io::Result<()> {
126 let raw_menu_info = MENUINFO {
127 cbSize: mem::size_of::<MENUINFO>()
128 .try_into()
129 .unwrap_or_else(|_| unreachable!()),
130 fMask: MIM_STYLE,
131 dwStyle: MNS_NOTIFYBYPOS,
132 cyMax: 0,
133 hbrBack: Default::default(),
134 dwContextHelpID: 0,
135 dwMenuData: 0,
136 };
137 unsafe {
138 SetMenuInfo(self.raw_handle, &raw const raw_menu_info)?;
139 }
140 Ok(())
141 }
142
143 fn insert_menu_item<MI: MenuItemKind>(&self, item: &MI, idx: u32) -> io::Result<()> {
144 let insert_call = |raw_item_info| {
145 unsafe {
146 InsertMenuItemW(self.raw_handle, idx, true, &raw const raw_item_info)?;
147 }
148 Ok(())
149 };
150 item.call_with_raw_menu_info(insert_call)
151 }
152
153 fn modify_menu_item<MI: MenuItemKind>(&self, item: &MI, idx: u32) -> io::Result<()> {
154 let insert_call = |raw_item_info| {
155 unsafe {
156 SetMenuItemInfoW(self.raw_handle, idx, true, &raw const raw_item_info)?;
157 }
158 Ok(())
159 };
160 item.call_with_raw_menu_info(insert_call)
161 }
162
163 fn remove_item(&self, idx: u32) -> io::Result<()> {
167 unsafe {
168 RemoveMenu(self.raw_handle, idx, MF_BYPOSITION)?;
169 }
170 Ok(())
171 }
172
173 pub(crate) fn get_item_id(&self, item_idx: u32) -> io::Result<u32> {
174 let id = unsafe { GetMenuItemID(self.raw_handle, item_idx.cast_signed()) };
175 id.if_eq_to_error((-1i32).cast_unsigned(), || ErrorKind::Other.into())?;
176 Ok(id)
177 }
178
179 fn get_item_count(&self) -> io::Result<i32> {
180 let count = unsafe { GetMenuItemCount(Some(self.raw_handle)) };
181 count.if_eq_to_error(-1, io::Error::last_os_error)?;
182 Ok(count)
183 }
184
185 #[expect(dead_code)]
186 fn is_menu(&self) -> bool {
187 unsafe { IsMenu(self.raw_handle).as_bool() }
188 }
189
190 fn destroy(&self) -> io::Result<()> {
191 unsafe {
192 DestroyMenu(self.raw_handle)?;
193 }
194 Ok(())
195 }
196}
197
198impl From<MenuHandle> for HMENU {
199 fn from(value: MenuHandle) -> Self {
200 value.raw_handle
201 }
202}
203
204impl From<&MenuHandle> for HMENU {
205 fn from(value: &MenuHandle) -> Self {
206 value.raw_handle
207 }
208}
209
210pub trait MenuKind: MenuKindPrivate {}
211
212#[derive(Debug)]
213pub enum MenuBarKind {}
214
215impl MenuKindPrivate for MenuBarKind {
216 type MenuItem = TextMenuItem;
217
218 fn new_handle() -> io::Result<MenuHandle> {
219 MenuHandle::new_menu()
220 }
221}
222
223impl MenuKind for MenuBarKind {}
224
225#[derive(Debug)]
226pub enum SubMenuKind {}
227
228impl MenuKindPrivate for SubMenuKind {
229 type MenuItem = SubMenuItem;
230
231 fn new_handle() -> io::Result<MenuHandle> {
232 MenuHandle::new_submenu()
233 }
234}
235
236impl MenuKind for SubMenuKind {}
237
238#[cfg(test)]
239static_assertions::assert_not_impl_any!(Menu<MenuBarKind>: Send, Sync);
240#[cfg(test)]
241static_assertions::assert_not_impl_any!(Menu<SubMenuKind>: Send, Sync);
242
243#[derive(Debug)]
245pub struct Menu<MK: MenuKind> {
246 handle: MenuHandle,
247 items: Vec<MK::MenuItem>,
248}
249
250impl<MK: MenuKind> Menu<MK> {
251 pub fn new() -> io::Result<Self> {
252 Ok(Self {
253 handle: MK::new_handle()?,
254 items: Vec::new(),
255 })
256 }
257
258 pub fn new_from_items<I>(items: I) -> io::Result<Self>
259 where
260 I: IntoIterator<Item = MK::MenuItem>,
261 {
262 let mut result = Self::new()?;
263 result.insert_menu_items(items)?;
264 Ok(result)
265 }
266
267 pub fn as_handle(&self) -> &MenuHandle {
268 &self.handle
269 }
270
271 pub fn insert_menu_item(&mut self, item: MK::MenuItem, index: Option<u32>) -> io::Result<()> {
279 let handle_item_count: u32 = self
280 .handle
281 .get_item_count()?
282 .try_into()
283 .unwrap_or_else(|_| unreachable!());
284 assert_eq!(handle_item_count, self.items.len().try_into().unwrap());
285 let idx = match index {
286 Some(idx) => idx,
287 None => handle_item_count,
288 };
289 self.handle.insert_menu_item(&item, idx)?;
290 self.items.insert(idx.try_into().unwrap(), item);
291 Ok(())
292 }
293
294 pub fn insert_menu_items<I>(&mut self, items: I) -> io::Result<()>
295 where
296 I: IntoIterator<Item = MK::MenuItem>,
297 {
298 for item in items {
299 self.insert_menu_item(item, None)?;
300 }
301 Ok(())
302 }
303
304 pub fn modify_menu_item_by_index(
310 &mut self,
311 index: u32,
312 modify_fn: impl FnOnce(&mut MK::MenuItem) -> io::Result<()>,
313 ) -> io::Result<()> {
314 let item = &mut self.items[usize::try_from(index).unwrap()];
315 let mut modified_item = item.clone();
316 modify_fn(&mut modified_item)?;
317 self.handle.modify_menu_item(&modified_item, index)?;
318 *item = modified_item;
319 Ok(())
320 }
321
322 pub fn remove_menu_item(&mut self, index: u32) -> io::Result<()> {
328 let index_usize = usize::try_from(index).unwrap();
329 assert!(index_usize < self.items.len());
330 self.handle.remove_item(index)?;
331 let _ = self.items.remove(index_usize);
332 Ok(())
333 }
334}
335
336impl Menu<SubMenuKind> {
337 pub fn modify_text_menu_items_by_id(
341 &mut self,
342 id: u32,
343 mut modify_fn: impl FnMut(&mut TextMenuItem) -> io::Result<()>,
344 ) -> io::Result<()> {
345 let indexes: Vec<_> = (0..)
346 .zip(&self.items)
347 .filter_map(|(index, item)| match item {
348 SubMenuItem::Text(text_menu_item) => {
349 if text_menu_item.id == id {
350 Some(index)
351 } else {
352 None
353 }
354 }
355 SubMenuItem::Separator => None,
356 })
357 .collect();
358 let mut internal_modify_fn = |item: &mut SubMenuItem| {
359 if let SubMenuItem::Text(item) = item {
360 modify_fn(item)?;
361 } else {
362 unreachable!()
363 }
364 Ok(())
365 };
366 for index in indexes {
367 self.modify_menu_item_by_index(index, &mut internal_modify_fn)?;
368 }
369 Ok(())
370 }
371
372 pub fn show_menu(&self, window: WindowHandle, coords: Point) -> io::Result<()> {
380 unsafe {
381 TrackPopupMenu(
382 self.handle.raw_handle,
383 Default::default(),
384 coords.x,
385 coords.y,
386 None,
387 window.into(),
388 None,
389 )
390 .if_null_get_last_error_else_drop()?;
391 }
392 Ok(())
393 }
394}
395
396impl<MK: MenuKind> Drop for Menu<MK> {
397 fn drop(&mut self) {
398 let size_u32 = u32::try_from(self.items.len()).unwrap();
399 for index in (0..size_u32).rev() {
401 self.remove_menu_item(index)
402 .unwrap_or_default_and_print_error();
403 }
404 self.handle.destroy().unwrap_or_default_and_print_error();
405 }
406}
407
408pub type MenuBar = Menu<MenuBarKind>;
412
413pub type SubMenu = Menu<SubMenuKind>;
417
418#[derive(Clone, Debug)]
422pub enum SubMenuItem {
423 Text(TextMenuItem),
424 Separator,
425}
426
427impl MenuItemKind for SubMenuItem {
428 fn call_with_raw_menu_info<O>(&self, call: impl FnOnce(MENUITEMINFOW) -> O) -> O {
429 match self {
430 SubMenuItem::Text(text_item) => text_item.call_with_raw_menu_info(call),
431 SubMenuItem::Separator => {
432 let mut item_info = default_raw_item_info();
433 item_info.fMask |= MIIM_FTYPE;
434 item_info.fType |= MFT_SEPARATOR;
435 call(item_info)
436 }
437 }
438 }
439}
440
441#[derive(Clone, Default, Debug)]
442pub struct TextMenuItem {
443 pub id: u32,
444 pub text: String,
445 pub disabled: bool,
446 pub item_symbol: Option<ItemSymbol>,
447 pub sub_menu: Option<Rc<RefCell<SubMenu>>>,
448}
449
450impl TextMenuItem {
451 pub fn default_with_text(id: u32, text: impl Into<String>) -> Self {
452 Self {
453 id,
454 text: text.into(),
455 disabled: false,
456 item_symbol: None,
457 sub_menu: None,
458 }
459 }
460}
461
462impl MenuItemKind for TextMenuItem {
463 fn call_with_raw_menu_info<O>(&self, call: impl FnOnce(MENUITEMINFOW) -> O) -> O {
464 let mut text_wide_string = ZeroTerminatedWideString::from_os_str(&self.text);
466 let mut item_info = default_raw_item_info();
467 item_info.fMask |= MIIM_FTYPE | MIIM_STATE | MIIM_ID | MIIM_SUBMENU | MIIM_STRING;
468 item_info.fType |= MFT_STRING;
469 item_info.cch = text_wide_string.as_ref().len().try_into().unwrap();
470 item_info.dwTypeData = text_wide_string.as_raw_pwstr();
471 if self.disabled {
472 item_info.fState |= MFS_DISABLED;
473 }
474 if let Some(checkmark) = self.item_symbol {
475 item_info.fState |= MFS_CHECKED;
476 match checkmark {
477 ItemSymbol::CheckMark => (),
478 ItemSymbol::RadioButton => item_info.fType |= MFT_RADIOCHECK,
479 }
480 }
481 item_info.wID = self.id;
484 if let Some(submenu) = &self.sub_menu {
485 item_info.hSubMenu = submenu.borrow().handle.raw_handle;
486 }
487 call(item_info)
488 }
489}
490
491#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
492pub enum ItemSymbol {
493 #[default]
494 CheckMark,
495 RadioButton,
496}
497
498fn default_raw_item_info() -> MENUITEMINFOW {
499 MENUITEMINFOW {
500 cbSize: mem::size_of::<MENUITEMINFOW>()
501 .try_into()
502 .unwrap_or_else(|_| unreachable!()),
503 ..Default::default()
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn create_test_menu() -> io::Result<()> {
513 let mut menu = SubMenu::new()?;
514 const TEST_ID: u32 = 42;
515 const TEST_ID2: u32 = 43;
516 menu.insert_menu_items([
517 SubMenuItem::Text(TextMenuItem::default_with_text(TEST_ID, "text")),
518 SubMenuItem::Separator,
519 ])?;
520 menu.modify_menu_item_by_index(0, |item| {
521 if let SubMenuItem::Text(item) = item {
522 item.disabled = true;
523 Ok(())
524 } else {
525 panic!()
526 }
527 })?;
528 menu.modify_menu_item_by_index(1, |item| {
529 *item = SubMenuItem::Text(TextMenuItem::default_with_text(TEST_ID2, "text2"));
530 Ok(())
531 })?;
532 let submenu2: Rc<RefCell<_>> = {
533 let submenu2 = SubMenu::new_from_items([SubMenuItem::Separator])?;
534 Rc::new(RefCell::new(submenu2))
535 };
536 {
537 let mut menu2 = SubMenu::new()?;
538 menu2.insert_menu_item(
539 SubMenuItem::Text(TextMenuItem {
540 sub_menu: Some(submenu2.clone()),
541 ..TextMenuItem::default_with_text(0, "")
542 }),
543 None,
544 )?;
545 }
546 menu.insert_menu_item(
547 SubMenuItem::Text(TextMenuItem {
548 sub_menu: Some(submenu2),
549 ..TextMenuItem::default_with_text(0, "Submenu")
550 }),
551 None,
552 )?;
553 assert_eq!(menu.handle.get_item_count()?, 3);
554 assert_eq!(menu.handle.get_item_id(0)?, TEST_ID);
555 assert_eq!(menu.handle.get_item_id(1)?, TEST_ID2);
556
557 let menu = Rc::new(RefCell::new(menu));
558 let menu_bar = MenuBar::new_from_items([TextMenuItem {
559 sub_menu: Some(menu),
560 ..TextMenuItem::default_with_text(0, "File")
561 }])?;
562 assert_eq!(menu_bar.handle.get_item_count()?, 1);
563
564 Ok(())
565 }
566}