use super::{
create_write_factory,
image::{get_icon_space, get_text_metrics},
IconSpace, MenuData, MenuItem, Size, CORNER_RADIUS, DEFAULT_ICON_MARGIN, MIN_BUTTON_WIDTH,
};
use crate::{
config::{hex_from_rgb, rgba_from_hex, ColorScheme, Config, Corner, Theme},
MenuItemType,
};
use std::{
mem::{size_of, transmute},
os::windows::ffi::OsStrExt,
sync::LazyLock,
};
use windows::{
core::{w, Error, PCSTR, PCWSTR},
Win32::{
Foundation::{FreeLibrary, COLORREF, HMODULE, HWND},
Globalization::lstrlenW,
Graphics::{
DirectWrite::IDWriteFactory,
Dwm::{DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_COLOR_NONE},
},
System::{
Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED},
LibraryLoader::{GetProcAddress, LoadLibraryW},
},
UI::WindowsAndMessaging::{GetWindowLongPtrW, SetWindowLongPtrW, GWL_USERDATA},
},
UI::ViewManagement::{UIColorType, UISettings},
};
static HUXTHEME: LazyLock<isize> = LazyLock::new(|| unsafe { LoadLibraryW(w!("uxtheme.dll")).unwrap_or_default().0 as _ });
macro_rules! hwnd {
($expression:expr) => {
windows::Win32::Foundation::HWND($expression as isize as *mut std::ffi::c_void)
};
}
pub(crate) use hwnd;
macro_rules! vtoi {
($expression:expr) => {
$expression as *mut std::ffi::c_void as isize
};
}
pub(crate) use vtoi;
pub(crate) fn free_library() {
let _ = unsafe { FreeLibrary(HMODULE(*HUXTHEME as _)) };
}
pub(crate) fn clear_userdata(hwnd: HWND) {
unsafe { SetWindowLongPtrW(hwnd, GWL_USERDATA, 0) };
}
pub(crate) fn is_userdata_avive(hwnd: HWND) -> bool {
let userdata = unsafe { GetWindowLongPtrW(hwnd, GWL_USERDATA) };
userdata == 0
}
pub(crate) fn get_menu_data<'a>(window_handle: isize) -> &'a MenuData {
let userdata = unsafe { GetWindowLongPtrW(hwnd!(window_handle), GWL_USERDATA) };
let item_data_ptr = userdata as *const MenuData;
unsafe { &*item_data_ptr }
}
pub(crate) fn get_menu_data_mut<'a>(window_handle: isize) -> &'a mut MenuData {
let userdata = unsafe { GetWindowLongPtrW(hwnd!(window_handle), GWL_USERDATA) };
let item_data_ptr = userdata as *mut MenuData;
unsafe { &mut *item_data_ptr }
}
pub(crate) fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
string.as_ref().encode_wide().chain(std::iter::once(0)).collect()
}
#[allow(dead_code)]
pub(crate) fn decode_wide(wide: &[u16]) -> String {
let len = unsafe { lstrlenW(PCWSTR::from_raw(wide.as_ptr())) } as usize;
let w_str_slice = unsafe { std::slice::from_raw_parts(wide.as_ptr(), len) };
String::from_utf16_lossy(w_str_slice)
}
#[allow(dead_code)]
#[allow(non_snake_case)]
pub(crate) fn LOWORD(dword: u32) -> u16 {
(dword & 0xFFFF) as u16
}
#[allow(dead_code)]
#[allow(non_snake_case)]
pub(crate) fn HIWORD(dword: u32) -> u16 {
((dword & 0xFFFF_0000) >> 16) as u16
}
pub(crate) fn toggle_radio(data: &mut MenuData, index: usize) {
data.items[index].checked = true;
for i in 0..data.items.len() {
if data.items[i].menu_item_type == MenuItemType::Radio && data.items[i].name == data.items[index].name && i != index {
data.items[i].checked = false;
}
}
}
pub(crate) fn calculate(items: &mut [MenuItem], config: &Config, theme: Theme, icon_space: IconSpace) -> Result<Size, Error> {
let mut width = 0;
let mut height = 0;
height += config.size.vertical_padding;
height += config.size.border_size;
if config.corner == Corner::Round {
height += CORNER_RADIUS;
}
let factory = create_write_factory()?;
let mut widest_accel = (0.0, "");
let mut cloned_items = Vec::new();
items.clone_into(&mut cloned_items);
let accels = cloned_items.iter().map(|i| i.accelerator.as_str());
for accel in accels {
if !accel.is_empty() {
let mut raw_text = encode_wide(accel);
let metrics = get_text_metrics(&factory, theme, config, &mut raw_text)?;
if metrics.width >= widest_accel.0 {
widest_accel = (metrics.width, accel);
}
}
}
for (index, item) in items.iter_mut().enumerate() {
item.index = index as u32;
if !item.visible {
item.top = -1;
item.left = -1;
item.bottom = -1;
item.right = -1;
continue;
}
item.top = height;
item.left = config.size.border_size + config.size.horizontal_padding;
let (item_width, item_height) = measure_item(&factory, config, item, theme, icon_space, widest_accel.1)?;
item.bottom = item.top + item_height;
width = std::cmp::max(width, item_width);
height += item_height;
}
for item in items {
if item.visible {
item.right = item.left + width;
}
}
width += config.size.horizontal_padding * 2;
height += config.size.vertical_padding;
if config.corner == Corner::Round {
height += CORNER_RADIUS;
}
width += config.size.border_size * 2;
height += config.size.border_size;
Ok(Size {
width,
height,
})
}
pub(crate) fn recalculate(data: &mut MenuData) {
data.icon_space = get_icon_space(&data.items, data.config.icon.as_ref().unwrap(), &data.check_icon, &data.submenu_icon);
if let Ok(size) = calculate(&mut data.items, &data.config, data.current_theme, data.icon_space) {
data.size = size;
}
}
pub(crate) fn measure_item(factory: &IDWriteFactory, config: &Config, menu_item: &MenuItem, theme: Theme, icon_space: IconSpace, widest_accel: &str) -> Result<(i32, i32), Error> {
let mut width = 0;
let mut height = 0;
match menu_item.menu_item_type {
MenuItemType::Separator => {
height += config.size.separator_size;
height += config.size.item_vertical_padding * 2;
}
_ => {
let mut raw_text = encode_wide(&menu_item.label);
if !widest_accel.is_empty() {
raw_text.extend(encode_wide(widest_accel));
}
let metrics = get_text_metrics(factory, theme, config, &mut raw_text)?;
height = metrics.height as i32;
if height < 0 {
height = -height;
}
let icon_height = std::cmp::max(icon_space.left.width, icon_space.mid.width);
let icon_height = std::cmp::max(icon_space.right.width, icon_height);
if height < icon_height {
height += icon_height - height;
}
height += config.size.item_vertical_padding * 2;
width = metrics.width as i32;
width += config.size.item_horizontal_padding * 2;
width += icon_space.left.width + icon_space.left.lmargin + icon_space.left.rmargin;
if menu_item.icon.is_some() || config.icon.as_ref().unwrap().reserve_icon_size {
width += icon_space.mid.width + icon_space.mid.lmargin + icon_space.mid.rmargin;
}
width += icon_space.right.width + icon_space.right.lmargin + icon_space.right.rmargin;
if !menu_item.accelerator.is_empty() {
width += MIN_BUTTON_WIDTH + DEFAULT_ICON_MARGIN;
}
}
}
Ok((width, height))
}
pub(crate) fn get_current_theme(theme: Theme) -> Theme {
let is_dark = if theme == Theme::System {
is_sys_dark_color()
} else {
theme == Theme::Dark
};
if is_dark {
Theme::Dark
} else {
Theme::Light
}
}
pub(crate) fn get_color_scheme(data: &MenuData) -> &ColorScheme {
let is_dark = if data.current_theme == Theme::System {
is_sys_dark_color()
} else {
data.current_theme == Theme::Dark
};
if is_dark {
&data.config.color.dark
} else {
&data.config.color.light
}
}
pub(crate) fn is_win11() -> bool {
let version = windows_version::OsVersion::current();
version.major == 10 && version.build >= 22000
}
pub(crate) fn set_window_border_color(window_handle: isize, data: &MenuData) -> Result<(), Error> {
if is_win11() {
let hwnd = hwnd!(window_handle);
if data.config.size.border_size > 0 {
let color = get_color_scheme(data).border;
let rgb = rgba_from_hex(color);
let hex = hex_from_rgb(rgb.b, rgb.g, rgb.r);
unsafe { DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &COLORREF(hex) as *const _ as *const _, size_of::<COLORREF>() as u32)? };
} else {
unsafe { DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &DWMWA_COLOR_NONE as *const _ as *const _, size_of::<COLORREF>() as u32)? };
}
}
Ok(())
}
pub(crate) fn should_apps_use_dark_mode() -> bool {
const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: u16 = 132;
type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool;
static SHOULD_APPS_USE_DARK_MODE: LazyLock<Option<ShouldAppsUseDarkMode>> = LazyLock::new(|| unsafe {
if HMODULE(*HUXTHEME as _).is_invalid() {
return None;
}
GetProcAddress(HMODULE(*HUXTHEME as _), PCSTR::from_raw(UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL as usize as *mut _)).map(|handle| transmute(handle))
});
SHOULD_APPS_USE_DARK_MODE.map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }).unwrap_or(false)
}
pub(crate) fn is_sys_dark_color() -> bool {
if let Ok(settings) = UISettings::new() {
if let Ok(color) = settings.GetColorValue(UIColorType::Background) {
let sum: u32 = (5 * color.G as u32) + (2 * color.R as u32) + (color.B as u32);
sum <= (8 * 128)
} else {
false
}
} else {
false
}
}
pub(crate) struct ComGuard;
impl ComGuard {
pub fn new() -> Self {
let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
Self
}
}
impl Drop for ComGuard {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}