1use std::io;
4use std::io::ErrorKind;
5use std::marker::PhantomData;
6use std::mem;
7
8use windows::core::PWSTR;
9use windows::Win32::UI::WindowsAndMessaging::{
10 CreatePopupMenu,
11 DestroyMenu,
12 GetMenuItemCount,
13 GetMenuItemID,
14 InsertMenuItemW,
15 SetMenuInfo,
16 TrackPopupMenu,
17 HMENU,
18 MENUINFO,
19 MENUITEMINFOW,
20 MFT_SEPARATOR,
21 MIIM_FTYPE,
22 MIIM_ID,
23 MIIM_STRING,
24 MIM_APPLYTOSUBMENUS,
25 MIM_STYLE,
26 MNS_NOTIFYBYPOS,
27};
28
29use crate::internal::ReturnValue;
30use crate::string::ToWideString;
31use crate::ui::{
32 Point,
33 WindowHandle,
34};
35
36#[derive(Eq, PartialEq, Debug)]
37pub(crate) struct MenuHandle {
38 raw_handle: HMENU,
39 marker: PhantomData<*mut ()>,
40}
41
42impl MenuHandle {
43 fn new_popup_menu() -> io::Result<Self> {
44 let handle = unsafe { CreatePopupMenu()?.if_null_get_last_error()? };
45 let result = Self {
46 raw_handle: handle,
47 marker: PhantomData,
48 };
49 result.set_info()?;
50 Ok(result)
51 }
52
53 #[allow(dead_code)]
54 pub(crate) fn from_non_null(raw_handle: HMENU) -> Self {
55 Self {
56 raw_handle,
57 marker: PhantomData,
58 }
59 }
60
61 pub(crate) fn from_maybe_null(handle: HMENU) -> Option<Self> {
62 if !handle.is_null() {
63 Some(Self {
64 raw_handle: handle,
65 marker: PhantomData,
66 })
67 } else {
68 None
69 }
70 }
71
72 fn set_info(&self) -> io::Result<()> {
73 let raw_menu_info = MENUINFO {
74 cbSize: mem::size_of::<MENUINFO>()
75 .try_into()
76 .expect("MENUINFO size conversion failed"),
77 fMask: MIM_APPLYTOSUBMENUS | MIM_STYLE,
78 dwStyle: MNS_NOTIFYBYPOS,
79 cyMax: 0,
80 hbrBack: Default::default(),
81 dwContextHelpID: 0,
82 dwMenuData: 0,
83 };
84 unsafe {
85 SetMenuInfo(self.raw_handle, &raw_menu_info)?;
86 }
87 Ok(())
88 }
89
90 fn insert_submenu_item(&self, idx: u32, item: MenuItem, id: u32) -> io::Result<()> {
91 unsafe {
92 InsertMenuItemW(
93 self.raw_handle,
94 idx,
95 true,
96 &MenuItemCallData::new(Some(&mut item.into()), Some(id)).item_info_struct,
97 )?;
98 }
99 Ok(())
100 }
101
102 pub(crate) fn get_item_id(&self, item_idx: u32) -> io::Result<u32> {
103 let id = unsafe {
104 GetMenuItemID(
105 self.raw_handle,
106 item_idx.try_into().map_err(|_err| {
107 io::Error::new(
108 ErrorKind::InvalidInput,
109 format!("Bad item index: {}", item_idx),
110 )
111 })?,
112 )
113 };
114 id.if_eq_to_error(-1i32 as u32, || ErrorKind::Other.into())?;
115 Ok(id)
116 }
117
118 fn get_item_count(&self) -> io::Result<i32> {
119 let count = unsafe { GetMenuItemCount(self.raw_handle) };
120 count.if_eq_to_error(-1, io::Error::last_os_error)?;
121 Ok(count)
122 }
123
124 fn destroy(&self) -> io::Result<()> {
125 unsafe {
126 DestroyMenu(self.raw_handle)?;
127 }
128 Ok(())
129 }
130}
131
132impl From<MenuHandle> for HMENU {
133 fn from(value: MenuHandle) -> Self {
134 value.raw_handle
135 }
136}
137
138impl From<&MenuHandle> for HMENU {
139 fn from(value: &MenuHandle) -> Self {
140 value.raw_handle
141 }
142}
143
144#[derive(Debug)]
146pub struct PopupMenu {
147 handle: MenuHandle,
148}
149
150impl PopupMenu {
151 pub fn new() -> io::Result<Self> {
152 Ok(PopupMenu {
153 handle: MenuHandle::new_popup_menu()?,
154 })
155 }
156
157 pub fn insert_menu_item(&self, item: MenuItem, id: u32, index: Option<u32>) -> io::Result<()> {
161 let idx = match index {
162 Some(idx) => idx,
163 None => self.handle.get_item_count()?.try_into().unwrap(),
164 };
165 self.handle.insert_submenu_item(idx, item, id)?;
166 Ok(())
167 }
168
169 pub fn show_popup_menu(&self, window: &WindowHandle, coords: Point) -> io::Result<()> {
174 unsafe {
175 TrackPopupMenu(
176 self.handle.raw_handle,
177 Default::default(),
178 coords.x,
179 coords.y,
180 0,
181 window.raw_handle,
182 None,
183 )
184 .if_null_get_last_error_else_drop()?;
185 }
186 Ok(())
187 }
188}
189
190impl Drop for PopupMenu {
191 fn drop(&mut self) {
192 self.handle.destroy().unwrap()
193 }
194}
195
196#[derive(Copy, Clone, Eq, PartialEq, Debug)]
200pub enum MenuItem<'a> {
201 Text(&'a str),
202 Separator,
203}
204
205enum MenuItemRaw {
206 WideText(Vec<u16>),
207 Separator,
208}
209
210impl<'a> From<MenuItem<'a>> for MenuItemRaw {
211 fn from(item: MenuItem<'a>) -> Self {
212 match item {
213 MenuItem::Text(text) => MenuItemRaw::WideText(text.to_wide_string()),
214 MenuItem::Separator => MenuItemRaw::Separator,
215 }
216 }
217}
218
219struct MenuItemCallData<'a> {
220 item_info_struct: MENUITEMINFOW,
221 phantom: PhantomData<&'a MenuItemRaw>,
222}
223
224impl<'a> MenuItemCallData<'a> {
225 fn new(mut menu_item: Option<&'a mut MenuItemRaw>, id: Option<u32>) -> Self {
226 let mut item_info = MENUITEMINFOW {
227 cbSize: mem::size_of::<MENUITEMINFOW>()
228 .try_into()
229 .expect("MENUITEMINFOW size conversion failed"),
230 ..Default::default()
231 };
232 match &mut menu_item {
233 Some(MenuItemRaw::WideText(ref mut wide_string)) => {
234 item_info.fMask |= MIIM_STRING;
235 item_info.cch = wide_string.len().try_into().unwrap();
236 item_info.dwTypeData = PWSTR::from_raw(wide_string.as_mut_ptr());
237 }
238 Some(MenuItemRaw::Separator) => {
239 item_info.fMask |= MIIM_FTYPE;
240 item_info.fType |= MFT_SEPARATOR;
241 }
242 None => (),
243 }
244 if let Some(id) = id {
245 item_info.fMask |= MIIM_ID;
246 item_info.wID = id;
247 }
248 Self {
249 item_info_struct: item_info,
250 phantom: PhantomData,
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn create_test_menu() -> io::Result<()> {
261 let menu = PopupMenu::new()?;
262 const TEST_ID: u32 = 42;
263 menu.insert_menu_item(MenuItem::Text("Show window"), TEST_ID, None)?;
264 menu.insert_menu_item(MenuItem::Separator, TEST_ID + 1, None)?;
265 assert_eq!(menu.handle.get_item_count()?, 2);
266 assert_eq!(menu.handle.get_item_id(0)?, TEST_ID);
267 Ok(())
268 }
269}