use windows_sys::Win32::Foundation::HWND;
use windows_sys::Win32::Foundation::POINT;
use windows_sys::Win32::UI::WindowsAndMessaging::{
AppendMenuW, CreatePopupMenu, DestroyMenu, PostMessageW, SetForegroundWindow, TrackPopupMenu,
HMENU, MF_CHECKED, MF_DISABLED, MF_POPUP, MF_SEPARATOR, MF_STRING, MF_UNCHECKED,
TPM_RIGHTBUTTON, WM_NULL,
};
use winspaces_win32::text::encode_wide;
use super::MenuEntry;
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn show(owner: HWND, entries: &[MenuEntry], anchor: POINT) {
unsafe {
let hmenu = build_hmenu(entries);
SetForegroundWindow(owner);
TrackPopupMenu(
hmenu,
TPM_RIGHTBUTTON,
anchor.x,
anchor.y,
0,
owner,
std::ptr::null(),
);
PostMessageW(owner, WM_NULL, 0, 0);
DestroyMenu(hmenu);
}
}
pub fn build_hmenu(entries: &[MenuEntry]) -> HMENU {
unsafe {
let hmenu = CreatePopupMenu();
for entry in entries {
append_entry(hmenu, entry);
}
hmenu
}
}
unsafe fn append_entry(hmenu: HMENU, entry: &MenuEntry) {
match entry {
MenuEntry::Header(text) => {
AppendMenuW(
hmenu,
MF_DISABLED | MF_STRING,
0,
encode_wide(text).as_ptr(),
);
}
MenuEntry::Separator => {
AppendMenuW(hmenu, MF_SEPARATOR, 0, std::ptr::null());
}
MenuEntry::Item(it) => {
let label = labeled(&it.label, it.shortcut.as_deref());
if let Some(sub) = &it.submenu {
let hsub = build_hmenu(sub);
AppendMenuW(hmenu, MF_POPUP, hsub as usize, encode_wide(&label).as_ptr());
} else {
let flags = MF_STRING | if it.checked { MF_CHECKED } else { MF_UNCHECKED };
AppendMenuW(hmenu, flags, it.id, encode_wide(&label).as_ptr());
}
}
}
}
fn labeled(label: &str, shortcut: Option<&str>) -> String {
match shortcut {
Some(s) => format!("{} ({})", label, s),
None => label.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::menu::MenuItemData;
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetMenuItemCount, GetMenuItemID, GetMenuState, GetMenuStringW, GetSubMenu, MF_BYPOSITION,
};
fn item(
id: usize,
label: &str,
shortcut: Option<&str>,
checked: bool,
submenu: Option<Vec<MenuEntry>>,
) -> MenuEntry {
MenuEntry::Item(MenuItemData {
id,
glyph: None,
label: label.to_string(),
shortcut: shortcut.map(str::to_string),
checked,
submenu,
})
}
fn menu_text(hmenu: HMENU, pos: u32) -> String {
unsafe {
let mut buf = [0u16; 256];
let len = GetMenuStringW(
hmenu,
pos,
buf.as_mut_ptr(),
buf.len() as i32,
MF_BYPOSITION,
);
String::from_utf16_lossy(&buf[..len.max(0) as usize])
}
}
fn fixture() -> Vec<MenuEntry> {
vec![
MenuEntry::Header("WinSpaces v0.1.0".to_string()),
item(999, "Overview", Some("Win+Tab"), false, None),
MenuEntry::Separator,
item(
0,
"Display 1",
None,
false,
Some(vec![
item(2000, "Space 1", Some("Alt+1"), true, None),
item(2001, "Space 2", Some("Alt+2"), false, None),
MenuEntry::Separator,
item(2099, "Remove Space 2", None, false, None),
]),
),
item(1002, "Settings", None, false, None),
]
}
#[test]
fn item_order_preserved() {
let entries = fixture();
let hmenu = build_hmenu(&entries);
unsafe {
assert_eq!(GetMenuItemCount(hmenu), 5);
assert_eq!(menu_text(hmenu, 0), "WinSpaces v0.1.0");
assert_eq!(menu_text(hmenu, 1), "Overview (Win+Tab)");
assert_eq!(menu_text(hmenu, 3), "Display 1");
assert_eq!(menu_text(hmenu, 4), "Settings");
DestroyMenu(hmenu);
}
}
#[test]
fn header_and_separator_carry_id_zero() {
let entries = fixture();
let hmenu = build_hmenu(&entries);
unsafe {
assert_eq!(GetMenuItemID(hmenu, 0), 0);
assert_eq!(GetMenuItemID(hmenu, 2), 0);
let header_state = GetMenuState(hmenu, 0, MF_BYPOSITION);
assert_ne!(header_state & MF_DISABLED, 0);
DestroyMenu(hmenu);
}
}
#[test]
fn submenu_nests_as_popup() {
let entries = fixture();
let hmenu = build_hmenu(&entries);
unsafe {
let sub = GetSubMenu(hmenu, 3);
assert!(!sub.is_null());
assert_eq!(GetMenuItemCount(sub), 4);
assert_eq!(GetMenuItemID(sub, 0) as usize, 2000);
assert_eq!(GetMenuItemID(sub, 1) as usize, 2001);
assert_eq!(GetMenuItemID(sub, 2), 0); assert_eq!(GetMenuItemID(sub, 3) as usize, 2099);
DestroyMenu(hmenu);
}
}
#[test]
fn checked_item_is_checked_only_the_checked_one() {
let entries = fixture();
let hmenu = build_hmenu(&entries);
unsafe {
let sub = GetSubMenu(hmenu, 3);
let space1_state = GetMenuState(sub, 0, MF_BYPOSITION);
let space2_state = GetMenuState(sub, 1, MF_BYPOSITION);
assert_ne!(space1_state & MF_CHECKED, 0);
assert_eq!(space2_state & MF_CHECKED, 0);
DestroyMenu(hmenu);
}
}
#[test]
fn shortcut_suffix_format_matches_original() {
let entries = fixture();
let hmenu = build_hmenu(&entries);
unsafe {
let sub = GetSubMenu(hmenu, 3);
assert_eq!(menu_text(sub, 0), "Space 1 (Alt+1)");
assert_eq!(menu_text(sub, 3), "Remove Space 2");
DestroyMenu(hmenu);
}
}
}