use std::path::{Path, PathBuf};
#[cfg(target_os = "windows")]
use crate::backend::MessageDialogButton;
use crate::backend::{
BackendError, Clipboard, Display, FileDialogOptions, MessageDialogChoice, MessageDialogOptions,
Notification, PlatformServices, ServiceResult, SystemTheme,
};
use crate::event::{Point, Rect};
#[cfg(target_os = "windows")]
use crate::primitives::dialog::DialogSeverity;
use crate::types::Color;
#[cfg(target_os = "windows")]
use std::cell::Cell;
#[cfg(target_os = "windows")]
use std::sync::atomic::{AtomicU32, Ordering};
#[cfg(target_os = "windows")]
use windows::core::{IUnknown, BOOL, PCWSTR};
#[cfg(target_os = "windows")]
use windows::Win32::Foundation::{GlobalFree, HANDLE, HGLOBAL, HWND, LPARAM, POINT, RECT};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFOEXW,
};
#[cfg(target_os = "windows")]
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, IBindCtx, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
};
#[cfg(target_os = "windows")]
use windows::Win32::System::DataExchange::{
CloseClipboard, EmptyClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
SetClipboardData,
};
#[cfg(target_os = "windows")]
use windows::Win32::System::Diagnostics::Debug::MessageBeep;
#[cfg(target_os = "windows")]
use windows::Win32::System::Memory::{
GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, GMEM_MOVEABLE,
};
#[cfg(target_os = "windows")]
use windows::Win32::System::Ole::{CF_HDROP, CF_UNICODETEXT};
#[cfg(target_os = "windows")]
use windows::Win32::UI::Controls::{
TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOGCONFIG_0, TASKDIALOG_BUTTON,
TDF_ALLOW_DIALOG_CANCELLATION, TD_ERROR_ICON, TD_INFORMATION_ICON, TD_WARNING_ICON,
};
#[cfg(target_os = "windows")]
use windows::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI};
#[cfg(target_os = "windows")]
use windows::Win32::UI::Shell::Common::{COMDLG_FILTERSPEC, ITEMIDLIST};
#[cfg(target_os = "windows")]
use windows::Win32::UI::Shell::{
DragQueryFileW, FileOpenDialog, FileSaveDialog, IFileDialog, IFileOpenDialog, IFileSaveDialog,
ILClone, ILCreateFromPathW, ILFindLastID, ILFree, ILRemoveLastID, IShellItem,
SHCreateItemFromParsingName, SHOpenFolderAndSelectItems, Shell_NotifyIconW, FOS_PICKFOLDERS,
HDROP, NIF_ICON, NIF_INFO, NIIF_ERROR, NIIF_INFO, NIIF_NOSOUND, NIM_ADD, NIM_DELETE,
NOTIFYICONDATAW, SIGDN_FILESYSPATH,
};
#[cfg(target_os = "windows")]
use windows::Win32::UI::WindowsAndMessaging::{
GetCursorPos, LoadIconW, HICON, IDI_ERROR, IDI_INFORMATION, MB_OK, MONITORINFOF_PRIMARY,
USER_DEFAULT_SCREEN_DPI,
};
#[cfg(target_os = "windows")]
use windows::UI::ViewManagement::{AccessibilitySettings, UIColorType, UISettings};
pub struct WinClipboard;
impl Clipboard for WinClipboard {
fn read_text(&self) -> Option<String> {
#[cfg(target_os = "windows")]
{
win_clipboard_read()
}
#[cfg(not(target_os = "windows"))]
{
None
}
}
fn write_text(&self, text: &str) {
#[cfg(target_os = "windows")]
{
let _ = win_clipboard_write(text);
}
#[cfg(not(target_os = "windows"))]
{
let _ = text;
}
}
fn write_text_result(&self, text: &str) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
win_clipboard_write(text)
}
#[cfg(not(target_os = "windows"))]
{
let _ = text;
Ok(())
}
}
fn read_file_list(&self) -> ServiceResult<Vec<PathBuf>> {
#[cfg(target_os = "windows")]
{
win_clipboard_read_file_list()
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn clear(&self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
win_clipboard_clear()
}
#[cfg(not(target_os = "windows"))]
{
Ok(())
}
}
}
pub struct WinPlatformServices {
clipboard: WinClipboard,
#[cfg(target_os = "windows")]
window: Cell<Option<HWND>>,
}
impl WinPlatformServices {
pub fn new() -> Self {
Self {
clipboard: WinClipboard,
#[cfg(target_os = "windows")]
window: Cell::new(None),
}
}
#[cfg(target_os = "windows")]
pub(crate) fn set_window(&self, hwnd: HWND) {
self.window.set(Some(hwnd));
}
}
impl Default for WinPlatformServices {
fn default() -> Self {
Self::new()
}
}
impl PlatformServices for WinPlatformServices {
fn platform_name(&self) -> &'static str {
"win-gui"
}
fn clipboard(&self) -> &dyn Clipboard {
&self.clipboard
}
fn show_file_open_dialog(&self, opts: FileDialogOptions) -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
win_show_open_dialog(self.window.get(), &opts)
}
#[cfg(not(target_os = "windows"))]
{
let _ = opts;
None
}
}
fn show_file_save_dialog(&self, opts: FileDialogOptions) -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
win_show_save_dialog(self.window.get(), &opts)
}
#[cfg(not(target_os = "windows"))]
{
let _ = opts;
None
}
}
fn show_folder_open_dialog(&self, opts: FileDialogOptions) -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
win_show_folder_open_dialog(self.window.get(), &opts)
}
#[cfg(not(target_os = "windows"))]
{
let _ = opts;
None
}
}
fn show_message_dialog(&self, opts: MessageDialogOptions) -> Option<MessageDialogChoice> {
#[cfg(target_os = "windows")]
{
win_show_message_dialog(self.window.get(), &opts)
}
#[cfg(not(target_os = "windows"))]
{
let _ = opts;
None
}
}
fn send_notification(&self, n: Notification) {
#[cfg(target_os = "windows")]
{
win_send_notification(self.window.get(), &n);
}
#[cfg(not(target_os = "windows"))]
{
let _ = n;
}
}
fn open_url(&self, url: &str) {
let _ = self.open_url_result(url);
}
fn open_url_result(&self, url: &str) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
crate::desktop::open_with_default(std::ffi::OsStr::new(url))
}
#[cfg(not(target_os = "windows"))]
{
let _ = url;
Err(BackendError::Unsupported)
}
}
fn reveal_in_file_manager(&self, path: &Path) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
win_reveal_in_file_manager(path)
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
Err(BackendError::Unsupported)
}
}
fn open_path(&self, path: &Path) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
crate::desktop::open_with_default(path.as_os_str())
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
Err(BackendError::Unsupported)
}
}
fn move_to_trash(&self, path: &Path) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
crate::desktop::move_to_trash(path)
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
Err(BackendError::Unsupported)
}
}
fn beep(&self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
win_beep()
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn system_theme(&self) -> ServiceResult<SystemTheme> {
#[cfg(target_os = "windows")]
{
win_system_theme()
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn displays(&self) -> ServiceResult<Vec<Display>> {
#[cfg(target_os = "windows")]
{
win_displays()
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn cursor_screen_point(&self) -> ServiceResult<Point> {
#[cfg(target_os = "windows")]
{
win_cursor_screen_point()
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
}
use crate::desktop::wide_nul_terminated;
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn decode_wide_nul_terminated(slice: &[u16]) -> String {
let len = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
String::from_utf16_lossy(&slice[..len])
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn copy_wide_truncated(dst: &mut [u16], text: &str) {
if dst.is_empty() {
return;
}
let wide = wide_nul_terminated(text);
let take = wide.len().min(dst.len() - 1);
dst[..take].copy_from_slice(&wide[..take]);
dst[take] = 0;
}
#[cfg(target_os = "windows")]
fn win_clipboard_read() -> Option<String> {
unsafe {
IsClipboardFormatAvailable(CF_UNICODETEXT.0 as u32).ok()?;
OpenClipboard(None).ok()?;
let text = (|| {
let handle = GetClipboardData(CF_UNICODETEXT.0 as u32).ok()?;
let hglobal = HGLOBAL(handle.0);
let ptr = GlobalLock(hglobal) as *const u16;
if ptr.is_null() {
return None;
}
let word_len = GlobalSize(hglobal) / std::mem::size_of::<u16>();
let slice = std::slice::from_raw_parts(ptr, word_len);
let text = decode_wide_nul_terminated(slice);
let _ = GlobalUnlock(hglobal);
Some(text)
})();
let _ = CloseClipboard();
text
}
}
#[cfg(target_os = "windows")]
fn win_clipboard_write(text: &str) -> ServiceResult<()> {
unsafe {
if OpenClipboard(None).is_err() {
return Err(BackendError::PlatformFailure {
context: "OpenClipboard".to_string(),
});
}
let _ = EmptyClipboard();
let wide = wide_nul_terminated(text);
let bytes = wide.len() * std::mem::size_of::<u16>();
let result = match GlobalAlloc(GMEM_MOVEABLE, bytes) {
Ok(hglobal) => {
let ptr = GlobalLock(hglobal) as *mut u16;
if ptr.is_null() {
let _ = GlobalFree(Some(hglobal));
Err(BackendError::PlatformFailure {
context: "GlobalLock".to_string(),
})
} else {
std::ptr::copy_nonoverlapping(wide.as_ptr(), ptr, wide.len());
let _ = GlobalUnlock(hglobal);
if SetClipboardData(CF_UNICODETEXT.0 as u32, Some(HANDLE(hglobal.0))).is_err() {
let _ = GlobalFree(Some(hglobal));
Err(BackendError::PlatformFailure {
context: "SetClipboardData".to_string(),
})
} else {
Ok(())
}
}
}
Err(_) => Err(BackendError::PlatformFailure {
context: "GlobalAlloc".to_string(),
}),
};
let _ = CloseClipboard();
result
}
}
#[cfg(target_os = "windows")]
fn win_clipboard_read_file_list() -> ServiceResult<Vec<PathBuf>> {
unsafe {
if IsClipboardFormatAvailable(CF_HDROP.0 as u32).is_err() {
return Err(BackendError::PlatformFailure {
context: "IsClipboardFormatAvailable(CF_HDROP)".to_string(),
});
}
if OpenClipboard(None).is_err() {
return Err(BackendError::PlatformFailure {
context: "OpenClipboard".to_string(),
});
}
let result = (|| {
let handle =
GetClipboardData(CF_HDROP.0 as u32).map_err(|_| BackendError::PlatformFailure {
context: "GetClipboardData(CF_HDROP)".to_string(),
})?;
let hdrop = HDROP(handle.0);
let count = DragQueryFileW(hdrop, u32::MAX, None);
let mut files = Vec::with_capacity(count as usize);
for i in 0..count {
let needed = DragQueryFileW(hdrop, i, None);
if needed == 0 {
continue;
}
let mut buf = vec![0u16; needed as usize + 1];
let copied = DragQueryFileW(hdrop, i, Some(&mut buf));
buf.truncate(copied as usize);
files.push(PathBuf::from(String::from_utf16_lossy(&buf)));
}
Ok(files)
})();
let _ = CloseClipboard();
result
}
}
#[cfg(target_os = "windows")]
fn win_clipboard_clear() -> ServiceResult<()> {
unsafe {
if OpenClipboard(None).is_err() {
return Err(BackendError::PlatformFailure {
context: "OpenClipboard".to_string(),
});
}
let result = EmptyClipboard().map_err(|_| BackendError::PlatformFailure {
context: "EmptyClipboard".to_string(),
});
let _ = CloseClipboard();
result
}
}
#[cfg(target_os = "windows")]
pub(crate) fn ensure_com_initialized() {
thread_local! {
static COM_INITIALIZED: Cell<bool> = const { Cell::new(false) };
}
COM_INITIALIZED.with(|done| {
if !done.get() {
let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
done.set(true);
}
});
}
#[cfg(target_os = "windows")]
fn configure_file_dialog(
dialog: &IFileDialog,
opts: &FileDialogOptions,
initial_name: Option<&str>,
) -> windows::core::Result<()> {
unsafe {
if let Some(ref title) = opts.title {
dialog.SetTitle(PCWSTR::from_raw(wide_nul_terminated(title).as_ptr()))?;
}
if let Some(ref dir) = opts.initial_dir {
if let Some(dir_str) = dir.to_str() {
let path = wide_nul_terminated(dir_str);
if let Ok(item) = SHCreateItemFromParsingName::<_, Option<&IBindCtx>, IShellItem>(
PCWSTR::from_raw(path.as_ptr()),
None,
) {
let _ = dialog.SetFolder(&item);
}
}
}
if let Some(name) = initial_name {
dialog.SetFileName(PCWSTR::from_raw(wide_nul_terminated(name).as_ptr()))?;
}
if !opts.filters.is_empty() {
let buffers: Vec<(Vec<u16>, Vec<u16>)> = opts
.filters
.iter()
.map(|(name, exts)| {
let spec = exts
.iter()
.map(|ext| format!("*.{ext}"))
.collect::<Vec<_>>()
.join(";");
(wide_nul_terminated(name), wide_nul_terminated(&spec))
})
.collect();
let specs: Vec<COMDLG_FILTERSPEC> = buffers
.iter()
.map(|(name, spec)| COMDLG_FILTERSPEC {
pszName: PCWSTR::from_raw(name.as_ptr()),
pszSpec: PCWSTR::from_raw(spec.as_ptr()),
})
.collect();
dialog.SetFileTypes(&specs)?;
}
}
Ok(())
}
#[cfg(target_os = "windows")]
fn shell_item_path(item: &IShellItem) -> Option<PathBuf> {
unsafe {
let pwstr = item.GetDisplayName(SIGDN_FILESYSPATH).ok()?;
let text = pwstr.to_string().ok();
windows::Win32::System::Com::CoTaskMemFree(Some(pwstr.0 as *const core::ffi::c_void));
text.map(PathBuf::from)
}
}
#[cfg(target_os = "windows")]
fn win_show_open_dialog(owner: Option<HWND>, opts: &FileDialogOptions) -> Option<PathBuf> {
ensure_com_initialized();
unsafe {
let dialog: IFileOpenDialog =
CoCreateInstance(&FileOpenDialog, None::<&IUnknown>, CLSCTX_INPROC_SERVER).ok()?;
configure_file_dialog(&dialog, opts, None).ok()?;
dialog.Show(owner).ok()?;
let item = dialog.GetResult().ok()?;
shell_item_path(&item)
}
}
#[cfg(target_os = "windows")]
fn win_show_folder_open_dialog(owner: Option<HWND>, opts: &FileDialogOptions) -> Option<PathBuf> {
ensure_com_initialized();
unsafe {
let dialog: IFileOpenDialog =
CoCreateInstance(&FileOpenDialog, None::<&IUnknown>, CLSCTX_INPROC_SERVER).ok()?;
let current_options = dialog.GetOptions().ok()?;
dialog.SetOptions(current_options | FOS_PICKFOLDERS).ok()?;
configure_file_dialog(&dialog, opts, None).ok()?;
dialog.Show(owner).ok()?;
let item = dialog.GetResult().ok()?;
shell_item_path(&item)
}
}
#[cfg(target_os = "windows")]
fn win_show_save_dialog(owner: Option<HWND>, opts: &FileDialogOptions) -> Option<PathBuf> {
ensure_com_initialized();
unsafe {
let dialog: IFileSaveDialog =
CoCreateInstance(&FileSaveDialog, None::<&IUnknown>, CLSCTX_INPROC_SERVER).ok()?;
configure_file_dialog(&dialog, opts, opts.initial_filename.as_deref()).ok()?;
dialog.Show(owner).ok()?;
let item = dialog.GetResult().ok()?;
shell_item_path(&item)
}
}
#[cfg(target_os = "windows")]
const FIRST_CUSTOM_BUTTON_ID: i32 = 100;
#[cfg(target_os = "windows")]
const TASKDIALOG_IDCANCEL: i32 = 2;
#[cfg(target_os = "windows")]
fn assign_button_ids(buttons: &[MessageDialogButton]) -> Vec<i32> {
let mut cancel_assigned = false;
buttons
.iter()
.enumerate()
.map(|(i, b)| {
if b.is_cancel && !cancel_assigned {
cancel_assigned = true;
TASKDIALOG_IDCANCEL
} else {
FIRST_CUSTOM_BUTTON_ID + i as i32
}
})
.collect()
}
#[cfg(target_os = "windows")]
fn win_show_message_dialog(
owner: Option<HWND>,
opts: &MessageDialogOptions,
) -> Option<MessageDialogChoice> {
ensure_com_initialized();
let ids = assign_button_ids(&opts.buttons);
let title_wide = wide_nul_terminated(&opts.title);
let body_wide = wide_nul_terminated(&opts.body);
let label_wides: Vec<Vec<u16>> = opts
.buttons
.iter()
.map(|b| wide_nul_terminated(&b.label))
.collect();
let button_specs: Vec<TASKDIALOG_BUTTON> = ids
.iter()
.zip(label_wides.iter())
.map(|(&id, wide)| TASKDIALOG_BUTTON {
nButtonID: id,
pszButtonText: PCWSTR::from_raw(wide.as_ptr()),
})
.collect();
let default_id = opts
.buttons
.iter()
.zip(ids.iter())
.find(|(b, _)| b.is_default)
.map(|(_, &id)| id)
.unwrap_or(0);
let icon = match opts.severity {
Some(DialogSeverity::Error) => TD_ERROR_ICON,
Some(DialogSeverity::Warning) => TD_WARNING_ICON,
Some(DialogSeverity::Info) => TD_INFORMATION_ICON,
Some(DialogSeverity::Question) | None => PCWSTR::null(),
};
let config = TASKDIALOGCONFIG {
cbSize: std::mem::size_of::<TASKDIALOGCONFIG>() as u32,
hwndParent: owner.unwrap_or_default(),
dwFlags: TDF_ALLOW_DIALOG_CANCELLATION,
pszMainInstruction: PCWSTR::from_raw(title_wide.as_ptr()),
pszContent: PCWSTR::from_raw(body_wide.as_ptr()),
cButtons: button_specs.len() as u32,
pButtons: button_specs.as_ptr(),
nDefaultButton: default_id,
Anonymous1: TASKDIALOGCONFIG_0 { pszMainIcon: icon },
..Default::default()
};
let mut chosen: i32 = 0;
unsafe {
TaskDialogIndirect(&config, Some(&mut chosen), None, None).ok()?;
}
ids.iter()
.position(|&id| id == chosen)
.map(|i| opts.buttons[i].id)
}
#[cfg(target_os = "windows")]
static NEXT_NOTIFICATION_ID: AtomicU32 = AtomicU32::new(1);
#[cfg(target_os = "windows")]
const NOTIFICATION_ICON_LIFETIME: std::time::Duration = std::time::Duration::from_secs(8);
#[cfg(target_os = "windows")]
fn win_send_notification(owner: Option<HWND>, n: &Notification) {
let Some(hwnd) = owner else {
return;
};
unsafe {
let icon_resource = if n.urgent { IDI_ERROR } else { IDI_INFORMATION };
let icon = LoadIconW(None, icon_resource).unwrap_or(HICON(std::ptr::null_mut()));
let uid = NEXT_NOTIFICATION_ID.fetch_add(1, Ordering::Relaxed);
let mut severity_flags = if n.urgent { NIIF_ERROR } else { NIIF_INFO };
if n.is_silent() {
severity_flags |= NIIF_NOSOUND;
}
let mut data = NOTIFYICONDATAW {
cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
hWnd: hwnd,
uID: uid,
uFlags: NIF_ICON | NIF_INFO,
hIcon: icon,
dwInfoFlags: severity_flags,
..Default::default()
};
copy_wide_truncated(&mut data.szInfo, &n.body);
copy_wide_truncated(&mut data.szInfoTitle, &n.title);
if !Shell_NotifyIconW(NIM_ADD, &data).as_bool() {
return;
}
let hwnd_value = hwnd.0 as isize;
std::thread::spawn(move || {
std::thread::sleep(NOTIFICATION_ICON_LIFETIME);
let removal = NOTIFYICONDATAW {
cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
hWnd: HWND(hwnd_value as *mut core::ffi::c_void),
uID: uid,
..Default::default()
};
let _ = Shell_NotifyIconW(NIM_DELETE, &removal);
});
}
}
#[cfg(target_os = "windows")]
fn win_reveal_in_file_manager(path: &Path) -> ServiceResult<()> {
ensure_com_initialized();
let wide = wide_nul_terminated(&path.to_string_lossy());
unsafe {
let full_pidl = ILCreateFromPathW(PCWSTR::from_raw(wide.as_ptr()));
if full_pidl.is_null() {
return Err(BackendError::PlatformFailure {
context: "ILCreateFromPathW returned null".to_string(),
});
}
let pidl_last: *const ITEMIDLIST = ILFindLastID(full_pidl);
let folder_pidl = ILClone(full_pidl);
let result = if folder_pidl.is_null() {
Err(BackendError::PlatformFailure {
context: "ILClone returned null".to_string(),
})
} else if !ILRemoveLastID(Some(folder_pidl)).as_bool() {
Err(BackendError::PlatformFailure {
context: "ILRemoveLastID: path has no parent folder".to_string(),
})
} else {
let children: [*const ITEMIDLIST; 1] = [pidl_last];
SHOpenFolderAndSelectItems(folder_pidl, Some(&children), 0).map_err(|e| {
BackendError::PlatformFailure {
context: format!("SHOpenFolderAndSelectItems: {e}"),
}
})
};
ILFree(Some(folder_pidl));
ILFree(Some(full_pidl));
result
}
}
#[cfg(target_os = "windows")]
fn win_beep() -> ServiceResult<()> {
unsafe { MessageBeep(MB_OK) }.map_err(|e| BackendError::PlatformFailure {
context: format!("MessageBeep: {e}"),
})
}
#[cfg(target_os = "windows")]
fn win_system_theme() -> ServiceResult<SystemTheme> {
let platform_failure = |context: &str| BackendError::PlatformFailure {
context: context.to_string(),
};
let settings = UISettings::new()
.map_err(|_| platform_failure("windows::UI::ViewManagement::UISettings::new"))?;
let bg = settings
.GetColorValue(UIColorType::Background)
.map_err(|_| platform_failure("UISettings::GetColorValue(Background)"))?;
let accent = settings
.GetColorValue(UIColorType::Accent)
.map_err(|_| platform_failure("UISettings::GetColorValue(Accent)"))?;
let high_contrast = AccessibilitySettings::new()
.and_then(|a| a.HighContrast())
.unwrap_or(false);
Ok(system_theme_from_ui_colors(
(bg.R, bg.G, bg.B),
(accent.R, accent.G, accent.B),
high_contrast,
))
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn system_theme_from_ui_colors(
bg: (u8, u8, u8),
accent: (u8, u8, u8),
high_contrast: bool,
) -> SystemTheme {
let (r, g, b) = bg;
let luma = (r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000;
SystemTheme {
dark: luma < 128,
accent: Some(Color::rgb(accent.0, accent.1, accent.2)),
high_contrast,
}
}
#[cfg(target_os = "windows")]
unsafe extern "system" fn win_collect_monitor(
hmonitor: HMONITOR,
_hdc: HDC,
_clip_rect: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = &mut *(lparam.0 as *mut Vec<HMONITOR>);
monitors.push(hmonitor);
BOOL(1)
}
#[cfg(target_os = "windows")]
fn win_displays() -> ServiceResult<Vec<Display>> {
let mut handles: Vec<HMONITOR> = Vec::new();
unsafe {
let _ = EnumDisplayMonitors(
None,
None,
Some(win_collect_monitor),
LPARAM(&mut handles as *mut Vec<HMONITOR> as isize),
);
}
let mut out = Vec::with_capacity(handles.len());
for hmonitor in handles {
let mut info = MONITORINFOEXW::default();
info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
let ok = unsafe { GetMonitorInfoW(hmonitor, &mut info.monitorInfo) };
if !ok.as_bool() {
continue;
}
let mut dpi_x = USER_DEFAULT_SCREEN_DPI;
let mut dpi_y = USER_DEFAULT_SCREEN_DPI;
let _ = unsafe { GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) };
let r = info.monitorInfo.rcMonitor;
let w = info.monitorInfo.rcWork;
out.push(display_from_monitor_rects(
(r.left, r.top, r.right, r.bottom),
(w.left, w.top, w.right, w.bottom),
(info.monitorInfo.dwFlags & MONITORINFOF_PRIMARY) != 0,
dpi_x,
));
}
Ok(out)
}
#[cfg(target_os = "windows")]
fn win_cursor_screen_point() -> ServiceResult<Point> {
let mut point = POINT::default();
unsafe { GetCursorPos(&mut point) }.map_err(|e| BackendError::PlatformFailure {
context: format!("GetCursorPos: {e}"),
})?;
Ok(Point::new(point.x as f32, point.y as f32))
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn display_from_monitor_rects(
monitor_rect: (i32, i32, i32, i32),
work_rect: (i32, i32, i32, i32),
primary: bool,
dpi: u32,
) -> Display {
let (ml, mt, mr, mb) = monitor_rect;
let (wl, wt, wr, wb) = work_rect;
Display {
bounds: Rect::new(ml as f32, mt as f32, (mr - ml) as f32, (mb - mt) as f32),
work_area: Rect::new(wl as f32, wt as f32, (wr - wl) as f32, (wb - wt) as f32),
scale: dpi as f32 / 96.0,
primary,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_name_is_win_gui() {
let svc = WinPlatformServices::new();
assert_eq!(svc.platform_name(), "win-gui");
}
#[test]
fn system_theme_from_ui_colors_black_background_is_dark() {
let theme = system_theme_from_ui_colors((0, 0, 0), (0, 120, 215), false);
assert!(theme.dark);
assert_eq!(theme.accent, Some(Color::rgb(0, 120, 215)));
assert!(!theme.high_contrast);
}
#[test]
fn system_theme_from_ui_colors_white_background_is_light() {
let theme = system_theme_from_ui_colors((255, 255, 255), (0, 120, 215), false);
assert!(!theme.dark);
}
#[test]
fn system_theme_from_ui_colors_reports_high_contrast() {
assert!(system_theme_from_ui_colors((0, 0, 0), (255, 255, 0), true).high_contrast);
}
#[test]
fn display_from_monitor_rects_converts_rects_to_bounds() {
let d = display_from_monitor_rects((0, 0, 1920, 1080), (0, 0, 1920, 1040), true, 96);
assert_eq!(d.bounds, Rect::new(0.0, 0.0, 1920.0, 1080.0));
assert_eq!(d.work_area, Rect::new(0.0, 0.0, 1920.0, 1040.0));
assert!(d.primary);
assert_eq!(d.scale, 1.0);
}
#[test]
fn display_from_monitor_rects_handles_negative_origin_and_non_primary() {
let d = display_from_monitor_rects((-1920, 0, 0, 1080), (-1920, 0, 0, 1080), false, 96);
assert_eq!(d.bounds, Rect::new(-1920.0, 0.0, 1920.0, 1080.0));
assert!(!d.primary);
}
#[test]
fn display_from_monitor_rects_scales_by_dpi() {
let d = display_from_monitor_rects((0, 0, 3840, 2160), (0, 0, 3840, 2120), true, 192);
assert_eq!(d.scale, 2.0);
}
#[test]
fn wide_round_trips_through_decode() {
let wide = wide_nul_terminated("hello");
assert_eq!(wide.len(), "hello".len() + 1);
assert_eq!(*wide.last().unwrap(), 0);
assert_eq!(decode_wide_nul_terminated(&wide), "hello");
}
#[test]
fn decode_stops_at_first_nul_not_slice_end() {
let mut wide = wide_nul_terminated("hi");
wide.extend_from_slice(&[b'X' as u16, b'Y' as u16]);
assert_eq!(decode_wide_nul_terminated(&wide), "hi");
}
#[test]
fn decode_handles_a_slice_with_no_nul_at_all() {
let wide: Vec<u16> = "no-nul".encode_utf16().collect();
assert_eq!(decode_wide_nul_terminated(&wide), "no-nul");
}
#[test]
fn copy_wide_truncated_nul_terminates_within_bounds() {
let mut dst = [0u16; 8];
copy_wide_truncated(&mut dst, "hi");
assert_eq!(decode_wide_nul_terminated(&dst), "hi");
}
#[test]
fn copy_wide_truncated_truncates_text_longer_than_dst() {
let mut dst = [0u16; 4];
copy_wide_truncated(&mut dst, "abcdefgh");
assert_eq!(decode_wide_nul_terminated(&dst), "abc");
assert_eq!(dst[3], 0);
}
#[test]
fn copy_wide_truncated_empty_dst_does_not_panic() {
let mut dst: [u16; 0] = [];
copy_wide_truncated(&mut dst, "anything");
}
#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_stubs_degrade_gracefully() {
let svc = WinPlatformServices::new();
assert!(svc.clipboard().read_text().is_none());
svc.clipboard().write_text("ignored");
assert_eq!(svc.clipboard().read_image(), Err(BackendError::Unsupported));
assert_eq!(
svc.clipboard().read_file_list(),
Err(BackendError::Unsupported)
);
assert_eq!(svc.clipboard().clear(), Ok(()));
assert!(svc
.show_file_open_dialog(FileDialogOptions::default())
.is_none());
assert!(svc
.show_file_save_dialog(FileDialogOptions::default())
.is_none());
assert!(svc
.show_folder_open_dialog(FileDialogOptions::default())
.is_none());
svc.send_notification(Notification::new("t", "b"));
assert!(svc
.show_message_dialog(MessageDialogOptions {
title: "t".to_string(),
body: "b".to_string(),
buttons: Vec::new(),
severity: None,
})
.is_none());
svc.open_url("https://example.com");
assert_eq!(
svc.open_url_result("https://example.com"),
Err(BackendError::Unsupported)
);
assert_eq!(svc.system_theme(), Err(BackendError::Unsupported));
let scratch = std::path::Path::new("ignored");
assert_eq!(
svc.reveal_in_file_manager(scratch),
Err(BackendError::Unsupported)
);
assert_eq!(svc.open_path(scratch), Err(BackendError::Unsupported));
assert_eq!(svc.move_to_trash(scratch), Err(BackendError::Unsupported));
assert_eq!(svc.beep(), Err(BackendError::Unsupported));
assert_eq!(svc.displays(), Err(BackendError::Unsupported));
assert_eq!(svc.cursor_screen_point(), Err(BackendError::Unsupported));
}
#[cfg(target_os = "windows")]
#[test]
fn open_url_result_and_open_path_report_a_real_shell_execute_failure() {
let unopenable = format!(
"C:\\quadraui-1087-no-such-directory-{}\\x.quadraui1087nohandler",
std::process::id()
);
let svc = WinPlatformServices::new();
assert!(svc.open_url_result(&unopenable).is_err());
assert!(svc.open_path(Path::new(&unopenable)).is_err());
}
#[cfg(target_os = "windows")]
fn win_button(id: &str, is_default: bool, is_cancel: bool) -> MessageDialogButton {
MessageDialogButton {
id: crate::types::WidgetId::new(id),
label: id.to_string(),
is_default,
is_cancel,
}
}
#[cfg(target_os = "windows")]
#[test]
fn assign_button_ids_starts_at_100_with_no_cancel_button() {
let buttons = [
win_button("ok", true, false),
win_button("retry", false, false),
];
assert_eq!(assign_button_ids(&buttons), vec![100, 101]);
}
#[cfg(target_os = "windows")]
#[test]
fn assign_button_ids_gives_the_first_cancel_button_idcancel() {
let buttons = [
win_button("save", false, false),
win_button("dont_save", false, false),
win_button("cancel", false, true),
];
assert_eq!(
assign_button_ids(&buttons),
vec![100, 101, TASKDIALOG_IDCANCEL]
);
}
#[cfg(target_os = "windows")]
#[test]
fn assign_button_ids_only_the_first_cancel_button_gets_idcancel() {
let buttons = [win_button("a", false, true), win_button("b", false, true)];
assert_eq!(assign_button_ids(&buttons), vec![TASKDIALOG_IDCANCEL, 101]);
}
}