#![allow(unsafe_code)]
mod input;
mod present;
mod window;
#[cfg(feature = "a11y")]
mod a11y;
use std::cell::{Cell, RefCell};
use std::ptr::null_mut;
use std::rc::Rc;
#[cfg(feature = "a11y")]
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Once;
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, S_OK, WPARAM};
use windows_sys::Win32::Graphics::Gdi::{
CreateBitmap, CreateDIBSection, DeleteObject, GetDC, GetMonitorInfoW, MonitorFromRect,
ReleaseDC, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HGDIOBJ, MONITORINFO,
MONITOR_DEFAULTTONEAREST,
};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::System::Registry::{RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD};
use windows_sys::Win32::UI::HiDpi::{
GetDpiForMonitor, GetDpiForSystem, GetDpiForWindow, MDT_EFFECTIVE_DPI,
};
use windows_sys::Win32::UI::Shell::{
Shell_NotifyIconGetRect, Shell_NotifyIconW, NIF_ICON, NIF_MESSAGE, NIF_STATE, NIF_TIP, NIM_ADD,
NIM_DELETE, NIM_MODIFY, NIS_HIDDEN, NOTIFYICONDATAW, NOTIFYICONIDENTIFIER,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateIconIndirect, CreateWindowExW, DefWindowProcW, DestroyIcon,
DestroyWindow, DispatchMessageW, GetMessageW, GetWindowLongPtrW, GetWindowRect, PostMessageW,
PostQuitMessage, RegisterClassW, RegisterWindowMessageW, SetWindowsHookExW, TranslateMessage,
UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, HHOOK, HICON, HWND_MESSAGE, ICONINFO,
KBDLLHOOKSTRUCT, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL, WM_ACTIVATEAPP, WM_APP,
WM_KEYDOWN, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_RBUTTONDOWN, WNDCLASSW,
};
use crate::anchor::place_popup;
use crate::error::{Error, Result};
use crate::flyout::{next_flyout, place_flyout, HoverTarget};
use crate::geometry::{Edge, LogicalPoint, LogicalRect, LogicalSize};
use crate::keynav::{handle_key, FlyoutFocus, MenuFocus, NavAction, NavKey};
use crate::menu::{Icon, Item, Menu, MenuId};
use crate::platform::{Appearance, Platform};
use crate::render::paint::{render_menu, LaidMenu};
use crate::render::RasterDrawer;
use crate::style::Color;
use crate::theme::{MenuOptions, OsFamily, Theme};
use crate::{Tray, TrayCommand};
const WM_TRAY_CALLBACK: u32 = WM_APP + 1;
const WM_MURI_DRAIN: u32 = WM_APP + 2;
const TRAY_ICON_UID: u32 = 0x0001;
const POPUP_TAG: isize = 1;
const FLYOUT_TAG: isize = 2;
static TRAY_CLASS_ONCE: Once = Once::new();
static POPUP_CLASS_ONCE: Once = Once::new();
static TASKBAR_CREATED_MSG: AtomicU32 = AtomicU32::new(0);
#[cfg(feature = "a11y")]
static A11Y_OWNER: AtomicIsize = AtomicIsize::new(0);
#[cfg(feature = "a11y")]
static A11Y_ACTIONS: std::sync::Mutex<Vec<(WindowKind, accesskit::ActionRequest)>> =
std::sync::Mutex::new(Vec::new());
thread_local! {
static MAIN_APP: RefCell<Option<Rc<RefCell<AppState>>>> = const { RefCell::new(None) };
static EVENTS: RefCell<Vec<UiEvent>> = const { RefCell::new(Vec::new()) };
static OWNER_HWND: Cell<isize> = const { Cell::new(0) };
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
pub(super) fn push_event(event: UiEvent) {
EVENTS.with(|e| e.borrow_mut().push(event));
let owner = OWNER_HWND.with(|h| h.get());
if owner != 0 {
unsafe {
PostMessageW(owner as HWND, WM_MURI_DRAIN, 0, 0);
}
}
}
#[cfg(feature = "a11y")]
pub(super) fn push_a11y_action(kind: WindowKind, request: accesskit::ActionRequest) {
if let Ok(mut q) = A11Y_ACTIONS.lock() {
q.push((kind, request));
}
let owner = A11Y_OWNER.load(Ordering::SeqCst);
if owner != 0 {
unsafe {
PostMessageW(owner as HWND, WM_MURI_DRAIN, 0, 0);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum WindowKind {
Popup,
Flyout(usize),
}
impl WindowKind {
fn menu_level(self) -> usize {
match self {
WindowKind::Popup => 0,
WindowKind::Flyout(depth) => depth + 1,
}
}
fn tag(self) -> isize {
match self {
WindowKind::Popup => POPUP_TAG,
WindowKind::Flyout(depth) => FLYOUT_TAG + depth as isize,
}
}
}
pub(super) enum UiEvent {
TrayClicked,
MouseMoved {
kind: WindowKind,
x: f32,
y: f32,
},
MouseClick {
kind: WindowKind,
x: f32,
y: f32,
},
Key(NavKey),
GlobalMouseDown {
x: i32,
y: i32,
},
AppDeactivated,
}
fn lparam_xy(lparam: LPARAM) -> (i32, i32) {
let x = (lparam & 0xFFFF) as i16 as i32;
let y = ((lparam >> 16) & 0xFFFF) as i16 as i32;
(x, y)
}
fn window_kind(hwnd: HWND) -> Option<WindowKind> {
match unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } {
POPUP_TAG => Some(WindowKind::Popup),
n if n >= FLYOUT_TAG => Some(WindowKind::Flyout((n - FLYOUT_TAG) as usize)),
_ => None,
}
}
unsafe extern "system" fn wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
WM_MURI_DRAIN => {
let app = MAIN_APP.with(|slot| slot.borrow().clone());
if let Some(app) = app {
if let Ok(mut state) = app.try_borrow_mut() {
state.drain();
}
}
0
}
WM_TRAY_CALLBACK => {
if (lparam & 0xFFFF) as u32 == WM_LBUTTONUP {
push_event(UiEvent::TrayClicked);
}
0
}
WM_MOUSEMOVE => {
if let Some(kind) = window_kind(hwnd) {
let (x, y) = to_logical_client(hwnd, lparam_xy(lparam));
push_event(UiEvent::MouseMoved { kind, x, y });
}
0
}
WM_LBUTTONUP => {
if let Some(kind) = window_kind(hwnd) {
let (x, y) = to_logical_client(hwnd, lparam_xy(lparam));
push_event(UiEvent::MouseClick { kind, x, y });
}
0
}
WM_ACTIVATEAPP => {
if wparam == 0 {
push_event(UiEvent::AppDeactivated);
}
0
}
_ if msg != 0 && msg == TASKBAR_CREATED_MSG.load(Ordering::SeqCst) => {
let app = MAIN_APP.with(|slot| slot.borrow().clone());
if let Some(app) = app {
if let Ok(mut state) = app.try_borrow_mut() {
if let Anchor::Tray(a) = &mut state.session.anchor {
let _ = a.readd();
}
}
}
0
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
fn to_logical_client(hwnd: HWND, (x, y): (i32, i32)) -> (f32, f32) {
let dpi = unsafe { GetDpiForWindow(hwnd) };
let scale = if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 };
(x as f32 / scale, y as f32 / scale)
}
unsafe extern "system" fn mouse_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
if code == HC_ACTION as i32 {
let msg = wparam as u32;
if msg == WM_LBUTTONDOWN || msg == WM_RBUTTONDOWN {
let data = &*(lparam as *const MSLLHOOKSTRUCT);
push_event(UiEvent::GlobalMouseDown {
x: data.pt.x,
y: data.pt.y,
});
}
}
CallNextHookEx(null_mut(), code, wparam, lparam)
}
unsafe extern "system" fn kbd_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
if code == HC_ACTION as i32 && wparam as u32 == WM_KEYDOWN {
let data = &*(lparam as *const KBDLLHOOKSTRUCT);
if let Some(key) = input::translate_vk(data.vkCode as u16) {
push_event(UiEvent::Key(key));
}
}
CallNextHookEx(null_mut(), code, wparam, lparam)
}
unsafe fn decode_hicon(icon: &Icon, size: i32) -> Option<HICON> {
let bytes = match icon {
Icon::Png(bytes) | Icon::Svg(bytes) => bytes,
_ => return None,
};
let (rgba, src_w, src_h) = crate::render::decode_icon_bytes(bytes)?;
let (sw, sh) = (src_w as i32, src_h as i32);
if sw == 0 || sh == 0 || size <= 0 {
return None;
}
let src = &rgba[..];
let screen_dc = GetDC(null_mut());
if screen_dc.is_null() {
return None;
}
let mut bmi: BITMAPINFO = std::mem::zeroed();
bmi.bmiHeader = BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: size,
biHeight: -size, biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
biSizeImage: 0,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
};
let mut bits: *mut core::ffi::c_void = null_mut();
let color = CreateDIBSection(screen_dc, &bmi, DIB_RGB_COLORS, &mut bits, null_mut(), 0);
ReleaseDC(null_mut(), screen_dc);
if color.is_null() || bits.is_null() {
if !color.is_null() {
DeleteObject(color as HGDIOBJ);
}
return None;
}
let dst = std::slice::from_raw_parts_mut(bits.cast::<u8>(), (size * size * 4) as usize);
for row in 0..size {
for col in 0..size {
let sx = (col * sw / size).clamp(0, sw - 1);
let sy = (row * sh / size).clamp(0, sh - 1);
let s = ((sy * sw + sx) * 4) as usize;
let d = ((row * size + col) * 4) as usize;
let a = src[s + 3];
let (r, g, b) = if a == 0 {
(0, 0, 0)
} else {
(src[s], src[s + 1], src[s + 2])
};
dst[d] = b;
dst[d + 1] = g;
dst[d + 2] = r;
dst[d + 3] = a;
}
}
let mask = CreateBitmap(size, size, 1, 1, std::ptr::null());
if mask.is_null() {
DeleteObject(color as HGDIOBJ);
return None;
}
let info = ICONINFO {
fIcon: 1,
xHotspot: 0,
yHotspot: 0,
hbmMask: mask,
hbmColor: color,
};
let hicon = CreateIconIndirect(&info);
DeleteObject(color as HGDIOBJ);
DeleteObject(mask as HGDIOBJ);
if hicon.is_null() {
None
} else {
Some(hicon)
}
}
#[derive(Clone, Copy)]
struct WinGeometry {
anchor: RECT,
work: RECT,
scale: f32,
}
impl WinGeometry {
fn anchor_rect_local(&self) -> LogicalRect {
self.rect_local(&self.anchor)
}
fn work_area_local(&self) -> LogicalRect {
self.rect_local(&self.work)
}
fn rect_local(&self, r: &RECT) -> LogicalRect {
LogicalRect::new(
LogicalPoint::new(r.left as f32 / self.scale, r.top as f32 / self.scale),
LogicalSize::new(
(r.right - r.left) as f32 / self.scale,
(r.bottom - r.top) as f32 / self.scale,
),
)
}
fn to_physical(self, origin: LogicalPoint) -> (i32, i32) {
(
(origin.x * self.scale).round() as i32,
(origin.y * self.scale).round() as i32,
)
}
fn for_rect(rect: LogicalRect) -> Option<WinGeometry> {
let sys_dpi = unsafe { GetDpiForSystem() };
let sys_scale = if sys_dpi == 0 {
1.0
} else {
sys_dpi as f32 / 96.0
};
let to_phys = |scale: f32| RECT {
left: (rect.origin.x * scale).round() as i32,
top: (rect.origin.y * scale).round() as i32,
right: ((rect.origin.x + rect.size.width) * scale).round() as i32,
bottom: ((rect.origin.y + rect.size.height) * scale).round() as i32,
};
let probe = to_phys(sys_scale);
let hmon = unsafe { MonitorFromRect(&probe, MONITOR_DEFAULTTONEAREST) };
if hmon.is_null() {
return None;
}
let mut dpi_x: u32 = 96;
let mut dpi_y: u32 = 96;
let scale = if unsafe { GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }
== S_OK
&& dpi_x != 0
{
dpi_x as f32 / 96.0
} else {
sys_scale
};
let anchor = to_phys(scale);
let mut mi: MONITORINFO = unsafe { std::mem::zeroed() };
mi.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
let work = if unsafe { GetMonitorInfoW(hmon, &mut mi) } != 0 {
mi.rcWork
} else {
RECT {
left: 0,
top: 0,
right: (1440.0 * scale) as i32,
bottom: (900.0 * scale) as i32,
}
};
Some(WinGeometry {
anchor,
work,
scale,
})
}
}
pub struct WindowsAnchor {
hwnd: HWND,
installed: bool,
tooltip: Option<String>,
hicon: HICON,
icon_bytes: Option<std::sync::Arc<[u8]>>,
}
impl std::fmt::Debug for WindowsAnchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowsAnchor")
.field("installed", &self.installed)
.finish()
}
}
impl Default for WindowsAnchor {
fn default() -> Self {
Self::new()
}
}
impl WindowsAnchor {
pub fn new() -> Self {
WindowsAnchor {
hwnd: null_mut(),
installed: false,
tooltip: None,
hicon: null_mut(),
icon_bytes: None,
}
}
unsafe fn create_message_window(&mut self) -> Result<()> {
let hinstance = GetModuleHandleW(null_mut());
let class_name = wide("muri_tray_msgwnd");
TRAY_CLASS_ONCE.call_once(|| unsafe {
let mut wc: WNDCLASSW = std::mem::zeroed();
wc.lpfnWndProc = Some(wnd_proc);
wc.hInstance = hinstance;
wc.lpszClassName = class_name.as_ptr();
RegisterClassW(&wc);
});
let hwnd = CreateWindowExW(
0,
class_name.as_ptr(),
null_mut(),
0,
0,
0,
0,
0,
HWND_MESSAGE,
null_mut(),
hinstance,
null_mut(),
);
if hwnd.is_null() {
return Err(Error::Platform(
"failed to create tray message window".into(),
));
}
self.hwnd = hwnd;
Ok(())
}
fn identifier(&self) -> NOTIFYICONIDENTIFIER {
let mut id: NOTIFYICONIDENTIFIER = unsafe { std::mem::zeroed() };
id.cbSize = std::mem::size_of::<NOTIFYICONIDENTIFIER>() as u32;
id.hWnd = self.hwnd;
id.uID = TRAY_ICON_UID;
id
}
fn base_nid(&self) -> NOTIFYICONDATAW {
let mut nid: NOTIFYICONDATAW = unsafe { std::mem::zeroed() };
nid.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
nid.hWnd = self.hwnd;
nid.uID = TRAY_ICON_UID;
nid
}
fn fill_tip(tip: &str, dst: &mut [u16; 128]) {
let src = wide(tip);
let n = src.len().min(dst.len() - 1);
dst[..n].copy_from_slice(&src[..n]);
}
unsafe fn resolve_hicon(&mut self, icon: &Icon) -> Result<HICON> {
if let Icon::Png(bytes) = icon {
if !self.hicon.is_null()
&& self
.icon_bytes
.as_ref()
.is_some_and(|b| std::sync::Arc::ptr_eq(b, bytes))
{
return Ok(self.hicon);
}
}
let new = match icon {
Icon::Png(_) => decode_hicon(icon, 16)
.ok_or_else(|| Error::BadIcon("could not decode PNG tray icon bytes".into()))?,
Icon::Svg(_) => decode_hicon(icon, 16)
.ok_or_else(|| Error::BadIcon("could not rasterize SVG tray icon bytes".into()))?,
_ => null_mut(),
};
if !self.hicon.is_null() {
DestroyIcon(self.hicon);
}
self.hicon = new;
self.icon_bytes = match icon {
Icon::Png(bytes) => Some(std::sync::Arc::clone(bytes)),
_ => None,
};
Ok(new)
}
fn install(&mut self, icon: &Icon, tooltip: Option<&str>) -> Result<()> {
unsafe {
let hicon = self.resolve_hicon(icon)?;
self.create_message_window()?;
self.tooltip = tooltip.map(str::to_owned);
let mut nid = self.base_nid();
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_TRAY_CALLBACK;
nid.hIcon = hicon;
if let Some(tip) = tooltip {
Self::fill_tip(tip, &mut nid.szTip);
}
if Shell_NotifyIconW(NIM_ADD, &nid) == 0 {
let _ = DestroyWindow(self.hwnd);
self.hwnd = null_mut();
return Err(Error::Platform("Shell_NotifyIcon(NIM_ADD) failed".into()));
}
self.installed = true;
}
Ok(())
}
fn readd(&mut self) -> Result<()> {
if self.hwnd.is_null() {
return Ok(());
}
unsafe {
let mut nid = self.base_nid();
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_TRAY_CALLBACK;
nid.hIcon = self.hicon;
if let Some(tip) = self.tooltip.clone() {
Self::fill_tip(&tip, &mut nid.szTip);
}
if Shell_NotifyIconW(NIM_ADD, &nid) == 0 {
return Err(Error::Platform("Shell_NotifyIcon re-add failed".into()));
}
self.installed = true;
}
Ok(())
}
fn set_icon(&mut self, icon: &Icon) {
if !self.installed {
return;
}
unsafe {
let Ok(hicon) = self.resolve_hicon(icon) else {
return;
};
let mut nid = self.base_nid();
nid.uFlags = NIF_ICON;
nid.hIcon = hicon;
Shell_NotifyIconW(NIM_MODIFY, &nid);
}
}
fn set_tooltip(&mut self, tooltip: Option<&str>) {
self.tooltip = tooltip.map(str::to_owned);
if !self.installed {
return;
}
unsafe {
let mut nid = self.base_nid();
nid.uFlags = NIF_TIP;
if let Some(tip) = tooltip {
Self::fill_tip(tip, &mut nid.szTip);
}
Shell_NotifyIconW(NIM_MODIFY, &nid);
}
}
fn set_visible(&self, visible: bool) {
if !self.installed {
return;
}
unsafe {
let mut nid = self.base_nid();
nid.uFlags = NIF_STATE;
nid.dwStateMask = NIS_HIDDEN;
nid.dwState = if visible { 0 } else { NIS_HIDDEN };
Shell_NotifyIconW(NIM_MODIFY, &nid);
}
}
fn geometry(&self) -> Option<WinGeometry> {
if !self.installed {
return None;
}
let dpi = unsafe { GetDpiForWindow(self.hwnd) };
let scale = if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 };
let id = self.identifier();
let mut anchor: RECT = unsafe { std::mem::zeroed() };
let ok = unsafe { Shell_NotifyIconGetRect(&id, &mut anchor) } == S_OK;
if !ok {
let mut pt: POINT = unsafe { std::mem::zeroed() };
if unsafe { windows_sys::Win32::UI::WindowsAndMessaging::GetCursorPos(&mut pt) } == 0 {
return None;
}
anchor = RECT {
left: pt.x,
top: pt.y,
right: pt.x + 1,
bottom: pt.y + 1,
};
}
let hmon = unsafe { MonitorFromRect(&anchor, MONITOR_DEFAULTTONEAREST) };
let mut mi: MONITORINFO = unsafe { std::mem::zeroed() };
mi.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
let work = if !hmon.is_null() && unsafe { GetMonitorInfoW(hmon, &mut mi) } != 0 {
mi.rcWork
} else {
RECT {
left: 0,
top: 0,
right: (1440.0 * scale) as i32,
bottom: (900.0 * scale) as i32,
}
};
Some(WinGeometry {
anchor,
work,
scale,
})
}
fn icon_rect_physical(&self) -> Option<RECT> {
if !self.installed {
return None;
}
let id = self.identifier();
let mut rect: RECT = unsafe { std::mem::zeroed() };
if unsafe { Shell_NotifyIconGetRect(&id, &mut rect) } == S_OK {
Some(rect)
} else {
None
}
}
fn anchor_rect(&self) -> Result<LogicalRect> {
self.geometry()
.map(|g| g.anchor_rect_local())
.ok_or_else(|| Error::Platform("tray icon not installed".into()))
}
fn work_area(&self) -> Result<LogicalRect> {
self.geometry()
.map(|g| g.work_area_local())
.ok_or_else(|| Error::Platform("tray icon not installed".into()))
}
fn supports_tray_anchor(&self) -> bool {
true
}
pub fn popup_origin(&self, popup: LogicalSize, work_area: LogicalRect) -> Result<LogicalPoint> {
let anchor = self.anchor_rect()?;
Ok(place_popup(anchor, popup, work_area, Edge::Top, 2.0))
}
}
impl Drop for WindowsAnchor {
fn drop(&mut self) {
unsafe {
if self.installed {
let nid = self.base_nid();
Shell_NotifyIconW(NIM_DELETE, &nid);
}
if !self.hicon.is_null() {
DestroyIcon(self.hicon);
}
if !self.hwnd.is_null() {
let _ = DestroyWindow(self.hwnd);
}
}
}
}
enum Anchor {
Tray(WindowsAnchor),
Fixed(WinGeometry),
}
impl Anchor {
fn geometry(&self) -> Option<WinGeometry> {
match self {
Anchor::Tray(a) => a.geometry(),
Anchor::Fixed(g) => Some(*g),
}
}
}
struct Panel {
hwnd: HWND,
drawer: RasterDrawer,
laid: Option<LaidMenu>,
cursor: LogicalPoint,
hovered: Option<usize>,
origin: LogicalPoint,
px: i32,
py: i32,
#[cfg(feature = "a11y")]
adapter: accesskit_windows::SubclassingAdapter,
#[cfg(feature = "a11y")]
snapshot: Rc<RefCell<a11y::A11ySnapshot>>,
}
impl Panel {
fn destroy(&self) {
unsafe {
DestroyWindow(self.hwnd);
}
}
fn phys_rect(&self) -> (i32, i32, i32, i32) {
let mut r: RECT = unsafe { std::mem::zeroed() };
if unsafe { GetWindowRect(self.hwnd, &mut r) } != 0 {
(r.left, r.top, r.right, r.bottom)
} else {
(0, 0, 0, 0)
}
}
}
struct Flyout {
parent: usize,
panel: Panel,
}
struct PopupSession<'a> {
menu: Menu,
options: MenuOptions,
dispatch: Box<dyn Fn(&MenuId) + 'a>,
anchor: Anchor,
edge: Edge,
hinstance: windows_sys::Win32::Foundation::HINSTANCE,
popup: Option<Panel>,
flyouts: Vec<Flyout>,
mouse_hook: HHOOK,
kbd_hook: HHOOK,
}
impl PopupSession<'_> {
fn theme(&self) -> Theme {
let mut theme = self
.options
.theme
.resolve(OsFamily::Windows, system_is_dark());
if self.options.theme.injects_system() {
if let Some((r, g, b, a)) = system_accent() {
theme.accent = Color::Rgba(r, g, b, a);
}
if let Some(font) = read_system_menu_font() {
font.apply_size_to(&mut theme);
}
if !transparency_enabled() {
theme.make_opaque();
}
}
theme
}
fn menu_at_level(&self, level: usize) -> Option<&Menu> {
crate::menu::descend(
&self.menu,
self.flyouts.iter().take(level).map(|f| f.parent),
)
}
fn panel(&self, kind: WindowKind) -> Option<&Panel> {
match kind {
WindowKind::Popup => self.popup.as_ref(),
WindowKind::Flyout(d) => self.flyouts.get(d).map(|f| &f.panel),
}
}
fn panel_mut(&mut self, kind: WindowKind) -> Option<&mut Panel> {
match kind {
WindowKind::Popup => self.popup.as_mut(),
WindowKind::Flyout(d) => self.flyouts.get_mut(d).map(|f| &mut f.panel),
}
}
fn flyout_stack(&self) -> Vec<usize> {
self.flyouts.iter().map(|f| f.parent).collect()
}
fn current_focus(&self) -> MenuFocus {
MenuFocus {
top: self.popup.as_ref().and_then(|p| p.hovered),
flyout: self
.flyouts
.iter()
.map(|f| FlyoutFocus {
parent: f.parent,
child: f.panel.hovered,
})
.collect(),
}
}
fn install_hooks(&mut self) {
if self.mouse_hook.is_null() {
self.mouse_hook =
unsafe { SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), self.hinstance, 0) };
}
if self.kbd_hook.is_null() {
self.kbd_hook = unsafe {
SetWindowsHookExW(WH_KEYBOARD_LL, Some(kbd_hook_proc), self.hinstance, 0)
};
}
}
fn remove_hooks(&mut self) {
if !self.mouse_hook.is_null() {
unsafe { UnhookWindowsHookEx(self.mouse_hook) };
self.mouse_hook = null_mut();
}
if !self.kbd_hook.is_null() {
unsafe { UnhookWindowsHookEx(self.kbd_hook) };
self.kbd_hook = null_mut();
}
}
fn open_popup(&mut self) {
if self.popup.is_some() {
return;
}
let theme = self.theme();
let Some(geom) = self.anchor.geometry() else {
return;
};
let scale = geom.scale.max(1.0);
let mut probe = RasterDrawer::new_native(scale);
let laid = render_menu(&mut probe, &self.menu, &theme, &self.options, None);
let origin = place_popup(
geom.anchor_rect_local(),
laid.size,
geom.work_area_local(),
self.edge,
2.0,
);
let (px, py) = geom.to_physical(origin);
let class = wide("muri_popup_wnd");
register_popup_class(self.hinstance, &class);
let hwnd = unsafe {
window::create_popup(class.as_ptr(), self.hinstance, WindowKind::Popup.tag())
};
if hwnd.is_null() {
return;
}
#[cfg(feature = "a11y")]
let snapshot = Rc::new(RefCell::new(a11y::A11ySnapshot {
menu: self.menu.clone(),
focus: MenuFocus {
top: None,
flyout: Vec::new(),
},
}));
#[cfg(feature = "a11y")]
let adapter = unsafe { a11y::make_adapter(hwnd, Rc::clone(&snapshot), WindowKind::Popup) };
self.popup = Some(Panel {
hwnd,
drawer: RasterDrawer::new_native(scale),
laid: Some(laid),
cursor: LogicalPoint::default(),
hovered: None,
origin,
px,
py,
#[cfg(feature = "a11y")]
adapter,
#[cfg(feature = "a11y")]
snapshot,
});
self.redraw(WindowKind::Popup);
unsafe { window::raise_topmost(hwnd) };
self.install_hooks();
self.sync_a11y();
}
fn push_flyout(&mut self, parent_index: usize) {
let depth = self.flyouts.len();
let Some(parent_menu) = self.menu_at_level(depth) else {
return;
};
let Some(child) = (match parent_menu.items.get(parent_index) {
Some(Item::Submenu { menu, .. }) => Some(menu.clone()),
_ => None,
}) else {
return;
};
let (parent_origin, parent_size, scale, row_rect) = {
let parent_panel = if depth == 0 {
self.popup.as_ref()
} else {
self.flyouts.get(depth - 1).map(|f| &f.panel)
};
let Some(pp) = parent_panel else {
return;
};
let Some(rect) = pp
.laid
.as_ref()
.and_then(|l| l.rows.iter().find(|r| r.index == parent_index))
.map(|r| r.rect)
else {
return;
};
(
pp.origin,
pp.laid.as_ref().map(|l| l.size).unwrap_or_default(),
pp.drawer.scale(),
rect,
)
};
let theme = self.theme();
let Some(geom) = self.anchor.geometry() else {
return;
};
let mut probe = RasterDrawer::new_native(scale);
let child_laid = render_menu(&mut probe, &child, &theme, &self.options, None);
let parent_rect = LogicalRect::new(parent_origin, parent_size);
let placement = place_flyout(
parent_rect,
row_rect,
child_laid.size,
geom.work_area_local(),
);
let (px, py) = geom.to_physical(placement.origin);
let kind = WindowKind::Flyout(depth);
let class = wide("muri_popup_wnd");
register_popup_class(self.hinstance, &class);
let hwnd = unsafe { window::create_popup(class.as_ptr(), self.hinstance, kind.tag()) };
if hwnd.is_null() {
return;
}
#[cfg(feature = "a11y")]
let snapshot = Rc::new(RefCell::new(a11y::A11ySnapshot {
menu: child.clone(),
focus: MenuFocus {
top: None,
flyout: Vec::new(),
},
}));
#[cfg(feature = "a11y")]
let adapter = unsafe { a11y::make_adapter(hwnd, Rc::clone(&snapshot), kind) };
self.flyouts.push(Flyout {
parent: parent_index,
panel: Panel {
hwnd,
drawer: RasterDrawer::new_native(scale),
laid: None,
cursor: LogicalPoint::default(),
hovered: None,
origin: placement.origin,
px,
py,
#[cfg(feature = "a11y")]
adapter,
#[cfg(feature = "a11y")]
snapshot,
},
});
self.redraw(kind);
unsafe { window::raise_topmost(hwnd) };
self.sync_a11y();
}
fn truncate_flyouts(&mut self, len: usize) {
while self.flyouts.len() > len {
if let Some(f) = self.flyouts.pop() {
f.panel.destroy();
}
}
}
fn apply_flyout_stack(&mut self, target: &[usize]) {
let common = common_flyout_prefix(&self.flyout_stack(), target);
self.truncate_flyouts(common);
for &parent in &target[common..] {
self.push_flyout(parent);
}
}
fn close_popup(&mut self) {
self.remove_hooks();
self.truncate_flyouts(0);
if let Some(popup) = self.popup.take() {
popup.destroy();
}
}
fn redraw(&mut self, kind: WindowKind) {
let theme = self.theme();
let options = self.options.clone();
let Some(menu) = crate::menu::descend(
&self.menu,
self.flyouts
.iter()
.take(kind.menu_level())
.map(|f| f.parent),
) else {
return;
};
let panel = match kind {
WindowKind::Popup => self.popup.as_mut(),
WindowKind::Flyout(d) => self.flyouts.get_mut(d).map(|f| &mut f.panel),
};
let Some(panel) = panel else {
return;
};
let laid = render_menu(&mut panel.drawer, menu, &theme, &options, panel.hovered);
unsafe {
present::present_layered(panel.hwnd, panel.drawer.framebuffer(), panel.px, panel.py)
};
panel.laid = Some(laid);
}
fn redraw_all(&mut self) {
self.redraw(WindowKind::Popup);
for d in 0..self.flyouts.len() {
self.redraw(WindowKind::Flyout(d));
}
}
fn set_cursor(&mut self, kind: WindowKind, pt: LogicalPoint) {
if let Some(p) = self.panel_mut(kind) {
p.cursor = pt;
}
}
fn on_cursor(&mut self, kind: WindowKind, pt: LogicalPoint) {
let (hovered, changed) = {
let Some(p) = self.panel_mut(kind) else {
return;
};
p.cursor = pt;
let h = p.laid.as_ref().and_then(|l| l.hit(pt));
let changed = h != p.hovered;
if changed {
p.hovered = h;
}
(h, changed)
};
if changed {
self.redraw(kind);
}
let panel_depth = kind.menu_level();
let level_menu = self.menu_at_level(panel_depth);
let target = match hovered {
Some(i)
if level_menu
.is_some_and(|m| matches!(m.items.get(i), Some(Item::Submenu { .. }))) =>
{
HoverTarget::ParentRow {
panel: panel_depth,
index: i,
}
}
Some(_) => HoverTarget::OtherRow { panel: panel_depth },
None => HoverTarget::Outside,
};
let next = next_flyout(&self.flyout_stack(), target);
self.apply_flyout_stack(&next);
self.sync_a11y();
}
fn on_click(&mut self, kind: WindowKind) {
let level = kind.menu_level();
let Some(menu) = self.menu_at_level(level) else {
return;
};
let (hit, id) = {
let Some(p) = self.panel(kind) else {
return;
};
(
p.laid.as_ref().and_then(|l| l.hit(p.cursor)),
p.laid.as_ref().and_then(|l| l.id_at(p.cursor)),
)
};
if let Some(i) = hit {
if matches!(menu.items.get(i), Some(Item::Submenu { .. })) {
self.truncate_flyouts(level);
self.push_flyout(i);
return;
}
}
if let Some(id) = id {
if !id.is_none() {
(self.dispatch)(&id);
}
self.close_popup();
}
}
fn on_key_nav(&mut self, key: NavKey) {
let mut focus = self.current_focus();
let action = handle_key(&self.menu, &mut focus, key);
if let Some(popup) = self.popup.as_mut() {
popup.hovered = focus.top;
}
match action {
NavAction::None => return,
NavAction::Redraw => {}
NavAction::OpenFlyout(i) => self.push_flyout(i),
NavAction::CloseFlyout => {
let keep = self.flyouts.len().saturating_sub(1);
self.truncate_flyouts(keep);
}
NavAction::Activate(id) => {
if !id.is_none() {
(self.dispatch)(&id);
}
self.close_popup();
return;
}
NavAction::CloseAll => {
self.close_popup();
return;
}
}
for (k, f) in self.flyouts.iter_mut().enumerate() {
if let Some(ff) = focus.flyout.get(k) {
f.panel.hovered = ff.child;
}
}
self.redraw_all();
self.sync_a11y();
}
fn on_global_mouse_down(&mut self, x: i32, y: i32) {
if self.popup.is_none() {
return;
}
let mut rects = Vec::with_capacity(1 + self.flyouts.len());
if let Some(p) = self.popup.as_ref() {
rects.push(p.phys_rect());
}
for f in &self.flyouts {
rects.push(f.panel.phys_rect());
}
if point_in_any(&rects, x, y) {
return;
}
if let Anchor::Tray(a) = &self.anchor {
if let Some(r) = a.icon_rect_physical() {
if x >= r.left && x < r.right && y >= r.top && y < r.bottom {
return;
}
}
}
self.close_popup();
}
#[cfg(feature = "a11y")]
fn is_submenu_at_path(&self, path: &[usize]) -> bool {
let mut menu = &self.menu;
for (k, &idx) in path.iter().enumerate() {
match menu.items.get(idx) {
Some(Item::Submenu { menu: child, .. }) => {
if k + 1 == path.len() {
return true;
}
menu = child;
}
_ => return false,
}
}
false
}
#[cfg(feature = "a11y")]
fn menu_id_at_path(&self, path: &[usize]) -> Option<MenuId> {
let mut menu = &self.menu;
for (k, &idx) in path.iter().enumerate() {
match menu.items.get(idx)? {
Item::Row(row) if k + 1 == path.len() => return Some(row.id.clone()),
Item::Submenu { menu: child, .. } => menu = child,
_ => return None,
}
}
None
}
#[cfg(feature = "a11y")]
fn sync_a11y(&mut self) {
let full = self.current_focus();
let root = &self.menu;
if let Some(popup) = self.popup.as_mut() {
a11y::sync(
&mut popup.adapter,
&popup.snapshot,
|| root.clone(),
MenuFocus {
top: full.top,
flyout: full.flyout.clone(),
},
);
}
let parents: Vec<usize> = self.flyouts.iter().map(|f| f.parent).collect();
for d in 0..self.flyouts.len() {
let Some(menu) = crate::menu::descend(&self.menu, parents[..=d].iter().copied()) else {
continue;
};
let top = full.flyout.get(d).and_then(|f| f.child);
let sub: Vec<FlyoutFocus> = full.flyout.iter().skip(d + 1).copied().collect();
if let Some(f) = self.flyouts.get_mut(d) {
a11y::sync(
&mut f.panel.adapter,
&f.panel.snapshot,
|| menu.clone(),
MenuFocus { top, flyout: sub },
);
}
}
}
#[cfg(not(feature = "a11y"))]
#[inline]
fn sync_a11y(&mut self) {}
#[cfg(feature = "a11y")]
fn on_a11y_action(&mut self, kind: WindowKind, request: accesskit::ActionRequest) {
let target = crate::a11y::AxId(request.target.0);
let level = kind.menu_level();
let Some(menu) = self.menu_at_level(level) else {
return;
};
let tree = crate::a11y::build_tree(menu);
let Some(rel_path) = crate::a11y::locate_path(&tree, target) else {
return;
};
let mut abs: Vec<usize> = self.flyouts.iter().take(level).map(|f| f.parent).collect();
abs.extend_from_slice(&rel_path);
self.apply_a11y(&abs, request.action);
}
#[cfg(feature = "a11y")]
fn apply_a11y(&mut self, abs: &[usize], action: accesskit::Action) {
use accesskit::Action;
if abs.is_empty() {
return;
}
match action {
Action::Focus => {
let target = &abs[..abs.len() - 1];
self.apply_flyout_stack(target);
let final_kind = if target.is_empty() {
WindowKind::Popup
} else {
WindowKind::Flyout(target.len() - 1)
};
if let (Some(&last), Some(p)) = (abs.last(), self.panel_mut(final_kind)) {
p.hovered = Some(last);
}
if let (Some(&first), Some(p)) = (abs.first(), self.popup.as_mut()) {
p.hovered = Some(first);
}
self.redraw_all();
self.sync_a11y();
}
Action::Click => {
if self.is_submenu_at_path(abs) {
self.apply_flyout_stack(abs);
if let (Some(&first), Some(p)) = (abs.first(), self.popup.as_mut()) {
p.hovered = Some(first);
}
self.sync_a11y();
return;
}
if let Some(id) = self.menu_id_at_path(abs) {
if !id.is_none() {
(self.dispatch)(&id);
}
}
self.close_popup();
}
_ => {}
}
}
fn apply_event(&mut self, event: UiEvent) {
match event {
UiEvent::TrayClicked => {
if self.popup.is_some() {
self.close_popup();
} else {
self.open_popup();
}
}
UiEvent::MouseMoved { kind, x, y } => {
self.on_cursor(kind, LogicalPoint::new(x, y));
}
UiEvent::MouseClick { kind, x, y } => {
self.set_cursor(kind, LogicalPoint::new(x, y));
self.on_click(kind);
}
UiEvent::Key(key) => self.on_key_nav(key),
UiEvent::GlobalMouseDown { x, y } => self.on_global_mouse_down(x, y),
UiEvent::AppDeactivated => {
if self.popup.is_some() {
self.close_popup();
}
}
}
}
#[cfg(feature = "a11y")]
fn drain_a11y_actions(&mut self) -> bool {
let actions = A11Y_ACTIONS
.lock()
.map(|mut q| std::mem::take(&mut *q))
.unwrap_or_default();
let any = !actions.is_empty();
for (kind, request) in actions {
self.on_a11y_action(kind, request);
}
any
}
fn drain_events(&mut self) {
loop {
let events = EVENTS.with(|e| std::mem::take(&mut *e.borrow_mut()));
let had_events = !events.is_empty();
for event in events {
self.apply_event(event);
}
#[cfg(feature = "a11y")]
let had_actions = self.drain_a11y_actions();
#[cfg(not(feature = "a11y"))]
let had_actions = false;
if !had_events && !had_actions {
break;
}
}
}
}
impl Drop for PopupSession<'_> {
fn drop(&mut self) {
self.close_popup();
}
}
struct AppState {
session: PopupSession<'static>,
tray: Tray,
}
impl AppState {
fn apply_command(&mut self, command: TrayCommand) {
match command {
TrayCommand::SetMenu(menu) => {
self.tray.menu = menu.clone();
self.session.menu = menu;
if self.session.popup.is_some() {
self.session.truncate_flyouts(0);
self.session.redraw(WindowKind::Popup);
self.session.sync_a11y();
}
}
TrayCommand::SetIcon(icon) => {
self.tray.icon = icon;
let icon = self.tray.icon.clone();
if let Anchor::Tray(a) = &mut self.session.anchor {
a.set_icon(&icon);
}
}
TrayCommand::SetTooltip(tooltip) => {
self.tray.tooltip = tooltip;
let tip = self.tray.tooltip.clone();
if let Anchor::Tray(a) = &mut self.session.anchor {
a.set_tooltip(tip.as_deref());
}
}
TrayCommand::SetTitle(title) => {
self.tray.title = title;
}
TrayCommand::SetVisible(visible) => {
if let Anchor::Tray(a) = &self.session.anchor {
a.set_visible(visible);
}
}
TrayCommand::Open => {
if self.session.popup.is_none() {
self.session.open_popup();
}
}
TrayCommand::Close => self.session.close_popup(),
TrayCommand::Shutdown => {
unsafe { PostQuitMessage(0) };
}
}
}
fn drain(&mut self) {
loop {
let events = EVENTS.with(|e| std::mem::take(&mut *e.borrow_mut()));
let commands: Vec<TrayCommand> = self
.tray
.commands
.lock()
.map(|mut q| std::mem::take(&mut *q))
.unwrap_or_default();
let had_work = !events.is_empty() || !commands.is_empty();
for command in commands {
self.apply_command(command);
}
for event in events {
self.session.apply_event(event);
}
#[cfg(feature = "a11y")]
let had_actions = self.session.drain_a11y_actions();
#[cfg(not(feature = "a11y"))]
let had_actions = false;
if !had_work && !had_actions {
break;
}
}
}
}
#[cfg(test)]
fn menu_at_stack<'a>(menu: &'a Menu, stack: &[usize]) -> Option<&'a Menu> {
crate::menu::descend(menu, stack.iter().copied())
}
fn common_flyout_prefix(current: &[usize], target: &[usize]) -> usize {
let mut common = 0;
while common < target.len() && common < current.len() && current[common] == target[common] {
common += 1;
}
common
}
fn point_in_any(rects: &[(i32, i32, i32, i32)], x: i32, y: i32) -> bool {
rects
.iter()
.any(|&(l, t, r, b)| x >= l && x < r && y >= t && y < b)
}
fn register_popup_class(hinstance: windows_sys::Win32::Foundation::HINSTANCE, class: &[u16]) {
POPUP_CLASS_ONCE.call_once(|| unsafe {
let mut wc: WNDCLASSW = std::mem::zeroed();
wc.lpfnWndProc = Some(wnd_proc);
wc.hInstance = hinstance;
wc.lpszClassName = class.as_ptr();
RegisterClassW(&wc);
});
}
fn read_system_menu_font() -> Option<crate::platform::SystemFont> {
use crate::platform::{SystemFont, SystemFontSource};
use windows_sys::Win32::UI::WindowsAndMessaging::{
SystemParametersInfoW, NONCLIENTMETRICSW, SPI_GETNONCLIENTMETRICS,
};
unsafe {
let mut ncm: NONCLIENTMETRICSW = std::mem::zeroed();
ncm.cbSize = std::mem::size_of::<NONCLIENTMETRICSW>() as u32;
let ok = SystemParametersInfoW(
SPI_GETNONCLIENTMETRICS,
ncm.cbSize,
(&mut ncm as *mut NONCLIENTMETRICSW).cast(),
0,
);
if ok == 0 {
return None;
}
let lf = ncm.lfMenuFont;
let len = lf
.lfFaceName
.iter()
.position(|&c| c == 0)
.unwrap_or(lf.lfFaceName.len());
let family = String::from_utf16_lossy(&lf.lfFaceName[..len]);
if family.is_empty() {
return None;
}
let point_size = if lf.lfHeight < 0 {
(-lf.lfHeight as f32) * 72.0 / 96.0
} else {
0.0
};
Some(SystemFont {
source: SystemFontSource::Family(family),
point_size,
})
}
}
fn system_accent() -> Option<(u8, u8, u8, u8)> {
use windows_sys::Win32::Graphics::Dwm::DwmGetColorizationColor;
unsafe {
let mut color: u32 = 0;
let mut opaque: i32 = 0;
if DwmGetColorizationColor(&mut color, &mut opaque) != 0 {
return None;
}
let a = ((color >> 24) & 0xff) as u8;
let r = ((color >> 16) & 0xff) as u8;
let g = ((color >> 8) & 0xff) as u8;
let b = (color & 0xff) as u8;
Some((r, g, b, if a == 0 { 255 } else { a }))
}
}
fn system_is_dark() -> bool {
read_personalize_dword("AppsUseLightTheme", 1) == 0
}
fn transparency_enabled() -> bool {
read_personalize_dword("EnableTransparency", 1) != 0
}
fn read_personalize_dword(value_name: &str, default: u32) -> u32 {
unsafe {
let subkey = wide("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
let value = wide(value_name);
let mut data: u32 = default;
let mut size = std::mem::size_of::<u32>() as u32;
let rc = RegGetValueW(
HKEY_CURRENT_USER,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_DWORD,
null_mut(),
(&mut data as *mut u32).cast(),
&mut size,
);
if rc == 0 {
data
} else {
default
}
}
}
#[derive(Debug, Default)]
pub struct WindowsPlatform {
anchor: WindowsAnchor,
}
impl WindowsPlatform {
pub fn new() -> Self {
WindowsPlatform {
anchor: WindowsAnchor::new(),
}
}
}
impl Platform for WindowsPlatform {
fn install_tray(&mut self, icon: &Icon, tooltip: Option<&str>) -> Result<()> {
self.anchor.install(icon, tooltip)
}
fn tray_anchor_rect(&self) -> Result<LogicalRect> {
self.anchor.anchor_rect()
}
fn supports_tray_anchor(&self) -> bool {
self.anchor.supports_tray_anchor()
}
fn appearance(&self) -> Appearance {
Appearance::from_is_dark(system_is_dark())
}
fn system_menu_font(&self) -> Option<crate::platform::SystemFont> {
read_system_menu_font()
}
fn work_area(&self) -> LogicalRect {
self.anchor.work_area().unwrap_or_else(|_| {
LogicalRect::new(LogicalPoint::new(0.0, 0.0), LogicalSize::new(1440.0, 900.0))
})
}
fn run_tray(self, tray: Tray) -> Result<()> {
run_event_loop(tray)
}
fn spawn_tray(self, tray: Tray) -> Result<()> {
super::spawn_tray_thread(tray, run_event_loop)
}
fn open_popup_session(
&mut self,
menu: Menu,
options: MenuOptions,
on_click: &(dyn Fn(&MenuId) + '_),
anchor: LogicalRect,
edge: Edge,
) -> Result<()> {
run_popup_session(menu, options, on_click, anchor, edge)
}
}
fn run_event_loop(mut tray: Tray) -> Result<()> {
let hinstance = unsafe { GetModuleHandleW(null_mut()) };
let taskbar_msg = unsafe { RegisterWindowMessageW(wide("TaskbarCreated").as_ptr()) };
TASKBAR_CREATED_MSG.store(taskbar_msg, Ordering::SeqCst);
let mut anchor = WindowsAnchor::new();
anchor.install(&tray.icon, tray.tooltip.as_deref())?;
let owner = anchor.hwnd as isize;
OWNER_HWND.with(|h| h.set(owner));
#[cfg(feature = "a11y")]
A11Y_OWNER.store(owner, Ordering::SeqCst);
if let Ok(mut waker) = tray.waker.lock() {
*waker = Some(Box::new(move || unsafe {
PostMessageW(owner as HWND, WM_MURI_DRAIN, 0, 0);
}));
}
let dispatch: Box<dyn Fn(&MenuId) + 'static> = match tray.on_click.take() {
Some(handler) => Box::new(move |id| {
handler(id);
crate::event::emit(id.clone());
}),
None => Box::new(|id| crate::event::emit(id.clone())),
};
let session = PopupSession {
menu: tray.menu.clone(),
options: tray.options.clone(),
dispatch,
anchor: Anchor::Tray(anchor),
edge: Edge::Top,
hinstance,
popup: None,
flyouts: Vec::new(),
mouse_hook: null_mut(),
kbd_hook: null_mut(),
};
let waker_arc = std::sync::Arc::clone(&tray.waker);
let state = Rc::new(RefCell::new(AppState { session, tray }));
MAIN_APP.with(|slot| *slot.borrow_mut() = Some(Rc::clone(&state)));
push_drain(owner as HWND);
unsafe {
let mut msg: MSG = std::mem::zeroed();
while GetMessageW(&mut msg, null_mut(), 0, 0) > 0 {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
MAIN_APP.with(|slot| *slot.borrow_mut() = None);
OWNER_HWND.with(|h| h.set(0));
if let Ok(mut waker) = waker_arc.lock() {
*waker = None;
}
#[cfg(feature = "a11y")]
A11Y_OWNER.store(0, Ordering::SeqCst);
Ok(())
}
fn push_drain(owner: HWND) {
unsafe {
PostMessageW(owner, WM_MURI_DRAIN, 0, 0);
}
}
fn run_popup_session(
menu: Menu,
options: MenuOptions,
on_click: &(dyn Fn(&MenuId) + '_),
anchor: LogicalRect,
edge: Edge,
) -> Result<()> {
let hinstance = unsafe { GetModuleHandleW(null_mut()) };
let geom = WinGeometry::for_rect(anchor)
.ok_or_else(|| Error::Platform("no monitor available for the popup".into()))?;
let mut session = PopupSession {
menu,
options,
dispatch: Box::new(move |id| on_click(id)),
anchor: Anchor::Fixed(geom),
edge,
hinstance,
popup: None,
flyouts: Vec::new(),
mouse_hook: null_mut(),
kbd_hook: null_mut(),
};
session.open_popup();
let Some(popup_hwnd) = session.popup.as_ref().map(|p| p.hwnd) else {
return Err(Error::Platform("failed to open the popup window".into()));
};
let prev_owner = OWNER_HWND.with(|h| h.replace(popup_hwnd as isize));
#[cfg(feature = "a11y")]
let prev_a11y_owner = A11Y_OWNER.swap(popup_hwnd as isize, Ordering::SeqCst);
unsafe {
let mut msg: MSG = std::mem::zeroed();
while session.popup.is_some() {
let got = GetMessageW(&mut msg, null_mut(), 0, 0);
if got <= 0 {
break;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
session.drain_events();
}
}
session.close_popup();
OWNER_HWND.with(|h| h.set(prev_owner));
#[cfg(feature = "a11y")]
A11Y_OWNER.store(prev_a11y_owner, Ordering::SeqCst);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn point_in_any_matches_windows_rects() {
let rects = [(100, 200, 300, 500), (300, 220, 460, 420)];
assert!(point_in_any(&rects, 150, 250));
assert!(point_in_any(&rects, 400, 300));
assert!(!point_in_any(&rects, 150, 100));
assert!(!point_in_any(&rects, 500, 300));
assert!(!point_in_any(&[(0, 0, 10, 10)], 10, 5));
assert!(!point_in_any(&[(0, 0, 10, 10)], 5, 10));
assert!(point_in_any(&[(0, 0, 10, 10)], 0, 0));
}
#[test]
fn flyout_reconcile_switches_to_shallower_sibling() {
let current = [1, 2, 3];
let target = [1, 5];
let common = common_flyout_prefix(¤t, &target);
assert_eq!(common, 1, "only the shared level-0 parent is kept");
assert_eq!(¤t[common..], &[2, 3]);
assert_eq!(&target[common..], &[5]);
}
#[test]
fn flyout_reconcile_rehovering_open_parent_keeps_children() {
let current = [1, 2];
let target = [1, 2];
let common = common_flyout_prefix(¤t, &target);
assert_eq!(common, 2);
assert!(current[common..].is_empty(), "no deeper level is closed");
assert!(target[common..].is_empty(), "no level is re-pushed");
}
#[test]
fn flyout_reconcile_switching_top_sibling_and_collapsing() {
assert_eq!(common_flyout_prefix(&[1, 2], &[5]), 0);
assert_eq!(common_flyout_prefix(&[1], &[1, 2]), 1);
assert_eq!(common_flyout_prefix(&[1, 2, 3], &[]), 0);
assert_eq!(common_flyout_prefix(&[], &[1, 2]), 0);
}
#[test]
fn geometry_maps_logical_to_physical_round_trip() {
let geom = WinGeometry {
anchor: RECT {
left: 1800,
top: 1400,
right: 1832,
bottom: 1432,
},
work: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2100,
},
scale: 2.0,
};
let wa = geom.work_area_local();
assert_eq!(wa.size.width, 1920.0);
assert_eq!(wa.size.height, 1050.0);
let (px, py) = geom.to_physical(LogicalPoint::new(100.0, 50.0));
assert_eq!((px, py), (200, 100));
let anchor = geom.anchor_rect_local();
assert_eq!(anchor.origin.y, 700.0);
}
#[test]
fn fill_tip_always_nul_terminates_when_overlong() {
let long = "A".repeat(200);
let mut buf = [0u16; 128];
WindowsAnchor::fill_tip(&long, &mut buf);
assert_eq!(
buf[buf.len() - 1],
0,
"the last szTip slot must remain a NUL terminator"
);
assert_eq!(buf[0], u16::from(b'A'));
assert_eq!(buf[126], u16::from(b'A'));
assert_ne!(buf[126], 0);
}
#[test]
fn fill_tip_terminates_a_short_tooltip_too() {
let mut buf = [0u16; 128];
WindowsAnchor::fill_tip("Hi", &mut buf);
assert_eq!(buf[0], u16::from(b'H'));
assert_eq!(buf[1], u16::from(b'i'));
assert_eq!(buf[2], 0);
assert_eq!(buf[127], 0);
}
#[test]
fn nested_submenu_opens_deep_flyout_and_dispatches_deep_leaf() {
use crate::menu::Row;
let menu = Menu::new()
.row(Row::new("a").label("Apple"))
.submenu(
Row::new("outer").label("Outer"),
Menu::new()
.row(Row::new("inner_leaf").label("Inner Leaf"))
.row(Row::new("inner_leaf2").label("Inner Leaf 2"))
.submenu(
Row::new("middle").label("Middle"),
Menu::new().row(Row::new("deep_leaf").label("Deep Leaf")),
),
)
.row(Row::new("quit").label("Quit"));
let outer = 1; let middle = 2;
assert_eq!(menu_at_stack(&menu, &[]).unwrap().items.len(), 3);
let level1 = menu_at_stack(&menu, &[outer]).expect("outer is a submenu");
assert!(
matches!(level1.items.get(middle), Some(Item::Submenu { .. })),
"clicking the nested submenu row must open a deeper flyout"
);
assert!(
menu_at_stack(&menu, &[middle]).is_none(),
"top-level resolution of a flyout-local submenu index must fail"
);
let deepest = menu_at_stack(&menu, &[outer, middle]).expect("middle is a submenu");
match deepest.items.first() {
Some(Item::Row(row)) => assert_eq!(row.id, MenuId::from("deep_leaf")),
other => panic!("expected the deep leaf row, got {other:?}"),
}
let stack_after_hover = next_flyout(
&[outer],
HoverTarget::ParentRow {
panel: 1,
index: middle,
},
);
assert_eq!(stack_after_hover, vec![outer, middle]);
let mut focus = MenuFocus {
top: Some(outer),
flyout: vec![FlyoutFocus {
parent: outer,
child: Some(middle),
}],
};
let action = handle_key(&menu, &mut focus, NavKey::Right);
assert_eq!(action, NavAction::OpenFlyout(middle));
let parents: Vec<usize> = focus.flyout.iter().map(|f| f.parent).collect();
assert_eq!(
parents,
vec![outer, middle],
"keyboard descent must build the same 2-level stack the mouse does"
);
let deepest = menu_at_stack(&menu, &parents).expect("keyboard stack resolves");
match deepest.items.first() {
Some(Item::Row(row)) => assert_eq!(row.id, MenuId::from("deep_leaf")),
other => panic!("expected the deep leaf row, got {other:?}"),
}
}
}