1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum DialogMode {
7 OpenFile,
9 OpenFiles,
11 PickFolder,
13 SaveFile,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Backend {
20 Auto,
22 Native,
24 ImGui,
26}
27
28impl Default for Backend {
29 fn default() -> Self {
30 Backend::Auto
31 }
32}
33
34#[derive(Clone, Debug, Default)]
40pub struct FileFilter {
41 pub name: String,
43 pub extensions: Vec<String>,
45}
46
47impl FileFilter {
48 pub fn new(name: impl Into<String>, exts: impl Into<Vec<String>>) -> Self {
53 let mut extensions: Vec<String> = exts.into();
54 for ext in &mut extensions {
57 *ext = ext.to_lowercase();
58 }
59 Self {
60 name: name.into(),
61 extensions,
62 }
63 }
64}
65
66impl From<(&str, &[&str])> for FileFilter {
67 fn from(value: (&str, &[&str])) -> Self {
68 Self {
69 name: value.0.to_owned(),
70 extensions: value.1.iter().map(|s| s.to_lowercase()).collect(),
71 }
72 }
73}
74
75#[derive(Clone, Debug, Default)]
77pub struct Selection {
78 pub paths: Vec<PathBuf>,
80}
81
82#[derive(Error, Debug)]
84pub enum FileDialogError {
85 #[error("cancelled")]
87 Cancelled,
88 #[error("io error: {0}")]
90 Io(#[from] std::io::Error),
91 #[error("unsupported operation for backend")]
93 Unsupported,
94 #[error("invalid path: {0}")]
96 InvalidPath(String),
97 #[error("internal error: {0}")]
99 Internal(String),
100}
101
102#[derive(Copy, Clone, Debug, PartialEq, Eq)]
104pub enum ClickAction {
105 Select,
107 Navigate,
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq)]
113pub enum LayoutStyle {
114 Standard,
116 Minimal,
118}
119
120#[derive(Copy, Clone, Debug, PartialEq, Eq)]
122pub enum SortBy {
123 Name,
125 Size,
127 Modified,
129}
130
131#[derive(Clone, Debug)]
133pub struct FileDialog {
134 pub(crate) backend: Backend,
135 pub(crate) mode: DialogMode,
136 pub(crate) start_dir: Option<PathBuf>,
137 pub(crate) default_name: Option<String>,
138 pub(crate) allow_multi: bool,
139 pub(crate) filters: Vec<FileFilter>,
140 pub(crate) show_hidden: bool,
141}
142
143impl FileDialog {
144 pub fn new(mode: DialogMode) -> Self {
146 Self {
147 backend: Backend::Auto,
148 mode,
149 start_dir: None,
150 default_name: None,
151 allow_multi: matches!(mode, DialogMode::OpenFiles),
152 filters: Vec::new(),
153 show_hidden: false,
154 }
155 }
156
157 pub fn backend(mut self, backend: Backend) -> Self {
159 self.backend = backend;
160 self
161 }
162 pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
164 self.start_dir = Some(dir.into());
165 self
166 }
167 pub fn default_file_name(mut self, name: impl Into<String>) -> Self {
169 self.default_name = Some(name.into());
170 self
171 }
172 pub fn multi_select(mut self, yes: bool) -> Self {
174 self.allow_multi = yes;
175 self
176 }
177 pub fn show_hidden(mut self, yes: bool) -> Self {
179 self.show_hidden = yes;
180 self
181 }
182 pub fn filter<F: Into<FileFilter>>(mut self, filter: F) -> Self {
193 self.filters.push(filter.into());
194 self
195 }
196 pub fn filters<I, F>(mut self, filters: I) -> Self
212 where
213 I: IntoIterator<Item = F>,
214 F: Into<FileFilter>,
215 {
216 self.filters.extend(filters.into_iter().map(Into::into));
217 self
218 }
219
220 pub(crate) fn effective_backend(&self) -> Backend {
222 match self.backend {
223 Backend::Native => Backend::Native,
224 Backend::ImGui => Backend::ImGui,
225 Backend::Auto => {
226 #[cfg(feature = "native-rfd")]
227 {
228 return Backend::Native;
229 }
230 #[cfg(not(feature = "native-rfd"))]
231 {
232 return Backend::ImGui;
233 }
234 }
235 }
236 }
237}
238
239#[cfg(not(feature = "native-rfd"))]
241impl FileDialog {
242 pub fn open_blocking(self) -> Result<Selection, FileDialogError> {
244 Err(FileDialogError::Unsupported)
245 }
246 pub async fn open_async(self) -> Result<Selection, FileDialogError> {
248 Err(FileDialogError::Unsupported)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn file_filter_new_normalizes_extensions_to_lowercase() {
258 let f = FileFilter::new(
259 "Images",
260 vec!["PNG".to_string(), "Jpg".to_string(), "gif".to_string()],
261 );
262 assert_eq!(f.extensions, vec!["png", "jpg", "gif"]);
263 }
264}