#[derive(Debug, Default)]
pub struct WindowsWin32FileDialogService;
impl FileDialogService for WindowsWin32FileDialogService {
fn open_file_dialog(&mut self, spec: &FileDialogSpec) -> ZsuiResult<Option<Vec<PathBuf>>> {
windows_win32_open_file_dialog(spec)
}
fn save_file_dialog(&mut self, spec: &SaveFileDialogSpec) -> ZsuiResult<Option<PathBuf>> {
windows_win32_save_file_dialog(spec)
}
}
pub fn windows_win32_open_file_dialog(spec: &FileDialogSpec) -> ZsuiResult<Option<Vec<PathBuf>>> {
const FILE_BUFFER_LEN: usize = 32_768;
let mut file_buffer = vec![0u16; FILE_BUFFER_LEN];
let title = wide_null(&spec.title);
let filter = windows_file_dialog_filter(&spec.filters);
let initial_dir =
native_file_dialog_initial_directory(spec.current_path.as_deref()).map(|path| {
path.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>()
});
let mut dialog: OPENFILENAMEW = unsafe { zeroed() };
dialog.lStructSize = size_of::<OPENFILENAMEW>() as u32;
dialog.hwndOwner = unsafe { GetActiveWindow() };
dialog.lpstrFilter = filter.as_ptr();
dialog.lpstrFile = file_buffer.as_mut_ptr();
dialog.nMaxFile = file_buffer.len() as u32;
dialog.lpstrInitialDir = initial_dir
.as_ref()
.map(|path| path.as_ptr())
.unwrap_or(null());
dialog.lpstrTitle = title.as_ptr();
dialog.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
if spec.allow_multiple {
dialog.Flags |= OFN_ALLOWMULTISELECT;
}
if unsafe { GetOpenFileNameW(&mut dialog) } == 0 {
return windows_common_dialog_cancel_or_error("windows_open_file_dialog");
}
Ok(Some(parse_windows_open_file_buffer(&file_buffer)))
}
pub fn windows_win32_save_file_dialog(spec: &SaveFileDialogSpec) -> ZsuiResult<Option<PathBuf>> {
const FILE_BUFFER_LEN: usize = 32_768;
let mut file_buffer = vec![0u16; FILE_BUFFER_LEN];
let suggested_name = native_save_dialog_suggested_name(
spec.suggested_name.as_deref(),
spec.current_path.as_deref(),
);
if let Some(name) = suggested_name.as_deref() {
let encoded = name.encode_utf16().collect::<Vec<_>>();
if encoded.len() + 1 > file_buffer.len() {
return Err(ZsuiError::invalid_spec(
"save_file_dialog.suggested_name",
"suggested file name is too long",
));
}
file_buffer[..encoded.len()].copy_from_slice(&encoded);
}
let title = wide_null(&spec.title);
let filter = windows_file_dialog_filter(&spec.filters);
let initial_dir =
native_file_dialog_initial_directory(spec.current_path.as_deref()).map(|path| {
path.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>()
});
let default_extension = windows_file_dialog_default_extension(&spec.filters);
let mut dialog: OPENFILENAMEW = unsafe { zeroed() };
dialog.lStructSize = size_of::<OPENFILENAMEW>() as u32;
dialog.hwndOwner = unsafe { GetActiveWindow() };
dialog.lpstrFilter = filter.as_ptr();
dialog.lpstrFile = file_buffer.as_mut_ptr();
dialog.nMaxFile = file_buffer.len() as u32;
dialog.lpstrInitialDir = initial_dir
.as_ref()
.map(|path| path.as_ptr())
.unwrap_or(null());
dialog.lpstrTitle = title.as_ptr();
dialog.lpstrDefExt = default_extension
.as_ref()
.map(|extension| extension.as_ptr())
.unwrap_or(null());
dialog.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
if unsafe { GetSaveFileNameW(&mut dialog) } == 0 {
return windows_common_dialog_cancel_or_error("windows_save_file_dialog");
}
Ok(parse_windows_utf16_segments(&file_buffer)
.into_iter()
.next()
.map(PathBuf::from))
}
pub fn windows_win32_pick_directory_dialog(
spec: &crate::DirectoryDialogSpec,
) -> ZsuiResult<Option<PathBuf>> {
spec.validate()?;
let _apartment = WindowsDirectoryDialogApartment::new()?;
let initial_path = spec.current_path.as_ref().map(|path| {
path.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>()
});
let title = wide_null(&spec.title);
let mut display_name = vec![0u16; 260];
let mut info: BROWSEINFOW = unsafe { zeroed() };
info.hwndOwner = unsafe { GetActiveWindow() };
info.lpszTitle = title.as_ptr();
info.pszDisplayName = display_name.as_mut_ptr();
info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
info.lpfn = Some(windows_directory_picker_callback);
info.lParam = initial_path
.as_ref()
.map_or(0, |path| path.as_ptr() as isize);
let pidl = unsafe { SHBrowseForFolderW(&mut info) };
if pidl.is_null() {
return Ok(None);
}
let mut path_buffer = vec![0u16; 32_768];
let resolved = unsafe {
windows_sys::Win32::UI::Shell::SHGetPathFromIDListEx(
pidl,
path_buffer.as_mut_ptr(),
path_buffer.len() as u32,
0,
)
};
unsafe { CoTaskMemFree(pidl as *const _) };
if resolved == 0 {
return Err(ZsuiError::host(
"windows_pick_directory_dialog",
"SHGetPathFromIDListEx could not resolve the selected folder",
));
}
let end = path_buffer
.iter()
.position(|unit| *unit == 0)
.unwrap_or(path_buffer.len());
Ok(Some(PathBuf::from(OsString::from_wide(
&path_buffer[..end],
))))
}
unsafe extern "system" fn windows_directory_picker_callback(
hwnd: HWND,
message: u32,
_param: isize,
initial: isize,
) -> i32 {
if message == windows_sys::Win32::UI::Shell::BFFM_INITIALIZED && initial != 0 {
unsafe {
SendMessageW(
hwnd,
windows_sys::Win32::UI::Shell::BFFM_SETSELECTIONW,
1,
initial,
);
}
}
0
}
struct WindowsDirectoryDialogApartment;
impl WindowsDirectoryDialogApartment {
fn new() -> ZsuiResult<Self> {
let result = unsafe {
windows_sys::Win32::System::Com::CoInitializeEx(
null(),
windows_sys::Win32::System::Com::COINIT_APARTMENTTHREADED as u32,
)
};
if result < 0 {
return Err(ZsuiError::host(
"windows_directory_apartment",
format!("COM apartment initialization failed: {result:#x}"),
));
}
Ok(Self)
}
}
impl Drop for WindowsDirectoryDialogApartment {
fn drop(&mut self) {
unsafe {
windows_sys::Win32::System::Com::CoUninitialize();
}
}
}
#[derive(Debug, Default)]
pub struct WindowsWin32DialogService;
impl NativeDialogService for WindowsWin32DialogService {
fn show_native_dialog(&mut self, spec: &NativeDialogSpec) -> ZsuiResult<DialogResponse> {
windows_win32_show_native_dialog(spec)
}
}
pub fn windows_win32_show_native_dialog(spec: &NativeDialogSpec) -> ZsuiResult<DialogResponse> {
spec.validate()?;
if let Some(labels) = &spec.button_labels {
return windows_win32_show_custom_native_dialog(spec, labels);
}
let owner = unsafe { GetActiveWindow() };
let mut style = windows_native_dialog_button_style(spec.buttons)
| windows_native_dialog_level_style(spec.level);
if owner.is_null() {
style |= MB_TASKMODAL;
}
let title = wide_null(&spec.title);
let message = wide_null(&spec.message);
let response = unsafe { MessageBoxW(owner, message.as_ptr(), title.as_ptr(), style) };
match response {
IDOK => Ok(DialogResponse::Ok),
IDCANCEL => Ok(DialogResponse::Cancel),
IDYES => Ok(DialogResponse::Yes),
IDNO => Ok(DialogResponse::No),
0 => Err(ZsuiError::host(
"windows_native_dialog",
format!("MessageBoxW failed with Win32 error {}", unsafe {
GetLastError()
}),
)),
other => Err(ZsuiError::host(
"windows_native_dialog",
format!("MessageBoxW returned unexpected response {other}"),
)),
}
}
fn windows_win32_show_custom_native_dialog(
spec: &NativeDialogSpec,
labels: &DialogButtonLabels,
) -> ZsuiResult<DialogResponse> {
let owner = unsafe { GetActiveWindow() };
let title = wide_null(&spec.title);
let message = wide_null(&spec.message);
let _custom_labels = WindowsMessageBoxButtonLabels::install(spec.buttons, labels)?;
let mut style = windows_native_dialog_button_style(spec.buttons)
| windows_native_dialog_level_style(spec.level);
if owner.is_null() {
style |= MB_TASKMODAL;
}
windows_native_dialog_response(unsafe {
MessageBoxW(owner, message.as_ptr(), title.as_ptr(), style)
})
}
thread_local! {
static WINDOWS_MESSAGE_BOX_LABELS: RefCell<Vec<(i32, Vec<u16>)>> =
const { RefCell::new(Vec::new()) };
}
struct WindowsMessageBoxButtonLabels {
hook: HHOOK,
}
impl WindowsMessageBoxButtonLabels {
fn install(buttons: DialogButtons, labels: &DialogButtonLabels) -> ZsuiResult<Self> {
let already_active = WINDOWS_MESSAGE_BOX_LABELS.with(|stored| !stored.borrow().is_empty());
if already_active {
return Err(ZsuiError::host(
"windows_native_dialog_labels",
"a localized native dialog is already active on this UI thread",
));
}
WINDOWS_MESSAGE_BOX_LABELS.with(|stored| {
*stored.borrow_mut() = windows_native_dialog_custom_button_order(buttons, labels)
.into_iter()
.map(|(response, label)| {
(
windows_native_dialog_response_id(response),
wide_null(label),
)
})
.collect();
});
let hook = unsafe {
SetWindowsHookExW(
WH_CBT,
Some(windows_message_box_label_hook),
null_mut(),
GetCurrentThreadId(),
)
};
if hook.is_null() {
WINDOWS_MESSAGE_BOX_LABELS.with(|stored| stored.borrow_mut().clear());
return Err(ZsuiError::host(
"windows_native_dialog_labels",
format!("SetWindowsHookExW failed with Win32 error {}", unsafe {
GetLastError()
}),
));
}
Ok(Self { hook })
}
}
impl Drop for WindowsMessageBoxButtonLabels {
fn drop(&mut self) {
if !self.hook.is_null() {
unsafe {
UnhookWindowsHookEx(self.hook);
}
}
WINDOWS_MESSAGE_BOX_LABELS.with(|stored| stored.borrow_mut().clear());
}
}
unsafe extern "system" fn windows_message_box_label_hook(
code: i32,
window: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if code == HCBT_ACTIVATE as i32 {
WINDOWS_MESSAGE_BOX_LABELS.with(|stored| {
for (id, label) in stored.borrow().iter() {
let button = unsafe { GetDlgItem(window as HWND, *id) };
if !button.is_null() {
unsafe {
SetWindowTextW(button, label.as_ptr());
}
}
}
});
}
unsafe { CallNextHookEx(null_mut(), code, window, lparam) }
}
fn windows_native_dialog_response(response: i32) -> ZsuiResult<DialogResponse> {
match response {
IDOK => Ok(DialogResponse::Ok),
IDCANCEL => Ok(DialogResponse::Cancel),
IDYES => Ok(DialogResponse::Yes),
IDNO => Ok(DialogResponse::No),
0 => Err(ZsuiError::host(
"windows_native_dialog",
format!("MessageBoxW failed with Win32 error {}", unsafe {
GetLastError()
}),
)),
other => Err(ZsuiError::host(
"windows_native_dialog",
format!("MessageBoxW returned unexpected response {other}"),
)),
}
}
fn windows_native_dialog_custom_button_order<'a>(
buttons: DialogButtons,
labels: &'a DialogButtonLabels,
) -> Vec<(DialogResponse, &'a str)> {
match buttons {
DialogButtons::Ok => vec![(DialogResponse::Ok, &labels.ok)],
DialogButtons::OkCancel => vec![
(DialogResponse::Ok, &labels.ok),
(DialogResponse::Cancel, &labels.cancel),
],
DialogButtons::YesNo => vec![
(DialogResponse::Yes, &labels.yes),
(DialogResponse::No, &labels.no),
],
DialogButtons::YesNoCancel => vec![
(DialogResponse::Yes, &labels.yes),
(DialogResponse::No, &labels.no),
(DialogResponse::Cancel, &labels.cancel),
],
}
}
fn windows_native_dialog_response_id(response: DialogResponse) -> i32 {
match response {
DialogResponse::Ok => IDOK,
DialogResponse::Cancel => IDCANCEL,
DialogResponse::Yes => IDYES,
DialogResponse::No => IDNO,
}
}
fn windows_native_dialog_button_style(buttons: DialogButtons) -> u32 {
match buttons {
DialogButtons::Ok => MB_OK,
DialogButtons::OkCancel => MB_OKCANCEL,
DialogButtons::YesNo => MB_YESNO,
DialogButtons::YesNoCancel => MB_YESNOCANCEL,
}
}
fn windows_native_dialog_level_style(level: DialogLevel) -> u32 {
match level {
DialogLevel::Info | DialogLevel::Question => MB_ICONINFORMATION,
DialogLevel::Warning => MB_ICONWARNING,
DialogLevel::Error => MB_ICONERROR,
}
}
fn windows_common_dialog_cancel_or_error<T>(operation: &'static str) -> ZsuiResult<Option<T>> {
let error = unsafe { CommDlgExtendedError() };
if error == 0 {
Ok(None)
} else {
Err(ZsuiError::host(
operation,
format!("common dialog error 0x{error:08x}"),
))
}
}
fn windows_file_dialog_filter(filters: &[crate::FileDialogFilter]) -> Vec<u16> {
let mut output = Vec::new();
if filters.is_empty() {
append_windows_filter_part(&mut output, "All files");
append_windows_filter_part(&mut output, "*.*");
} else {
for filter in filters {
append_windows_filter_part(&mut output, &filter.name);
let patterns = if filter.patterns.is_empty() {
"*.*".to_string()
} else {
filter.patterns.join(";")
};
append_windows_filter_part(&mut output, &patterns);
}
}
output.push(0);
output
}
fn append_windows_filter_part(output: &mut Vec<u16>, value: &str) {
output.extend(value.encode_utf16());
output.push(0);
}
fn windows_file_dialog_default_extension(filters: &[crate::FileDialogFilter]) -> Option<Vec<u16>> {
filters
.iter()
.flat_map(|filter| &filter.patterns)
.find_map(|pattern| {
pattern
.strip_prefix("*.")
.filter(|extension| !extension.is_empty() && !extension.contains(['*', '?', ';']))
})
.map(|extension| extension.encode_utf16().chain(Some(0)).collect())
}
fn parse_windows_open_file_buffer(buffer: &[u16]) -> Vec<PathBuf> {
let parts = parse_windows_utf16_segments(buffer);
match parts.as_slice() {
[] => Vec::new(),
[path] => vec![PathBuf::from(path)],
[directory, names @ ..] => names
.iter()
.map(|name| PathBuf::from(directory).join(name))
.collect(),
}
}
fn parse_windows_utf16_segments(buffer: &[u16]) -> Vec<OsString> {
buffer
.split(|unit| *unit == 0)
.take_while(|segment| !segment.is_empty())
.map(OsString::from_wide)
.collect()
}