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 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 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}