druid_shell/dialog.rs
1// Copyright 2019 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! File open/save dialogs.
16
17use std::path::{Path, PathBuf};
18
19/// Information about the path to be opened or saved.
20///
21/// This path might point to a file or a directory.
22#[derive(Debug, Clone)]
23pub struct FileInfo {
24 /// The path to the selected file.
25 ///
26 /// On macOS, this is already rewritten to use the extension that the user selected
27 /// with the `file format` property.
28 pub path: PathBuf,
29 /// The selected file format.
30 ///
31 /// If there're multiple different formats available
32 /// this allows understanding the kind of format that the user expects the file
33 /// to be written in. Examples could be Blender 2.4 vs Blender 2.6 vs Blender 2.8.
34 /// The `path` above will already contain the appropriate extension chosen in the
35 /// `format` property, so it is not necessary to mutate `path` any further.
36 pub format: Option<FileSpec>,
37}
38
39/// Type of file dialog.
40#[cfg(not(any(
41 all(
42 feature = "x11",
43 any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")
44 ),
45 feature = "wayland"
46)))]
47#[derive(Clone, Copy, PartialEq, Eq)]
48pub enum FileDialogType {
49 /// File open dialog.
50 Open,
51 /// File save dialog.
52 Save,
53}
54
55/// Options for file dialogs.
56///
57/// File dialogs let the user choose a specific path to open or save.
58///
59/// By default the file dialogs operate in *files mode* where the user can only choose files.
60/// Importantly these are files from the user's perspective, but technically the returned path
61/// will be a directory when the user chooses a package. You can read more about [packages] below.
62/// It's also possible for users to manually specify a path which they might otherwise not be able
63/// to choose. Thus it is important to verify that all the returned paths match your expectations.
64///
65/// The open dialog can also be switched to *directories mode* via [`select_directories`].
66///
67/// # Cross-platform compatibility
68///
69/// You could write platform specific code that really makes the best use of each platform.
70/// However if you want to write universal code that will work on all platforms then
71/// you have to keep some restrictions in mind.
72///
73/// ## Don't depend on directories with extensions
74///
75/// Your application should avoid having to deal with directories that have extensions
76/// in their name, e.g. `my_stuff.pkg`. This clashes with [packages] on macOS and you
77/// will either need platform specific code or a degraded user experience on macOS
78/// via [`packages_as_directories`].
79///
80/// ## Use the save dialog only for new paths
81///
82/// Don't direct the user to choose an existing file with the save dialog.
83/// Selecting existing files for overwriting is possible but extremely cumbersome on macOS.
84/// The much more optimized flow is to have the user select a file with the open dialog
85/// and then keep saving to that file without showing a save dialog.
86/// Use the save dialog only for selecting a new location.
87///
88/// # macOS
89///
90/// The file dialog works a bit differently on macOS. For a lot of applications this doesn't matter
91/// and you don't need to know the details. However if your application makes extensive use
92/// of file dialogs and you target macOS then you should understand the macOS specifics.
93///
94/// ## Packages
95///
96/// On macOS directories with known extensions are considered to be packages, e.g. `app_files.pkg`.
97/// Furthermore the packages are divided into two groups based on their extension.
98/// First there are packages that have been defined at the OS level, and secondly there are
99/// packages that are defined at the file dialog level based on [`allowed_types`].
100/// These two types have slightly different behavior in the file dialogs. Generally packages
101/// behave similarly to regular files in many contexts, including the file dialogs.
102/// This package concept can be turned off in the file dialog via [`packages_as_directories`].
103///
104///  | Packages as files. File filters apply to packages. | Packages as directories.
105/// -------- | -------------------------------------------------- | ------------------------
106/// Open directory | Not selectable. Not traversable. | Selectable. Traversable.
107/// Open file | Selectable. Not traversable. | Not selectable. Traversable.
108/// Save file | OS packages [clickable] but not traversable.<br/>Dialog packages traversable but not selectable. | Not selectable. Traversable.
109///
110/// Keep in mind that the file dialog may start inside any package if the user has traversed
111/// into one just recently. The user might also manually specify a path inside a package.
112///
113/// Generally this behavior should be kept, because it's least surprising to macOS users.
114/// However if your application requires selecting directories with extensions as directories
115/// or the user needs to be able to traverse into them to select a specific file,
116/// then you can change the default behavior via [`packages_as_directories`]
117/// to force macOS to behave like other platforms and not give special treatment to packages.
118///
119/// ## Selecting files for overwriting in the save dialog is cumbersome
120///
121/// Existing files can be clicked on in the save dialog, but that only copies their base file name.
122/// If the clicked file's extension is different than the first extension of the default type
123/// then the returned path does not actually match the path of the file that was clicked on.
124/// Clicking on a file doesn't change the base path either. Keep in mind that the macOS file dialog
125/// can have several directories open at once. So if a user has traversed into `/Users/Joe/foo/`
126/// and then clicks on an existing file `/Users/Joe/old.txt` in another directory then the returned
127/// path will actually be `/Users/Joe/foo/old.rtf` if the default type's first extension is `rtf`.
128///
129/// [clickable]: #selecting-files-for-overwriting-in-the-save-dialog-is-cumbersome
130/// [packages]: #packages
131/// [`select_directories`]: #method.select_directories
132/// [`allowed_types`]: #method.allowed_types
133/// [`packages_as_directories`]: #method.packages_as_directories
134#[derive(Debug, Clone, Default)]
135pub struct FileDialogOptions {
136 pub(crate) show_hidden: bool,
137 pub(crate) allowed_types: Option<Vec<FileSpec>>,
138 pub(crate) default_type: Option<FileSpec>,
139 pub(crate) select_directories: bool,
140 pub(crate) packages_as_directories: bool,
141 pub(crate) multi_selection: bool,
142 pub(crate) default_name: Option<String>,
143 pub(crate) name_label: Option<String>,
144 pub(crate) title: Option<String>,
145 pub(crate) button_text: Option<String>,
146 pub(crate) starting_directory: Option<PathBuf>,
147}
148
149/// A description of a filetype, for specifying allowed types in a file dialog.
150///
151/// # Windows
152///
153/// Each instance of this type is converted to a [`COMDLG_FILTERSPEC`] struct.
154///
155/// # macOS
156///
157/// These file types also apply to directories to define them as [packages].
158///
159/// [`COMDLG_FILTERSPEC`]: https://docs.microsoft.com/en-ca/windows/win32/api/shtypes/ns-shtypes-comdlg_filterspec
160/// [packages]: struct.FileDialogOptions.html#packages
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub struct FileSpec {
163 /// A human readable name, describing this filetype.
164 ///
165 /// This is used in the Windows file dialog, where the user can select
166 /// from a dropdown the type of file they would like to choose.
167 ///
168 /// This should not include the file extensions; they will be added automatically.
169 /// For instance, if we are describing Word documents, the name would be "Word Document",
170 /// and the displayed string would be "Word Document (*.doc)".
171 pub name: &'static str,
172 /// The file extensions used by this file type.
173 ///
174 /// This should not include the leading '.'.
175 pub extensions: &'static [&'static str],
176}
177
178impl FileInfo {
179 /// Returns the underlying path.
180 pub fn path(&self) -> &Path {
181 &self.path
182 }
183}
184
185impl FileDialogOptions {
186 /// Create a new set of options.
187 pub fn new() -> FileDialogOptions {
188 FileDialogOptions::default()
189 }
190
191 /// Set hidden files and directories to be visible.
192 pub fn show_hidden(mut self) -> Self {
193 self.show_hidden = true;
194 self
195 }
196
197 /// Set directories to be selectable instead of files.
198 ///
199 /// This is only relevant for open dialogs.
200 pub fn select_directories(mut self) -> Self {
201 self.select_directories = true;
202 self
203 }
204
205 /// Set [packages] to be treated as directories instead of files.
206 ///
207 /// This allows for writing more universal cross-platform code at the cost of user experience.
208 ///
209 /// This is only relevant on macOS.
210 ///
211 /// [packages]: #packages
212 pub fn packages_as_directories(mut self) -> Self {
213 self.packages_as_directories = true;
214 self
215 }
216
217 /// Set multiple items to be selectable.
218 ///
219 /// This is only relevant for open dialogs.
220 pub fn multi_selection(mut self) -> Self {
221 self.multi_selection = true;
222 self
223 }
224
225 /// Set the file types the user is allowed to select.
226 ///
227 /// This filter is only applied to files and [packages], but not to directories.
228 ///
229 /// An empty collection is treated as no filter.
230 ///
231 /// # macOS
232 ///
233 /// These file types also apply to directories to define [packages].
234 /// Which means the directories that match the filter are no longer considered directories.
235 /// The packages are defined by this collection even in *directories mode*.
236 ///
237 /// [packages]: #packages
238 pub fn allowed_types(mut self, types: Vec<FileSpec>) -> Self {
239 // An empty vector can cause platform issues, so treat it as no filter
240 if types.is_empty() {
241 self.allowed_types = None;
242 } else {
243 self.allowed_types = Some(types);
244 }
245 self
246 }
247
248 /// Set the default file type.
249 ///
250 /// The provided `default_type` must also be present in [`allowed_types`].
251 ///
252 /// If it's `None` then the first entry in [`allowed_types`] will be used as the default.
253 ///
254 /// This is only relevant in *files mode*.
255 ///
256 /// [`allowed_types`]: #method.allowed_types
257 pub fn default_type(mut self, default_type: FileSpec) -> Self {
258 self.default_type = Some(default_type);
259 self
260 }
261
262 /// Set the default filename that appears in the dialog.
263 pub fn default_name(mut self, default_name: impl Into<String>) -> Self {
264 self.default_name = Some(default_name.into());
265 self
266 }
267
268 /// Set the text in the label next to the filename editbox.
269 pub fn name_label(mut self, name_label: impl Into<String>) -> Self {
270 self.name_label = Some(name_label.into());
271 self
272 }
273
274 /// Set the title text of the dialog.
275 pub fn title(mut self, title: impl Into<String>) -> Self {
276 self.title = Some(title.into());
277 self
278 }
279
280 /// Set the text of the Open/Save button.
281 pub fn button_text(mut self, text: impl Into<String>) -> Self {
282 self.button_text = Some(text.into());
283 self
284 }
285
286 /// Force the starting directory to the specified `path`.
287 ///
288 /// # User experience
289 ///
290 /// This should almost never be used because it overrides the OS choice,
291 /// which will usually be a directory that the user recently visited.
292 pub fn force_starting_directory(mut self, path: impl Into<PathBuf>) -> Self {
293 self.starting_directory = Some(path.into());
294 self
295 }
296}
297
298impl FileSpec {
299 pub const TEXT: FileSpec = FileSpec::new("Text", &["txt"]);
300 pub const JPG: FileSpec = FileSpec::new("Jpeg", &["jpg", "jpeg"]);
301 pub const GIF: FileSpec = FileSpec::new("Gif", &["gif"]);
302 pub const PNG: FileSpec = FileSpec::new("Portable network graphics (png)", &["png"]);
303 pub const PDF: FileSpec = FileSpec::new("PDF", &["pdf"]);
304 pub const HTML: FileSpec = FileSpec::new("Web Page", &["htm", "html"]);
305
306 /// Create a new `FileSpec`.
307 pub const fn new(name: &'static str, extensions: &'static [&'static str]) -> Self {
308 FileSpec { name, extensions }
309 }
310}