From 6168ea633eb9697d78f4ee0ce17bb075787f0c95 Mon Sep 17 00:00:00 2001 From: Jake Shadle <jake.shadle@embark-studios.com> Date: Wed, 26 Apr 2023 21:08:49 +0200 Subject: [PATCH] Replace `windows` with `windows-sys` (#118) --- .github/workflows/rust.yml | 4 +- Cargo.toml | 5 +- build.rs | 23 +- src/backend/win_cid/file_dialog.rs | 5 +- src/backend/win_cid/file_dialog/com.rs | 268 ++++++++++++++ src/backend/win_cid/file_dialog/dialog_ffi.rs | 328 +++++++++++------- .../win_cid/file_dialog/dialog_future.rs | 10 +- src/backend/win_cid/message_dialog.rs | 122 ++++--- src/backend/win_cid/utils.rs | 30 +- 9 files changed, 565 insertions(+), 230 deletions(-) create mode 100644 src/backend/win_cid/file_dialog/com.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8b38323..cd21fcd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -36,14 +36,14 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: "[Ubuntu] install dependencies" if: matrix.name == 'Ubuntu GTK' run: sudo apt update && sudo apt install libgtk-3-dev - name: "[WASM] rustup" if: matrix.name == 'WASM32' run: rustup target add wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --target ${{ matrix.target }} ${{ matrix.flags }} - name: Test diff --git a/Cargo.toml b/Cargo.toml index 06d23df..d860baa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ default = ["gtk3"] file-handle-inner = [] gtk3 = ["gtk-sys", "glib-sys", "gobject-sys"] xdg-portal = ["ashpd", "urlencoding", "pollster"] -common-controls-v6 = ["windows/Win32_UI_Controls"] +common-controls-v6 = ["windows-sys/Win32_UI_Controls"] [dev-dependencies] futures = "0.3.12" @@ -32,7 +32,7 @@ block = "0.1.6" objc-foundation = "0.1.1" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.44", features = [ +windows-sys = { version = "0.48", features = [ "Win32_Foundation", "Win32_System_Com", "Win32_UI_Shell_Common", @@ -76,4 +76,3 @@ name = "async" [package.metadata.docs.rs] features = ["file-handle-inner"] - diff --git a/build.rs b/build.rs index cf8219a..6ec72c3 100644 --- a/build.rs +++ b/build.rs @@ -1,13 +1,18 @@ fn main() { - let target = std::env::var("TARGET").unwrap(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("target OS not detected"); - if target.contains("darwin") { - println!("cargo:rustc-link-lib=framework=AppKit"); - } - - #[cfg(all(feature = "gtk3", feature = "xdg-portal"))] - compile_error!("You can't enable both GTK3 & XDG Portal features at once"); + match target_os.as_str() { + "macos" => println!("cargo:rustc-link-lib=framework=AppKit"), + "windows" => {} + _ => { + let gtk = std::env::var_os("CARGO_FEATURE_GTK3").is_some(); + let xdg = std::env::var_os("CARGO_FEATURE_XDG_PORTAL").is_some(); - #[cfg(not(any(feature = "gtk3", feature = "xdg-portal")))] - compile_error!("You need to choose at least one backend: `gtk3` or `xdg-portal` features"); + if gtk && xdg { + panic!("You can't enable both `gtk3` and `xdg-portal` features at once"); + } else if !gtk && !xdg { + panic!("You need to choose at least one backend: `gtk3` or `xdg-portal` features"); + } + } + } } diff --git a/src/backend/win_cid/file_dialog.rs b/src/backend/win_cid/file_dialog.rs index ae4481d..d5c414a 100644 --- a/src/backend/win_cid/file_dialog.rs +++ b/src/backend/win_cid/file_dialog.rs @@ -1,7 +1,8 @@ +mod com; pub mod dialog_ffi; mod dialog_future; -use dialog_ffi::IDialog; +use dialog_ffi::{IDialog, Result}; use dialog_future::{multiple_return_future, single_return_future}; use crate::backend::DialogFutureType; @@ -10,8 +11,6 @@ use crate::FileHandle; use std::path::PathBuf; -use windows::core::Result; - use super::utils::init_com; // diff --git a/src/backend/win_cid/file_dialog/com.rs b/src/backend/win_cid/file_dialog/com.rs new file mode 100644 index 0000000..acb192c --- /dev/null +++ b/src/backend/win_cid/file_dialog/com.rs @@ -0,0 +1,268 @@ +#![allow(non_snake_case)] + +use std::ffi::c_void; +use windows_sys::core::{HRESULT, PCWSTR, PWSTR}; +pub use windows_sys::{ + core::GUID, + Win32::{ + Foundation::HWND, + UI::Shell::{Common::COMDLG_FILTERSPEC, FILEOPENDIALOGOPTIONS, SIGDN, SIGDN_FILESYSPATH}, + }, +}; + +pub(crate) type Result<T> = std::result::Result<T, HRESULT>; + +#[inline] +pub(super) fn wrap_err(hresult: HRESULT) -> Result<()> { + if hresult >= 0 { + Ok(()) + } else { + Err(hresult) + } +} + +#[inline] +unsafe fn read_to_string(ptr: *const u16) -> String { + let mut cursor = ptr; + + while *cursor != 0 { + cursor = cursor.add(1); + } + + let slice = std::slice::from_raw_parts(ptr, cursor.offset_from(ptr) as usize); + String::from_utf16(slice).unwrap() +} + +#[repr(C)] +pub(super) struct Interface<T> { + vtable: *mut T, +} + +impl<T> Interface<T> { + #[inline] + pub(super) fn vtbl(&self) -> &T { + unsafe { &*self.vtable } + } +} + +#[repr(C)] +pub(super) struct IUnknownV { + __query_interface: usize, + __add_ref: usize, + pub(super) release: unsafe extern "system" fn(this: *mut c_void) -> u32, +} + +pub(super) type IUnknown = Interface<IUnknownV>; + +#[inline] +fn drop_impl(ptr: *mut c_void) { + unsafe { + ((*ptr.cast::<IUnknown>()).vtbl().release)(ptr); + } +} + +#[repr(C)] +pub(super) struct IShellItemV { + base: IUnknownV, + BindToHandler: unsafe extern "system" fn( + this: *mut c_void, + pbc: *mut c_void, + bhid: *const GUID, + riid: *const GUID, + ppv: *mut *mut c_void, + ) -> HRESULT, + GetParent: unsafe extern "system" fn(this: *mut c_void, ppsi: *mut *mut c_void) -> HRESULT, + GetDisplayName: unsafe extern "system" fn( + this: *mut c_void, + sigdnname: SIGDN, + ppszname: *mut PWSTR, + ) -> HRESULT, + GetAttributes: usize, + Compare: unsafe extern "system" fn( + this: *mut c_void, + psi: *mut c_void, + hint: u32, + piorder: *mut i32, + ) -> HRESULT, +} + +#[repr(transparent)] +pub(super) struct IShellItem(pub(super) *mut Interface<IShellItemV>); + +impl IShellItem { + pub(super) fn get_path(&self) -> Result<std::path::PathBuf> { + let filename = unsafe { + let mut dname = std::mem::MaybeUninit::uninit(); + wrap_err(((*self.0).vtbl().GetDisplayName)( + self.0.cast(), + SIGDN_FILESYSPATH, + dname.as_mut_ptr(), + ))?; + + let dname = dname.assume_init(); + let fname = read_to_string(dname); + windows_sys::Win32::System::Com::CoTaskMemFree(dname.cast()); + fname + }; + + Ok(filename.into()) + } +} + +impl Drop for IShellItem { + fn drop(&mut self) { + drop_impl(self.0.cast()); + } +} + +#[repr(C)] +struct IShellItemArrayV { + base: IUnknownV, + BindToHandler: unsafe extern "system" fn( + this: *mut c_void, + pbc: *mut c_void, + bhid: *const GUID, + riid: *const GUID, + ppvout: *mut *mut c_void, + ) -> HRESULT, + GetPropertyStore: usize, + GetPropertyDescriptionList: usize, + GetAttributes: usize, + GetCount: unsafe extern "system" fn(this: *mut c_void, pdwnumitems: *mut u32) -> HRESULT, + GetItemAt: unsafe extern "system" fn( + this: *mut c_void, + dwindex: u32, + ppsi: *mut IShellItem, + ) -> HRESULT, + EnumItems: + unsafe extern "system" fn(this: *mut c_void, ppenumshellitems: *mut *mut c_void) -> HRESULT, +} + +#[repr(transparent)] +pub(super) struct IShellItemArray(*mut Interface<IShellItemArrayV>); + +impl IShellItemArray { + #[inline] + pub(super) fn get_count(&self) -> Result<u32> { + let mut count = 0; + unsafe { + wrap_err(((*self.0).vtbl().GetCount)(self.0.cast(), &mut count))?; + } + Ok(count) + } + + #[inline] + pub(super) fn get_item_at(&self, index: u32) -> Result<IShellItem> { + let mut item = std::mem::MaybeUninit::uninit(); + unsafe { + wrap_err(((*self.0).vtbl().GetItemAt)( + self.0.cast(), + index, + item.as_mut_ptr(), + ))?; + Ok(item.assume_init()) + } + } +} + +impl Drop for IShellItemArray { + fn drop(&mut self) { + drop_impl(self.0.cast()); + } +} + +#[repr(C)] +pub(super) struct IModalWindowV { + base: IUnknownV, + pub(super) Show: unsafe extern "system" fn(this: *mut c_void, owner: HWND) -> HRESULT, +} + +/// <https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifiledialog> +#[repr(C)] +pub(super) struct IFileDialogV { + pub(super) base: IModalWindowV, + pub(super) SetFileTypes: unsafe extern "system" fn( + this: *mut c_void, + cfiletypes: u32, + rgfilterspec: *const COMDLG_FILTERSPEC, + ) -> HRESULT, + SetFileTypeIndex: unsafe extern "system" fn(this: *mut c_void, ifiletype: u32) -> HRESULT, + GetFileTypeIndex: unsafe extern "system" fn(this: *mut c_void, pifiletype: *mut u32) -> HRESULT, + Advise: unsafe extern "system" fn( + this: *mut c_void, + pfde: *mut c_void, + pdwcookie: *mut u32, + ) -> HRESULT, + Unadvise: unsafe extern "system" fn(this: *mut c_void, dwcookie: u32) -> HRESULT, + pub(super) SetOptions: + unsafe extern "system" fn(this: *mut c_void, fos: FILEOPENDIALOGOPTIONS) -> HRESULT, + GetOptions: + unsafe extern "system" fn(this: *mut c_void, pfos: *mut FILEOPENDIALOGOPTIONS) -> HRESULT, + SetDefaultFolder: unsafe extern "system" fn(this: *mut c_void, psi: *mut c_void) -> HRESULT, + pub(super) SetFolder: unsafe extern "system" fn(this: *mut c_void, psi: *mut c_void) -> HRESULT, + GetFolder: unsafe extern "system" fn(this: *mut c_void, ppsi: *mut *mut c_void) -> HRESULT, + GetCurrentSelection: + unsafe extern "system" fn(this: *mut c_void, ppsi: *mut *mut c_void) -> HRESULT, + pub(super) SetFileName: + unsafe extern "system" fn(this: *mut c_void, pszname: PCWSTR) -> HRESULT, + GetFileName: unsafe extern "system" fn(this: *mut c_void, pszname: *mut PWSTR) -> HRESULT, + pub(super) SetTitle: unsafe extern "system" fn(this: *mut c_void, psztitle: PCWSTR) -> HRESULT, + SetOkButtonLabel: unsafe extern "system" fn(this: *mut c_void, psztext: PCWSTR) -> HRESULT, + SetFileNameLabel: unsafe extern "system" fn(this: *mut c_void, pszlabel: PCWSTR) -> HRESULT, + pub(super) GetResult: + unsafe extern "system" fn(this: *mut c_void, ppsi: *mut IShellItem) -> HRESULT, + AddPlace: unsafe extern "system" fn( + this: *mut c_void, + psi: *mut c_void, + fdap: windows_sys::Win32::UI::Shell::FDAP, + ) -> HRESULT, + pub(super) SetDefaultExtension: + unsafe extern "system" fn(this: *mut c_void, pszdefaultextension: PCWSTR) -> HRESULT, + Close: unsafe extern "system" fn(this: *mut c_void, hr: HRESULT) -> HRESULT, + SetClientGuid: unsafe extern "system" fn(this: *mut c_void, guid: *const GUID) -> HRESULT, + ClearClientData: unsafe extern "system" fn(this: *mut c_void) -> HRESULT, + SetFilter: unsafe extern "system" fn(this: *mut c_void, pfilter: *mut c_void) -> HRESULT, +} + +#[repr(transparent)] +pub(super) struct IFileDialog(pub(super) *mut Interface<IFileDialogV>); + +impl Drop for IFileDialog { + fn drop(&mut self) { + drop_impl(self.0.cast()); + } +} + +/// <https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifileopendialog> +#[repr(C)] +pub(super) struct IFileOpenDialogV { + pub(super) base: IFileDialogV, + /// <https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileopendialog-getresults> + GetResults: + unsafe extern "system" fn(this: *mut c_void, ppenum: *mut IShellItemArray) -> HRESULT, + GetSelectedItems: + unsafe extern "system" fn(this: *mut c_void, ppsai: *mut *mut c_void) -> HRESULT, +} + +#[repr(transparent)] +pub(super) struct IFileOpenDialog(pub(super) *mut Interface<IFileOpenDialogV>); + +impl IFileOpenDialog { + #[inline] + pub(super) fn get_results(&self) -> Result<IShellItemArray> { + let mut res = std::mem::MaybeUninit::uninit(); + unsafe { + wrap_err((((*self.0).vtbl()).GetResults)( + self.0.cast(), + res.as_mut_ptr(), + ))?; + Ok(res.assume_init()) + } + } +} + +impl Drop for IFileOpenDialog { + fn drop(&mut self) { + drop_impl(self.0.cast()); + } +} diff --git a/src/backend/win_cid/file_dialog/dialog_ffi.rs b/src/backend/win_cid/file_dialog/dialog_ffi.rs index 5a87ed9..c0f73a4 100644 --- a/src/backend/win_cid/file_dialog/dialog_ffi.rs +++ b/src/backend/win_cid/file_dialog/dialog_ffi.rs @@ -1,161 +1,257 @@ +use super::super::utils::str_to_vec_u16; +pub(crate) use super::com::Result; +use super::com::{ + wrap_err, IFileDialog, IFileDialogV, IFileOpenDialog, IShellItem, COMDLG_FILTERSPEC, + FILEOPENDIALOGOPTIONS, HWND, +}; use crate::FileDialog; -use std::{ffi::OsStr, iter::once, os::windows::ffi::OsStrExt, path::PathBuf}; - -use windows::core::{Result, PCWSTR, PWSTR}; -use windows::Win32::{ - Foundation::HWND, - System::Com::{CoCreateInstance, CoTaskMemFree, CLSCTX_INPROC_SERVER}, - UI::Shell::{ - Common::COMDLG_FILTERSPEC, FileOpenDialog, FileSaveDialog, IFileDialog, IFileOpenDialog, - IFileSaveDialog, IShellItem, SHCreateItemFromParsingName, FILEOPENDIALOGOPTIONS, - FOS_ALLOWMULTISELECT, FOS_PICKFOLDERS, SIGDN_FILESYSPATH, +use windows_sys::{ + core::GUID, + Win32::{ + System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, + UI::Shell::{ + FileOpenDialog, FileSaveDialog, SHCreateItemFromParsingName, FOS_ALLOWMULTISELECT, + FOS_PICKFOLDERS, + }, }, }; +use std::{ffi::c_void, path::PathBuf}; + use raw_window_handle::RawWindowHandle; -unsafe fn read_to_string(ptr: PWSTR) -> String { - let mut len = 0usize; - let mut cursor = ptr; - loop { - let val = cursor.0.read(); - if val == 0 { - break; +enum DialogInner { + Open(IFileOpenDialog), + Save(IFileDialog), +} + +impl DialogInner { + unsafe fn new(open: bool) -> Result<Self> { + const FILE_OPEN_DIALOG_IID: GUID = GUID::from_u128(0xd57c7288_d4ad_4768_be02_9d969532d960); + const FILE_SAVE_DIALOG_IID: GUID = GUID::from_u128(0x84bccd23_5fde_4cdb_aea4_af64b83d78ab); + + unsafe { + let (cls_id, iid) = if open { + (&FileOpenDialog, &FILE_OPEN_DIALOG_IID) + } else { + (&FileSaveDialog, &FILE_SAVE_DIALOG_IID) + }; + + let mut iptr = std::mem::MaybeUninit::uninit(); + wrap_err(CoCreateInstance( + cls_id, + std::ptr::null_mut(), + CLSCTX_INPROC_SERVER, + iid, + iptr.as_mut_ptr(), + ))?; + + let iptr = iptr.assume_init(); + + Ok(if open { + Self::Open(IFileOpenDialog(iptr.cast())) + } else { + Self::Save(IFileDialog(iptr.cast())) + }) } - len += 1; - cursor = PWSTR(cursor.0.add(1)); } - let slice = std::slice::from_raw_parts(ptr.0, len); - String::from_utf16(slice).unwrap() -} + #[inline] + unsafe fn open() -> Result<Self> { + unsafe { Self::new(true) } + } -pub enum DialogKind { - Open(IFileOpenDialog), - Save(IFileSaveDialog), -} + #[inline] + unsafe fn save() -> Result<Self> { + unsafe { Self::new(false) } + } -impl DialogKind { - fn as_dialog(&self) -> IFileDialog { + #[inline] + unsafe fn fd(&self) -> (*mut c_void, &IFileDialogV) { match self { - Self::Open(d) => d.into(), - Self::Save(d) => d.into(), + Self::Save(s) => unsafe { (s.0.cast(), (*s.0).vtbl()) }, + Self::Open(o) => unsafe { (o.0.cast(), &(*o.0).vtbl().base) }, } } + + #[inline] + unsafe fn set_options(&self, opts: FILEOPENDIALOGOPTIONS) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.SetOptions)(d, opts)) + } + + #[inline] + unsafe fn set_title(&self, title: &[u16]) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.SetTitle)(d, title.as_ptr())) + } + + #[inline] + unsafe fn set_default_extension(&self, extension: &[u16]) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.SetDefaultExtension)(d, extension.as_ptr())) + } + + #[inline] + unsafe fn set_file_types(&self, specs: &[COMDLG_FILTERSPEC]) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.SetFileTypes)(d, specs.len() as _, specs.as_ptr())) + } + + #[inline] + unsafe fn set_filename(&self, fname: &[u16]) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.SetFileName)(d, fname.as_ptr())) + } + + #[inline] + unsafe fn set_folder(&self, folder: &IShellItem) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.SetFolder)(d, folder.0.cast())) + } + + #[inline] + unsafe fn show(&self, parent: Option<HWND>) -> Result<()> { + let (d, v) = self.fd(); + wrap_err((v.base.Show)(d, parent.unwrap_or_default())) + } + + #[inline] + unsafe fn get_result(&self) -> Result<PathBuf> { + let (d, v) = self.fd(); + let mut res = std::mem::MaybeUninit::uninit(); + wrap_err((v.GetResult)(d, res.as_mut_ptr()))?; + let res = res.assume_init(); + res.get_path() + } + + #[inline] + unsafe fn get_results(&self) -> Result<Vec<PathBuf>> { + let Self::Open(od) = self else { unreachable!() }; + + let items = od.get_results()?; + let count = items.get_count()?; + + let mut paths = Vec::with_capacity(count as usize); + for index in 0..count { + let item = items.get_item_at(index)?; + + let path = item.get_path()?; + paths.push(path); + } + + Ok(paths) + } } -pub struct IDialog(pub DialogKind, Option<HWND>); +pub struct IDialog(DialogInner, Option<HWND>); impl IDialog { fn new_open_dialog(opt: &FileDialog) -> Result<Self> { - let dialog: IFileOpenDialog = - unsafe { CoCreateInstance(&FileOpenDialog, None, CLSCTX_INPROC_SERVER)? }; + let dialog = unsafe { DialogInner::open()? }; let parent = match opt.parent { - Some(RawWindowHandle::Win32(handle)) => Some(HWND(handle.hwnd as _)), + Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd as _), None => None, _ => unreachable!("unsupported window handle, expected: Windows"), }; - Ok(Self(DialogKind::Open(dialog), parent)) + Ok(Self(dialog, parent)) } fn new_save_dialog(opt: &FileDialog) -> Result<Self> { - let dialog: IFileSaveDialog = - unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_INPROC_SERVER)? }; + let dialog = unsafe { DialogInner::save()? }; let parent = match opt.parent { - Some(RawWindowHandle::Win32(handle)) => Some(HWND(handle.hwnd as _)), + Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd as _), None => None, _ => unreachable!("unsupported window handle, expected: Windows"), }; - Ok(Self(DialogKind::Save(dialog), parent)) + Ok(Self(dialog, parent)) } fn add_filters(&self, filters: &[crate::file_dialog::Filter]) -> Result<()> { - if let Some(first_filter) = filters.first() { + { + let Some(first_filter) = filters.first() else { return Ok(()) }; if let Some(first_extension) = first_filter.extensions.first() { - let mut extension: Vec<u16> = - first_extension.encode_utf16().chain(Some(0)).collect(); - unsafe { - self.0 - .as_dialog() - .SetDefaultExtension(PCWSTR(extension.as_mut_ptr()))?; - } + let extension = str_to_vec_u16(first_extension); + unsafe { self.0.set_default_extension(&extension)? } } } - let mut f_list = { + let f_list = { let mut f_list = Vec::new(); + let mut ext_string = String::new(); for f in filters.iter() { - let name: Vec<u16> = OsStr::new(&f.name).encode_wide().chain(once(0)).collect(); - let ext_string = f - .extensions - .iter() - .map(|item| format!("*.{}", item)) - .collect::<Vec<_>>() - .join(";"); - - let ext: Vec<u16> = OsStr::new(&ext_string) - .encode_wide() - .chain(once(0)) - .collect(); - - f_list.push((name, ext)); + let name = str_to_vec_u16(&f.name); + ext_string.clear(); + + for ext in &f.extensions { + use std::fmt::Write; + // This is infallible for String (barring OOM) + let _ = write!(&mut ext_string, "*.{ext};"); + } + + // pop trailing ; + ext_string.pop(); + + f_list.push((name, str_to_vec_u16(&ext_string))); } f_list }; let spec: Vec<_> = f_list - .iter_mut() + .iter() .map(|(name, ext)| COMDLG_FILTERSPEC { - pszName: PCWSTR(name.as_mut_ptr()), - pszSpec: PCWSTR(ext.as_mut_ptr()), + pszName: name.as_ptr(), + pszSpec: ext.as_ptr(), }) .collect(); unsafe { - if !spec.is_empty() { - self.0.as_dialog().SetFileTypes(&spec)?; - } + self.0.set_file_types(&spec)?; } Ok(()) } fn set_path(&self, path: &Option<PathBuf>) -> Result<()> { - if let Some(path) = path { - if let Some(path) = path.to_str() { - // Strip Win32 namespace prefix from the path - let path = path.strip_prefix(r"\\?\").unwrap_or(path); - - let mut wide_path: Vec<u16> = - OsStr::new(path).encode_wide().chain(once(0)).collect(); - - unsafe { - let item: Option<IShellItem> = - SHCreateItemFromParsingName(PCWSTR(wide_path.as_mut_ptr()), None).ok(); - - if let Some(item) = item { - // For some reason SetDefaultFolder(), does not guarantees default path, so we use SetFolder - self.0.as_dialog().SetFolder(&item)?; - } - } + const SHELL_ITEM_IID: GUID = GUID::from_u128(0x43826d1e_e718_42ee_bc55_a1e261c37bfe); + + let Some(path) = path.as_ref().and_then(|p| p.to_str()) else { return Ok(()) }; + + // Strip Win32 namespace prefix from the path + let path = path.strip_prefix(r"\\?\").unwrap_or(path); + + let wide_path = str_to_vec_u16(path); + + unsafe { + let mut item = std::mem::MaybeUninit::uninit(); + if wrap_err(SHCreateItemFromParsingName( + wide_path.as_ptr(), + std::ptr::null_mut(), + &SHELL_ITEM_IID, + item.as_mut_ptr(), + )) + .is_ok() + { + let item = IShellItem(item.assume_init().cast()); + // For some reason SetDefaultFolder(), does not guarantees default path, so we use SetFolder + self.0.set_folder(&item)?; } } + Ok(()) } fn set_file_name(&self, file_name: &Option<String>) -> Result<()> { if let Some(path) = file_name { - let mut wide_path: Vec<u16> = OsStr::new(path).encode_wide().chain(once(0)).collect(); + let wide_path = str_to_vec_u16(path); unsafe { - self.0 - .as_dialog() - .SetFileName(PCWSTR(wide_path.as_mut_ptr()))?; + self.0.set_filename(&wide_path)?; } } Ok(()) @@ -163,61 +259,25 @@ impl IDialog { fn set_title(&self, title: &Option<String>) -> Result<()> { if let Some(title) = title { - let mut wide_title: Vec<u16> = OsStr::new(title).encode_wide().chain(once(0)).collect(); + let wide_title = str_to_vec_u16(title); unsafe { - self.0 - .as_dialog() - .SetTitle(PCWSTR(wide_title.as_mut_ptr()))?; + self.0.set_title(&wide_title)?; } } Ok(()) } pub fn get_results(&self) -> Result<Vec<PathBuf>> { - unsafe { - let dialog = if let DialogKind::Open(ref d) = self.0 { - d - } else { - unreachable!() - }; - - let items = dialog.GetResults()?; - - let count = items.GetCount()?; - - let mut paths = Vec::new(); - for id in 0..count { - let res_item = items.GetItemAt(id)?; - - let display_name = res_item.GetDisplayName(SIGDN_FILESYSPATH)?; - - let filename = read_to_string(display_name); - - CoTaskMemFree(Some(display_name.0 as _)); - - let path = PathBuf::from(filename); - paths.push(path); - } - - Ok(paths) - } + unsafe { self.0.get_results() } } pub fn get_result(&self) -> Result<PathBuf> { - unsafe { - let res_item = self.0.as_dialog().GetResult()?; - let display_name = res_item.GetDisplayName(SIGDN_FILESYSPATH)?; - - let filename = read_to_string(display_name); - CoTaskMemFree(Some(display_name.0 as _)); - - Ok(PathBuf::from(filename)) - } + unsafe { self.0.get_result() } } pub fn show(&self) -> Result<()> { - unsafe { self.0.as_dialog().Show(self.1) } + unsafe { self.0.show(self.1) } } } @@ -251,7 +311,7 @@ impl IDialog { dialog.set_title(&opt.title)?; unsafe { - dialog.0.as_dialog().SetOptions(FOS_PICKFOLDERS as _)?; + dialog.0.set_options(FOS_PICKFOLDERS)?; } Ok(dialog) @@ -262,10 +322,10 @@ impl IDialog { dialog.set_path(&opt.starting_directory)?; dialog.set_title(&opt.title)?; - let opts = FILEOPENDIALOGOPTIONS(FOS_PICKFOLDERS.0 | FOS_ALLOWMULTISELECT.0); + let opts = FOS_PICKFOLDERS | FOS_ALLOWMULTISELECT; unsafe { - dialog.0.as_dialog().SetOptions(opts)?; + dialog.0.set_options(opts)?; } Ok(dialog) @@ -280,7 +340,7 @@ impl IDialog { dialog.set_title(&opt.title)?; unsafe { - dialog.0.as_dialog().SetOptions(FOS_ALLOWMULTISELECT as _)?; + dialog.0.set_options(FOS_ALLOWMULTISELECT)?; } Ok(dialog) diff --git a/src/backend/win_cid/file_dialog/dialog_future.rs b/src/backend/win_cid/file_dialog/dialog_future.rs index 2ab5f14..2ebf72c 100644 --- a/src/backend/win_cid/file_dialog/dialog_future.rs +++ b/src/backend/win_cid/file_dialog/dialog_future.rs @@ -2,15 +2,13 @@ use super::super::thread_future::ThreadFuture; use super::super::utils::init_com; use super::dialog_ffi::IDialog; -use windows::core::Result; - use crate::file_handle::FileHandle; -pub fn single_return_future<F: FnOnce() -> Result<IDialog> + Send + 'static>( +pub fn single_return_future<F: FnOnce() -> Result<IDialog, i32> + Send + 'static>( build: F, ) -> ThreadFuture<Option<FileHandle>> { ThreadFuture::new(move |data| { - let ret: Result<()> = (|| { + let ret: Result<(), i32> = (|| { init_com(|| { let dialog = build()?; dialog.show()?; @@ -28,11 +26,11 @@ pub fn single_return_future<F: FnOnce() -> Result<IDialog> + Send + 'static>( }) } -pub fn multiple_return_future<F: FnOnce() -> Result<IDialog> + Send + 'static>( +pub fn multiple_return_future<F: FnOnce() -> Result<IDialog, i32> + Send + 'static>( build: F, ) -> ThreadFuture<Option<Vec<FileHandle>>> { ThreadFuture::new(move |data| { - let ret: Result<()> = (|| { + let ret: Result<(), i32> = (|| { init_com(|| { let dialog = build()?; dialog.show()?; diff --git a/src/backend/win_cid/message_dialog.rs b/src/backend/win_cid/message_dialog.rs index 0f33c97..a9f6300 100644 --- a/src/backend/win_cid/message_dialog.rs +++ b/src/backend/win_cid/message_dialog.rs @@ -1,24 +1,20 @@ use super::thread_future::ThreadFuture; +use super::utils::str_to_vec_u16; use crate::message_dialog::{MessageButtons, MessageDialog, MessageLevel}; -use windows::{ - core::PCWSTR, - Win32::{ - Foundation::HWND, - UI::WindowsAndMessaging::{IDOK, IDYES}, - }, +use windows_sys::Win32::{ + Foundation::HWND, + UI::WindowsAndMessaging::{IDOK, IDYES}, }; #[cfg(not(feature = "common-controls-v6"))] -use windows::Win32::UI::WindowsAndMessaging::{ +use windows_sys::Win32::UI::WindowsAndMessaging::{ MessageBoxW, MB_ICONERROR, MB_ICONINFORMATION, MB_ICONWARNING, MB_OK, MB_OKCANCEL, MB_YESNO, MESSAGEBOX_STYLE, }; use raw_window_handle::RawWindowHandle; -use std::{ffi::OsStr, iter::once, os::windows::ffi::OsStrExt}; - pub struct WinMessageDialog { parent: Option<HWND>, text: Vec<u16>, @@ -33,10 +29,6 @@ pub struct WinMessageDialog { // fingers crossed unsafe impl Send for WinMessageDialog {} -fn str_to_vec_u16(str: &str) -> Vec<u16> { - OsStr::new(str).encode_wide().chain(once(0)).collect() -} - impl WinMessageDialog { pub fn new(opt: MessageDialog) -> Self { let text: Vec<u16> = str_to_vec_u16(&opt.description); @@ -57,7 +49,7 @@ impl WinMessageDialog { }; let parent = match opt.parent { - Some(RawWindowHandle::Win32(handle)) => Some(HWND(handle.hwnd as _)), + Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd as _), None => None, _ => unreachable!("unsupported window handle, expected: Windows"), }; @@ -74,57 +66,31 @@ impl WinMessageDialog { } #[cfg(feature = "common-controls-v6")] - pub fn run(mut self) -> bool { - use windows::Win32::{ - Foundation::BOOL, - UI::Controls::{ - TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOG_BUTTON, - TASKDIALOG_COMMON_BUTTON_FLAGS, TDCBF_CANCEL_BUTTON, TDCBF_NO_BUTTON, - TDCBF_OK_BUTTON, TDCBF_YES_BUTTON, TDF_ALLOW_DIALOG_CANCELLATION, - }, + pub fn run(self) -> bool { + use windows_sys::Win32::UI::Controls::{ + TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOGCONFIG_0, TASKDIALOGCONFIG_1, + TASKDIALOG_BUTTON, TDCBF_CANCEL_BUTTON, TDCBF_NO_BUTTON, TDCBF_OK_BUTTON, + TDCBF_YES_BUTTON, TDF_ALLOW_DIALOG_CANCELLATION, TD_ERROR_ICON, TD_INFORMATION_ICON, + TD_WARNING_ICON, }; - let mut pf_verification_flag_checked = BOOL(0); + let mut pf_verification_flag_checked = 0; let mut pn_button = 0; let mut pn_radio_button = 0; let id_custom_ok = 1000; let id_custom_cancel = 1001; - let mut task_dialog_config = TASKDIALOGCONFIG { - cbSize: core::mem::size_of::<TASKDIALOGCONFIG>() as u32, - hwndParent: self.parent.unwrap_or_default(), - dwFlags: TDF_ALLOW_DIALOG_CANCELLATION, - cButtons: 0, - pszWindowTitle: PCWSTR(self.caption.as_mut_ptr()), - pszContent: PCWSTR(self.text.as_mut_ptr()), - ..Default::default() - }; - let main_icon_ptr = match self.opt.level { - // `TD_WARNING_ICON` / `TD_ERROR_ICON` / `TD_INFORMATION_ICON` are missing in windows-rs - // https://github.com/microsoft/win32metadata/issues/968 - // Workaround via hard code: - // TD_WARNING_ICON - MessageLevel::Warning => -1 as i16 as u16, - // TD_ERROR_ICON - MessageLevel::Error => -2 as i16 as u16, - // TD_INFORMATION_ICON - MessageLevel::Info => -3 as i16 as u16, + MessageLevel::Warning => TD_WARNING_ICON, + MessageLevel::Error => TD_ERROR_ICON, + MessageLevel::Info => TD_INFORMATION_ICON, }; - task_dialog_config.Anonymous1.pszMainIcon = PCWSTR(main_icon_ptr as *const u16); - let (system_buttons, custom_buttons) = match self.opt.buttons { MessageButtons::Ok => (TDCBF_OK_BUTTON, vec![]), - MessageButtons::OkCancel => ( - TASKDIALOG_COMMON_BUTTON_FLAGS(TDCBF_OK_BUTTON.0 | TDCBF_CANCEL_BUTTON.0), - vec![], - ), - MessageButtons::YesNo => ( - TASKDIALOG_COMMON_BUTTON_FLAGS(TDCBF_YES_BUTTON.0 | TDCBF_NO_BUTTON.0), - vec![], - ), + MessageButtons::OkCancel => (TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON, vec![]), + MessageButtons::YesNo => (TDCBF_YES_BUTTON | TDCBF_NO_BUTTON, vec![]), MessageButtons::OkCustom(ok_text) => ( Default::default(), vec![(id_custom_ok, str_to_vec_u16(&ok_text))], @@ -142,32 +108,60 @@ impl WinMessageDialog { .iter() .map(|(id, text)| TASKDIALOG_BUTTON { nButtonID: *id, - pszButtonText: PCWSTR(text.as_ptr()), + pszButtonText: text.as_ptr(), }) .collect::<Vec<_>>(); - task_dialog_config.dwCommonButtons = system_buttons; - task_dialog_config.pButtons = p_buttons.as_ptr(); - task_dialog_config.cButtons = custom_buttons.len() as u32; + + let task_dialog_config = TASKDIALOGCONFIG { + cbSize: core::mem::size_of::<TASKDIALOGCONFIG>() as u32, + hwndParent: self.parent.unwrap_or_default(), + dwFlags: TDF_ALLOW_DIALOG_CANCELLATION, + pszWindowTitle: self.caption.as_ptr(), + pszContent: self.text.as_ptr(), + Anonymous1: TASKDIALOGCONFIG_0 { + pszMainIcon: main_icon_ptr, + }, + Anonymous2: TASKDIALOGCONFIG_1 { + pszFooterIcon: std::ptr::null(), + }, + dwCommonButtons: system_buttons, + pButtons: p_buttons.as_ptr(), + cButtons: custom_buttons.len() as u32, + pRadioButtons: std::ptr::null(), + cRadioButtons: 0, + cxWidth: 0, + hInstance: 0, + pfCallback: None, + lpCallbackData: 0, + nDefaultButton: 0, + nDefaultRadioButton: 0, + pszCollapsedControlText: std::ptr::null(), + pszExpandedControlText: std::ptr::null(), + pszExpandedInformation: std::ptr::null(), + pszMainInstruction: std::ptr::null(), + pszVerificationText: std::ptr::null(), + pszFooter: std::ptr::null(), + }; let ret = unsafe { TaskDialogIndirect( &task_dialog_config, - Some(&mut pn_button), - Some(&mut pn_radio_button), - Some(&mut pf_verification_flag_checked), + &mut pn_button, + &mut pn_radio_button, + &mut pf_verification_flag_checked, ) }; - ret.is_ok() && (pn_button == id_custom_ok || pn_button == IDYES.0 || pn_button == IDOK.0) + ret == 0 && (pn_button == id_custom_ok || pn_button == IDYES || pn_button == IDOK) } #[cfg(not(feature = "common-controls-v6"))] - pub fn run(mut self) -> bool { + pub fn run(self) -> bool { let ret = unsafe { MessageBoxW( - self.parent, - PCWSTR(self.text.as_mut_ptr()), - PCWSTR(self.caption.as_mut_ptr()), + self.parent.unwrap_or_default(), + self.text.as_ptr(), + self.caption.as_ptr(), self.flags, ) }; diff --git a/src/backend/win_cid/utils.rs b/src/backend/win_cid/utils.rs index d31b7cd..dd7b6a1 100644 --- a/src/backend/win_cid/utils.rs +++ b/src/backend/win_cid/utils.rs @@ -1,18 +1,30 @@ -use windows::core::Result; - -use windows::Win32::System::Com::{ - CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, +use windows_sys::{ + core::HRESULT, + Win32::System::Com::{ + CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, + }, }; -/// Makes sure that COM lib is initialized long enought -pub fn init_com<T, F: FnOnce() -> T>(f: F) -> Result<T> { - unsafe { +#[inline] +pub(crate) fn str_to_vec_u16(s: &str) -> Vec<u16> { + let mut v: Vec<_> = s.encode_utf16().collect(); + v.push(0); + v +} + +/// Makes sure that COM lib is initialized long enough +pub fn init_com<T, F: FnOnce() -> T>(f: F) -> Result<T, HRESULT> { + let res = unsafe { CoInitializeEx( - None, + std::ptr::null(), COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE, - )? + ) }; + if res < 0 { + return Err(res); + } + let out = f(); unsafe {