1use crate::key::Key;
17use crate::tree::OriginId;
18use crate::value::Value;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum FileDialogMode {
23 #[default]
25 Open,
26 Save,
29 Folder,
31}
32
33impl FileDialogMode {
34 pub const ALL: [FileDialogMode; 3] = [Self::Open, Self::Save, Self::Folder];
35
36 pub fn name(self) -> &'static str {
38 match self {
39 Self::Open => "open",
40 Self::Save => "save",
41 Self::Folder => "folder",
42 }
43 }
44
45 pub fn from_name(s: &str) -> Option<Self> {
46 Self::ALL.into_iter().find(|m| m.name() == s)
47 }
48}
49
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
53pub struct FileFilter {
54 pub name: String,
55 pub extensions: Vec<String>,
56}
57
58#[derive(Clone, Debug, Default, PartialEq)]
60pub struct FileDialog {
61 pub mode: FileDialogMode,
62 pub multiple: bool,
64 pub title: Option<String>,
66 pub filters: Vec<FileFilter>,
68 pub directory: Option<String>,
70 pub file_name: Option<String>,
72 pub tag: Value,
74}
75
76impl FileDialog {
77 pub fn open() -> Self {
79 Self::default()
80 }
81
82 pub fn save() -> Self {
84 Self {
85 mode: FileDialogMode::Save,
86 ..Self::default()
87 }
88 }
89
90 pub fn folder() -> Self {
92 Self {
93 mode: FileDialogMode::Folder,
94 ..Self::default()
95 }
96 }
97
98 pub fn multiple(mut self) -> Self {
99 self.multiple = true;
100 self
101 }
102
103 pub fn title(mut self, title: impl Into<String>) -> Self {
104 self.title = Some(title.into());
105 self
106 }
107
108 pub fn filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
113 self.filters.push(FileFilter {
114 name: name.into(),
115 extensions: extensions
116 .iter()
117 .map(|e| e.trim_start_matches('.').to_string())
118 .collect(),
119 });
120 self
121 }
122
123 pub fn directory(mut self, dir: impl Into<String>) -> Self {
124 self.directory = Some(dir.into());
125 self
126 }
127
128 pub fn file_name(mut self, name: impl Into<String>) -> Self {
129 self.file_name = Some(name.into());
130 self
131 }
132
133 pub fn tag(mut self, tag: impl Into<Value>) -> Self {
134 self.tag = tag.into();
135 self
136 }
137
138 pub fn to_value(&self) -> Value {
142 let mut entries = vec![
143 ("mode", Value::str(self.mode.name())),
144 ("multiple", Value::Bool(self.multiple)),
145 (
146 "filters",
147 Value::list(self.filters.iter().map(|f| {
148 Value::map([
149 ("name", Value::str(&f.name)),
150 (
151 "extensions",
152 Value::list(f.extensions.iter().map(Value::str)),
153 ),
154 ])
155 })),
156 ),
157 ("tag", self.tag.clone()),
158 ];
159 if let Some(t) = &self.title {
160 entries.push(("title", Value::str(t)));
161 }
162 if let Some(d) = &self.directory {
163 entries.push(("directory", Value::str(d)));
164 }
165 if let Some(n) = &self.file_name {
166 entries.push(("file_name", Value::str(n)));
167 }
168 Value::map(entries)
169 }
170
171 pub fn from_value(v: &Value) -> Result<Self, String> {
175 let s = |k: &str| v.get(k).and_then(Value::as_str).map(str::to_string);
176 let mode = match s("mode") {
177 None => FileDialogMode::Open,
178 Some(m) => FileDialogMode::from_name(&m)
179 .ok_or_else(|| format!("unknown dialog mode {m:?} (open, save or folder)"))?,
180 };
181 let mut filters = Vec::new();
182 if let Some(list) = v.get("filters").and_then(Value::as_list) {
183 for f in list {
184 let name = f
185 .get_str("name")
186 .ok_or("a filter needs a `name`")?
187 .to_string();
188 let extensions = f
189 .get("extensions")
190 .and_then(Value::as_list)
191 .unwrap_or(&[])
192 .iter()
193 .map(|e| {
194 e.as_str()
195 .map(|e| e.trim_start_matches('.').to_string())
196 .ok_or("a filter's `extensions` are strings")
197 })
198 .collect::<Result<Vec<_>, _>>()?;
199 filters.push(FileFilter { name, extensions });
200 }
201 }
202 Ok(FileDialog {
203 mode,
204 multiple: v.get_bool("multiple").unwrap_or(false),
205 title: s("title"),
206 filters,
207 directory: s("directory"),
208 file_name: s("file_name").or_else(|| s("fileName")),
209 tag: v.get("tag").cloned().unwrap_or(Value::Null),
210 })
211 }
212}
213
214#[derive(Clone, Debug, Default)]
216pub(crate) enum FileAsk {
217 #[default]
218 None,
219 Queued(FileDialog, OriginId),
221 Taken(Value, OriginId),
223}
224
225impl FileAsk {
226 pub(crate) fn pending(&self) -> bool {
227 !matches!(self, FileAsk::None)
228 }
229
230 pub(crate) fn answer(&mut self, paths: &[String]) -> Option<crate::input::UiEvent> {
233 let (tag, origin) = match std::mem::take(self) {
234 FileAsk::None => return None,
235 FileAsk::Queued(d, origin) => (d.tag, origin),
236 FileAsk::Taken(tag, origin) => (tag, origin),
237 };
238 Some(
239 crate::input::UiEvent::on(
240 origin,
241 Key::ROOT,
242 Value::map([
243 ("kind", Value::str("files")),
244 ("paths", Value::list(paths.iter().map(Value::str))),
245 ]),
246 )
247 .tagged(Some(&tag)),
248 )
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
259 fn an_extension_is_taken_without_its_dot_by_every_door() {
260 let dotted = FileDialog::open().filter("Text", &[".txt", "md"]);
261 assert_eq!(dotted.filters[0].extensions, ["txt", "md"]);
262 assert_eq!(
263 FileDialog::from_value(&dotted.to_value()).unwrap().filters,
264 FileDialog::open().filter("Text", &["txt", "md"]).filters
265 );
266 }
267}