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