use std::{
cell::{Cell, Ref, RefCell, RefMut},
mem::size_of,
rc::{Rc, Weak},
time::Duration,
};
use crate::shell::{
api_model::{PopupMenuRequest, PopupMenuResponse},
Context, IPoint, IRect,
};
use super::{
all_bindings::*,
error::PlatformResult,
menu::PlatformMenu,
util::{GET_X_LPARAM, GET_Y_LPARAM, HIWORD, MAKELONG},
window_base::WindowBaseState,
};
pub trait WindowMenuDelegate {
fn get_state(&self) -> Ref<WindowBaseState>;
}
pub struct WindowMenu {
context: Rc<Context>,
hwnd: HWND,
child_hwnd: HWND,
delegate: Option<Weak<dyn WindowMenuDelegate>>,
current_menu: RefCell<Option<MenuState>>,
mouse_state: RefCell<MouseState>,
}
struct MouseState {
ignore_mouse_leave: bool,
}
struct MenuState {
platform_menu: Rc<PlatformMenu>,
request: PopupMenuRequest,
mouse_in: bool,
current_item_is_first: bool,
current_item_is_last: bool,
seen_key_down: bool,
seen_mouse_down: bool,
menu_hwnd: HWND,
}
thread_local! {
static POPUP_PARENT: Cell<HWND> = Cell::new(HWND(0));
}
impl WindowMenu {
pub fn new(
context: Rc<Context>,
hwnd: HWND,
child_hwnd: HWND,
delegate: Weak<dyn WindowMenuDelegate>,
) -> Self {
Self {
context,
hwnd,
child_hwnd,
delegate: Some(delegate),
current_menu: RefCell::new(None),
mouse_state: RefCell::new(MouseState {
ignore_mouse_leave: false,
}),
}
}
fn delegate(&self) -> Rc<dyn WindowMenuDelegate> {
self.delegate.as_ref().and_then(|d| d.upgrade()).unwrap()
}
pub fn hide_popup(&self, menu: Rc<PlatformMenu>) {
if let Some(current_menu) = self.current_menu.borrow().as_ref() {
if current_menu.platform_menu.handle == menu.handle {
unsafe {
EndMenu();
}
}
}
}
pub fn show_popup<F>(&self, menu: Rc<PlatformMenu>, request: PopupMenuRequest, on_done: F)
where
F: FnOnce(PlatformResult<PopupMenuResponse>) + 'static,
{
let hook = unsafe {
SetWindowsHookExW(
WH_MSGFILTER,
Some(Self::hook_proc),
HINSTANCE(0),
GetCurrentThreadId(),
)
};
self.current_menu.borrow_mut().replace(MenuState {
platform_menu: menu.clone(),
request: request.clone(),
mouse_in: false,
current_item_is_first: true,
current_item_is_last: true,
seen_key_down: false,
seen_mouse_down: false,
menu_hwnd: HWND(0),
});
let position = self
.delegate()
.get_state()
.local_to_global(&request.position);
self.mouse_state.borrow_mut().ignore_mouse_leave = true;
let mut params = {
if let Some(item_rect) = request.item_rect.as_ref() {
let top_left = self
.delegate()
.get_state()
.local_to_global(&item_rect.top_left());
let bottom_right = self
.delegate()
.get_state()
.local_to_global(&item_rect.bottom_right());
Some(TPMPARAMS {
cbSize: size_of::<TPMPARAMS>() as u32,
rcExclude: RECT {
left: top_left.x,
top: top_left.y,
right: bottom_right.x,
bottom: bottom_right.y,
},
})
} else {
None
}
};
POPUP_PARENT.with(|parent| {
parent.set(self.hwnd);
});
let res = unsafe {
let res = TrackPopupMenuEx(
menu.menu,
(TPM_LEFTALIGN | TPM_TOPALIGN | TPM_VERTICAL | TPM_RETURNCMD).0,
position.x,
position.y,
self.hwnd,
match &mut params {
Some(params) => params as *mut _,
None => std::ptr::null_mut(),
},
);
UnhookWindowsHookEx(hook);
self.track_mouse_leave();
res.0
};
POPUP_PARENT.with(|parent| {
parent.set(HWND(0));
});
if res > 0 {
self.context.menu_manager.borrow().on_menu_action(
self.current_menu.borrow().as_ref().unwrap().request.handle,
res as i64,
);
}
self.current_menu.borrow_mut().take();
self.mouse_state.borrow_mut().ignore_mouse_leave = false;
on_done(Ok(PopupMenuResponse {
item_selected: res != 0,
}));
}
const WM_MENU_HOOK: u32 = WM_USER;
const WM_MENU_HWND: u32 = WM_USER + 1;
extern "system" fn hook_proc(code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
unsafe {
let ptr = l_param.0 as *const MSG;
let msg: &MSG = &*ptr;
if code == MSGF_MENU as i32 {
let mut parent = GetParent(msg.hwnd);
if parent.0 == 0 {
parent = msg.hwnd;
}
if msg.message == WM_PAINT as u32 {
POPUP_PARENT.with(|parent| {
SendMessageW(
parent.get(),
Self::WM_MENU_HWND as u32,
WPARAM(msg.hwnd.0 as usize),
LPARAM(0),
);
});
}
SendMessageW(parent, Self::WM_MENU_HOOK as u32, w_param, l_param);
}
CallNextHookEx(HHOOK(0), code, w_param, l_param)
}
}
pub fn on_subclass_proc(
&self,
_h_wnd: HWND,
u_msg: u32,
_w_param: WPARAM,
_l_param: LPARAM,
) -> Option<LRESULT> {
let mouse_state = self.mouse_state.borrow_mut();
if u_msg == WM_MOUSELEAVE as u32 && mouse_state.ignore_mouse_leave {
return Some(LRESULT(0));
}
None
}
unsafe fn preselect_first_enabled_item(menu_hwnd: HWND, menu: HMENU) {
for i in 0..GetMenuItemCount(menu) {
SendMessageW(
menu_hwnd,
WM_KEYDOWN as u32,
WPARAM(VK_DOWN as usize),
LPARAM(0),
);
let mut item_info = MENUITEMINFOW {
cbSize: std::mem::size_of::<MENUITEMINFOW>() as u32,
fMask: MIIM_STATE,
..Default::default()
};
GetMenuItemInfoW(menu, i as u32, true, &mut item_info as *mut _);
if item_info.fState & MFS_DISABLED == MENU_ITEM_STATE(0) {
break;
}
}
}
fn on_menu_hwnd(&self, menu_hwnd: HWND) {
let mut menu = self.current_menu.borrow_mut();
let menu = menu.as_mut();
if let Some(menu) = menu {
menu.menu_hwnd = menu_hwnd;
if menu.request.preselect_first {
let hmenu = menu.platform_menu.menu;
self.context
.run_loop
.borrow()
.schedule_now(move || unsafe {
Self::preselect_first_enabled_item(menu_hwnd, hmenu);
})
.detach();
}
}
}
fn on_menu_hook(&self, mut msg: MSG) {
if self.current_menu.borrow().is_none() {
return;
}
let message = msg.message;
let mut current_menu = RefMut::map(self.current_menu.borrow_mut(), |x| x.as_mut().unwrap());
if message == WM_LBUTTONDOWN || message == WM_RBUTTONDOWN {
current_menu.seen_mouse_down = true;
}
if message == WM_KEYDOWN {
current_menu.seen_key_down = true;
}
if (message == WM_LBUTTONUP || message == WM_RBUTTONUP) && !current_menu.seen_mouse_down {
unsafe {
SendMessageW(self.child_hwnd, msg.message, msg.wParam, msg.lParam);
}
}
if (message == WM_KEYUP || message == WM_SYSKEYUP) && !current_menu.seen_key_down {
unsafe {
SendMessageW(self.child_hwnd, WM_KEYUP as u32, msg.wParam, msg.lParam);
}
}
if message >= WM_MOUSEFIRST && message <= WM_MOUSELAST {
let point = IPoint::xy(GET_X_LPARAM(msg.lParam), GET_Y_LPARAM(msg.lParam));
let point = self.delegate().get_state().global_to_local_physical(&point);
msg.lParam = LPARAM(MAKELONG(point.x as u16, point.y as u16) as isize);
if let Some(rect) = ¤t_menu.request.tracking_rect {
let scaled: IRect = rect
.scaled(self.delegate().get_state().get_scaling_factor())
.into();
if scaled.is_inside(&point) {
if !current_menu.mouse_in {
current_menu.mouse_in = true;
}
unsafe {
SendMessageW(self.child_hwnd, msg.message, msg.wParam, msg.lParam);
}
} else {
self.send_mouse_leave(&mut current_menu);
}
}
} else if message == WM_KEYDOWN {
let key = msg.wParam.0 as u32;
let (key_prev, key_next) = match self.delegate().get_state().is_rtl() {
true => (VK_RIGHT, VK_LEFT),
false => (VK_LEFT, VK_RIGHT),
};
if key == key_prev && current_menu.current_item_is_first {
self.context
.menu_manager
.borrow()
.move_to_previous_menu(current_menu.platform_menu.handle);
} else if key == key_next && current_menu.current_item_is_last {
self.context
.menu_manager
.borrow()
.move_to_next_menu(current_menu.platform_menu.handle);
}
}
}
fn send_mouse_leave(&self, current_menu: &mut RefMut<MenuState>) {
if current_menu.mouse_in {
current_menu.mouse_in = false;
self.mouse_state.borrow_mut().ignore_mouse_leave = false;
unsafe {
SendMessageW(self.child_hwnd, WM_MOUSELEAVE as u32, WPARAM(1), LPARAM(0));
}
self.mouse_state.borrow_mut().ignore_mouse_leave = true;
}
}
unsafe fn track_mouse_leave(&self) {
let hwnd = self.child_hwnd;
self.context
.run_loop
.borrow()
.schedule(
Duration::from_millis(50),
move || {
let mut event = TRACKMOUSEEVENT {
cbSize: size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_LEAVE,
hwndTrack: hwnd,
dwHoverTime: 0,
};
TrackMouseEvent(&mut event as *mut _);
},
)
.detach();
}
pub fn on_menu_select(&self, _msg: u32, w_param: WPARAM, l_param: LPARAM) {
if self.current_menu.borrow().is_none() {
return;
}
let mut current_menu = RefMut::map(self.current_menu.borrow_mut(), |x| x.as_mut().unwrap());
let menu = HMENU(l_param.0);
let flags = HIWORD(w_param.0 as u32) as u32;
current_menu.current_item_is_first = menu == current_menu.platform_menu.menu;
current_menu.current_item_is_last =
flags & MF_POPUP.0 == 0 || flags & MF_MOUSESELECT.0 == MF_MOUSESELECT.0;
}
pub fn handle_message(
&self,
_h_wnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> Option<LRESULT> {
match msg {
WM_MENUSELECT => {
self.on_menu_select(msg, w_param, l_param);
}
Self::WM_MENU_HOOK => {
let ptr = l_param.0 as *const MSG;
let msg: &MSG = unsafe { &*ptr };
self.on_menu_hook(*msg);
}
Self::WM_MENU_HWND => {
self.on_menu_hwnd(HWND(w_param.0 as isize));
}
_ => {}
}
None
}
}