winapi_easy/ui/
menu.rs

1//! Menus and menu items.
2
3use 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/// A popup menu for use with [`crate::ui::NotificationIcon`].
145#[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    /// Inserts a menu item.
158    ///
159    /// If no index is given, it will be inserted after the last item.
160    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    /// Shows the popup menu at the given coordinates.
170    ///
171    /// The coordinates can for example be retrieved from the window message handler, see
172    /// [crate::ui::messaging::WindowMessageListener::handle_notification_icon_context_select]
173    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/// A menu item.
197///
198/// Can be added with [`PopupMenu::insert_menu_item`].
199#[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}