Skip to main content

winio_ui_windows_common/
filebox.rs

1use std::path::PathBuf;
2
3use widestring::U16CString;
4use windows::{
5    Win32::{
6        Foundation::{ERROR_CANCELLED, HWND},
7        System::Com::{
8            CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx,
9            CoTaskMemFree, CoUninitialize,
10        },
11        UI::Shell::{
12            Common::COMDLG_FILTERSPEC, FOS_ALLOWMULTISELECT, FOS_PICKFOLDERS, FileOpenDialog,
13            FileSaveDialog, IFileDialog, IFileOpenDialog, SIGDN_FILESYSPATH,
14        },
15    },
16    core::{HRESULT, Interface, PCWSTR},
17};
18use winio_handle::AsWindow;
19
20use crate::Result;
21
22#[derive(Debug, Default, Clone)]
23pub struct FileBox {
24    title: U16CString,
25    filename: U16CString,
26    filters: Vec<FileFilter>,
27}
28
29impl FileBox {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    pub fn title(&mut self, title: &str) {
35        self.title = U16CString::from_str_truncate(title);
36    }
37
38    pub fn filename(&mut self, filename: &str) {
39        self.filename = U16CString::from_str_truncate(filename);
40    }
41
42    pub fn filters(&mut self, filters: impl IntoIterator<Item = FileFilter>) {
43        self.filters = filters.into_iter().collect();
44    }
45
46    pub fn add_filter(&mut self, filter: FileFilter) {
47        self.filters.push(filter);
48    }
49
50    pub fn open(
51        self,
52        parent: Option<impl AsWindow>,
53    ) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
54        let parent = parent
55            .and_then(|p| p.as_window().handle().ok())
56            .map(|h| h as isize);
57        Ok(crate::spawn_blocking(move || {
58            let parent = parent.map(|w| HWND(w as _));
59            filebox(
60                parent,
61                self.title,
62                self.filename,
63                self.filters,
64                true,
65                false,
66                false,
67            )?
68            .result()
69        }))
70    }
71
72    pub fn open_multiple(
73        self,
74        parent: Option<impl AsWindow>,
75    ) -> Result<impl Future<Output = Result<Vec<PathBuf>>> + 'static> {
76        let parent = parent
77            .and_then(|p| p.as_window().handle().ok())
78            .map(|h| h as isize);
79        Ok(crate::spawn_blocking(move || {
80            let parent = parent.map(|w| HWND(w as _));
81            filebox(
82                parent,
83                self.title,
84                self.filename,
85                self.filters,
86                true,
87                true,
88                false,
89            )?
90            .results()
91        }))
92    }
93
94    pub fn open_folder(
95        self,
96        parent: Option<impl AsWindow>,
97    ) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
98        let parent = parent
99            .and_then(|p| p.as_window().handle().ok())
100            .map(|h| h as isize);
101        Ok(crate::spawn_blocking(move || {
102            let parent = parent.map(|w| HWND(w as _));
103            filebox(
104                parent,
105                self.title,
106                self.filename,
107                self.filters,
108                true,
109                false,
110                true,
111            )?
112            .result()
113        }))
114    }
115
116    pub fn save(
117        self,
118        parent: Option<impl AsWindow>,
119    ) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
120        let parent = parent
121            .and_then(|p| p.as_window().handle().ok())
122            .map(|h| h as isize);
123        Ok(crate::spawn_blocking(move || {
124            let parent = parent.map(|w| HWND(w as _));
125            filebox(
126                parent,
127                self.title,
128                self.filename,
129                self.filters,
130                false,
131                false,
132                false,
133            )?
134            .result()
135        }))
136    }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct FileFilter {
141    name: U16CString,
142    pattern: U16CString,
143}
144
145impl FileFilter {
146    pub fn new(name: &str, pattern: &str) -> Self {
147        Self {
148            name: U16CString::from_str_truncate(name),
149            pattern: U16CString::from_str_truncate(pattern),
150        }
151    }
152}
153
154fn filebox(
155    parent: Option<HWND>,
156    title: U16CString,
157    filename: U16CString,
158    filters: Vec<FileFilter>,
159    open: bool,
160    multiple: bool,
161    folder: bool,
162) -> Result<FileBoxInner> {
163    let init = CoInitialize::init()?;
164
165    unsafe {
166        let handle: IFileDialog = if open {
167            CoCreateInstance(&FileOpenDialog, None, CLSCTX_INPROC_SERVER)?
168        } else {
169            CoCreateInstance(&FileSaveDialog, None, CLSCTX_INPROC_SERVER)?
170        };
171
172        if !title.is_empty() {
173            handle.SetTitle(PCWSTR(title.as_ptr()))?;
174        }
175        if !filename.is_empty() {
176            handle.SetFileName(PCWSTR(filename.as_ptr()))?;
177        }
178
179        let types = filters
180            .iter()
181            .map(|filter| COMDLG_FILTERSPEC {
182                pszName: PCWSTR(filter.name.as_ptr()),
183                pszSpec: PCWSTR(filter.pattern.as_ptr()),
184            })
185            .collect::<Vec<_>>();
186        handle.SetFileTypes(&types)?;
187
188        if multiple {
189            debug_assert!(open, "Cannot save to multiple targets.");
190
191            let mut opts = handle.GetOptions()?;
192            opts |= FOS_ALLOWMULTISELECT;
193            handle.SetOptions(opts)?;
194        }
195
196        if folder {
197            debug_assert!(open, "Cannot save to a folder.");
198
199            let mut opts = handle.GetOptions()?;
200            opts |= FOS_PICKFOLDERS;
201            handle.SetOptions(opts)?;
202        }
203
204        let handle = match handle.Show(parent) {
205            Ok(()) => Some(handle),
206            Err(e) if e.code() == HRESULT::from(ERROR_CANCELLED) => None,
207            Err(e) => return Err(e),
208        };
209
210        Ok(FileBoxInner(handle, init))
211    }
212}
213
214struct FileBoxInner(Option<IFileDialog>, CoInitialize);
215
216impl FileBoxInner {
217    pub fn result(self) -> Result<Option<PathBuf>> {
218        if let Some(dialog) = self.0 {
219            unsafe {
220                let item = dialog.GetResult()?;
221                let name_ptr = item.GetDisplayName(SIGDN_FILESYSPATH)?;
222                let name_ptr = CoTaskMemPtr(name_ptr.0);
223                Ok(Some(PathBuf::from(name_ptr.to_string()?)))
224            }
225        } else {
226            Ok(None)
227        }
228    }
229
230    pub fn results(self) -> Result<Vec<PathBuf>> {
231        if let Some(dialog) = self.0 {
232            unsafe {
233                let handle: IFileOpenDialog = dialog.cast()?;
234                let results = handle.GetResults()?;
235                let count = results.GetCount()?;
236                let mut names = vec![];
237                for i in 0..count {
238                    let item = results.GetItemAt(i)?;
239                    let name_ptr = item.GetDisplayName(SIGDN_FILESYSPATH)?;
240                    let name_ptr = CoTaskMemPtr(name_ptr.0);
241                    let name = name_ptr.to_string()?;
242                    names.push(PathBuf::from(name));
243                }
244                Ok(names)
245            }
246        } else {
247            Ok(vec![])
248        }
249    }
250}
251
252pub struct CoTaskMemPtr<T>(*mut T);
253
254impl<T> CoTaskMemPtr<T> {
255    /// # Safety
256    /// Caller should ensure that the pointer is valid to be freed with
257    /// `CoTaskMemFree`.
258    pub unsafe fn new(ptr: *mut T) -> Self {
259        Self(ptr)
260    }
261
262    pub fn as_ptr(&self) -> *const T {
263        self.0
264    }
265
266    pub fn as_mut_ptr(&mut self) -> *mut T {
267        self.0
268    }
269}
270
271impl CoTaskMemPtr<u16> {
272    /// # Safety
273    /// Caller should ensure that the pointer is valid and points to a
274    /// null-terminated string.
275    pub unsafe fn to_string(&self) -> Result<String> {
276        Ok(unsafe { PCWSTR(self.as_ptr()).to_string()? })
277    }
278}
279
280impl<T> Drop for CoTaskMemPtr<T> {
281    fn drop(&mut self) {
282        unsafe { CoTaskMemFree(Some(self.0.cast())) }
283    }
284}
285
286struct CoInitialize;
287
288impl CoInitialize {
289    pub fn init() -> Result<Self> {
290        unsafe {
291            CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()?;
292        }
293        Ok(Self)
294    }
295}
296
297impl Drop for CoInitialize {
298    fn drop(&mut self) {
299        unsafe { CoUninitialize() };
300    }
301}