Skip to main content

ui/
filedlg.rs

1// filedlg -- Open / Save file dialog (ContentDialog with a folder list), both OS
2//                                                                  [src/ui/ui_filedlg.s]
3//
4//   filedlg(id, flags, open, path, cap, exts) -> EV_ACT when a file was chosen
5//        flags FD_OPEN / FD_SAVE / FD_FOLDER | index << FD_EXT_SHIFT (initially selected
6//        extension); FD_FOLDER chooses a folder (the own dialog lists folders only and returns
7//        the selected one, else the one shown).
8//        path (at most cap - 1 bytes, UTF-8) gives the initial file or folder (empty = the
9//        folder used last, else the home folder) and receives the chosen path ('/'
10//        separators on every OS; Windows accepts them).  exts = the extension list, one
11//        entry per type: "png=PNG-Bild", "jpg;jpeg;jfif=JPEG-Bild" (lower case; several
12//        extensions of one type separated by ';', the first is the one appended; optional
13//        "=description"; empty = every file); FD_SAVE appends the selected type's extension
14//        when the name has none.  Call it every frame like any dialog; open = 1 shows it.
15//        Where the system has a file dialog (Windows, the Linux portal) that one is shown
16//        (modal); Android's picker answers later: nothing is drawn while it is open, the
17//        chosen document arrives as its content:// name in a later frame.  Otherwise,
18//        headless or with jfgui_FILEDLG=builtin, the library's own dialog.
19//        FD_MULTI (E8, with FD_OPEN): several files -- the system's dialog where it chooses
20//        several (Windows, Linux's portal), the own dialog with Ctrl+click / Shift+click (or
21//        Shift+arrows) on the rows ("a" "b" in the name box, quoted names typed there too);
22//        path gets the first, ui.filedlg_paths() every chosen file.
23//        Widget ids used: id .. id + 0x3FFF (list rows id + 17 + i).
24//   filedlg_paths() -> the files of the last confirmed dialog (one without FD_MULTI, the folder
25//        of an FD_FOLDER dialog)
26//
27// One file dialog can be open at a time (the state below is shared).
28
29use alloc::string::String;
30use alloc::vec::Vec;
31
32use super::consts::*;
33use super::i18n::tr;
34use super::keys::*;
35use super::win::Shared;
36use super::Ui;
37
38const FD_MAX: usize = 8192; // entries per folder
39const FD_STORE: usize = 512 * 1024; // bytes of names per folder
40const FD_PATH: usize = 1040;
41const FD_DRIVES: usize = 256; // the plat::drives list (bytes, as the asm's buffer)
42const FD_NAME_CAP: usize = 256;
43const FD_DIRBIT: u32 = 0x8000_0000;
44
45/// state shared by all windows (one file dialog at a time)
46#[derive(Default)]
47pub(crate) struct S {
48    /// the names of the folder's entries, each ended by a NUL
49    store: Vec<u8>,
50    /// entries: offset in store | FD_DIRBIT for folders
51    idx: Vec<u32>,
52    /// the extension list of the open dialog
53    exts: Vec<String>,
54    msg: Option<&'static str>,
55    id: u32,
56    sel: i32,
57    /// the system's picker answers later (plat::FD_PENDING)
58    pending: bool,
59    /// FD_FOLDER: folders only, a folder is the answer
60    folder: bool,
61    dir: String,
62    last: String,
63    name: String,
64    /// (E8) FD_MULTI in the own dialog: the marked rows, the row a Shift+click counts from
65    marks: Vec<i32>,
66    anchor: i32,
67    /// (E8) the files of the last confirmed dialog
68    paths: Vec<String>,
69}
70
71// str_copy(src, cap): at most cap - 1 bytes, never splitting a UTF-8 sequence
72fn copy_cap(src: &str, cap: usize) -> String {
73    let src = super::basic::cstr(src);
74    if cap == 0 {
75        return String::new();
76    }
77    let mut n = src.len().min(cap - 1);
78    while !src.is_char_boundary(n) {
79        n -= 1;
80    }
81    String::from(&src[..n])
82}
83
84// str_cat(dst, src, cap): append, truncating like str_copy
85fn cat_cap(dst: &mut String, src: &str, cap: usize) {
86    if dst.len() + 1 >= cap {
87        return;
88    }
89    let room = cap - 1 - dst.len();
90    let src = super::basic::cstr(src);
91    let mut n = src.len().min(room);
92    while !src.is_char_boundary(n) {
93        n -= 1;
94    }
95    dst.push_str(&src[..n]);
96}
97
98// fd_norm(s): '\' -> '/'
99fn norm(s: &str) -> String {
100    s.replace('\\', "/")
101}
102
103// fd_is_root(s): "/" and "X:/"
104fn is_root(s: &str) -> bool {
105    let b = s.as_bytes();
106    match b.len() {
107        1 => b[0] == b'/',
108        3 => b[1] == b':' && b[2] == b'/',
109        _ => false,
110    }
111}
112
113// a path that names a root or starts at one ("/x", "\x", "C:...")
114fn is_abs(s: &str) -> bool {
115    let b = s.as_bytes();
116    !b.is_empty() && (b[0] == b'/' || b[0] == b'\\' || (b.len() > 1 && b[1] == b':'))
117}
118
119// fd_canon(p): resolve "." and ".." segments and repeated '/' ('/' paths; the root "/" or
120// "X:/" stays; ".." at the root is dropped)
121fn canon(p: &str) -> String {
122    let b = p.as_bytes();
123    let mut root = 0;
124    if !b.is_empty() && b[0] == b'/' {
125        root = 1;
126    } else if b.len() > 1 && b[1] == b':' {
127        root = 2;
128        if b.len() > 2 && b[2] == b'/' {
129            root = 3;
130        }
131    }
132    let mut out: Vec<u8> = b[..root].to_vec();
133    let mut r = root;
134    while r < b.len() {
135        if b[r] == b'/' {
136            r += 1;
137            continue;
138        }
139        let mut e = r;
140        while e < b.len() && b[e] != b'/' {
141            e += 1;
142        }
143        let seg = &b[r..e];
144        if seg == b"." {
145            // skip
146        } else if seg == b".." {
147            // drop the last written segment
148            while out.len() > root {
149                let c = out.pop();
150                if c == Some(b'/') {
151                    break;
152                }
153            }
154        } else {
155            if out.len() > root {
156                out.push(b'/');
157            }
158            out.extend_from_slice(seg);
159        }
160        r = e;
161    }
162    String::from_utf8(out).unwrap_or_default()
163}
164
165// fd_join(dst, name): dst = dst + '/' + name (FD_PATH bytes)
166fn join(dst: &mut String, name: &str) {
167    if !dst.is_empty() && !dst.ends_with('/') {
168        cat_cap(dst, "/", FD_PATH);
169    }
170    cat_cap(dst, name, FD_PATH);
171}
172
173// fd_ieq(a, b): equal ignoring ASCII case; b ends at '=' or ';'
174fn ieq(a: &[u8], b: &[u8]) -> bool {
175    let mut i = 0;
176    loop {
177        let x = a.get(i).copied().unwrap_or(0).to_ascii_lowercase();
178        let mut y = b.get(i).copied().unwrap_or(0);
179        if y == b'=' || y == b';' {
180            y = 0;
181        }
182        let y = y.to_ascii_lowercase();
183        if x != y {
184            return false;
185        }
186        if x == 0 {
187            return true;
188        }
189        i += 1;
190    }
191}
192
193impl S {
194    // fd_entry(i) -> (name, folder) (None when out of range)
195    fn entry(&self, i: i32) -> Option<(&str, bool)> {
196        if i < 0 || i as usize >= self.idx.len() {
197            return None;
198        }
199        let v = self.idx[i as usize];
200        let off = (v & !FD_DIRBIT) as usize;
201        let end = self.store[off..].iter().position(|&c| c == 0).map_or(self.store.len(), |n| off + n);
202        Some((core::str::from_utf8(&self.store[off..end]).unwrap_or(""), v & FD_DIRBIT != 0))
203    }
204
205    // fd_ext_ok(name): the name ends in one of the extensions (or there is no filter)
206    fn ext_ok(&self, name: &str) -> bool {
207        if self.exts.is_empty() || self.exts[0].is_empty() {
208            return true;
209        }
210        let Some(dot) = name.rfind('.') else { return false };
211        let ext = &name.as_bytes()[dot + 1..];
212        for e in &self.exts {
213            if e.is_empty() {
214                break;
215            }
216            // the extensions of this entry: "a;b;c=..."
217            let b = e.as_bytes();
218            let mut k = 0;
219            loop {
220                if ieq(ext, &b[k..]) {
221                    return true;
222                }
223                // skip to the next one after ';' (none after '=')
224                while k < b.len() && b[k] != b';' && b[k] != b'=' {
225                    k += 1;
226                }
227                if k >= b.len() || b[k] == b'=' {
228                    break;
229                }
230                k += 1;
231            }
232        }
233        false
234    }
235
236    // fd_sort: folders first, then the names in the user's collation with numbers by value
237    // ("Bild 2" before "Bild 10", "ä" with "a"), as the systems' file dialogs sort
238    fn sort(&mut self) {
239        let coll = super::i18n::collator().numeric(true);
240        let store = &self.store;
241        self.idx.sort_by_cached_key(|&v| {
242            let off = (v & !FD_DIRBIT) as usize;
243            let end = store[off..].iter().position(|&c| c == 0).map_or(store.len(), |n| off + n);
244            (v & FD_DIRBIT == 0, coll.key(core::str::from_utf8(&store[off..end]).unwrap_or("")))
245        });
246    }
247
248    // fd_set_dir(s): dir = s with '/' separators, no trailing separator except roots
249    // (E8) FD_MULTI: row i was selected with the modifiers mods -- Ctrl adds / removes it,
250    // Shift marks the files from the anchor to it, else it alone; the name box shows the
251    // marked names ("a" "b" for several)
252    fn mark(&mut self, i: i32, mods: u32) {
253        if self.entry(i).is_none_or(|(_, dir)| dir) {
254            return;
255        }
256        if mods & MOD_SHIFT != 0 && self.anchor >= 0 {
257            let (a, b) = (self.anchor.min(i), self.anchor.max(i));
258            self.marks = (a..=b).filter(|&k| self.entry(k).is_some_and(|(_, dir)| !dir)).collect();
259        } else if mods & MOD_CTRL != 0 {
260            if let Some(k) = self.marks.iter().position(|&m| m == i) {
261                self.marks.remove(k);
262            } else {
263                self.marks.push(i);
264            }
265            self.anchor = i;
266        } else {
267            self.marks = alloc::vec![i];
268            self.anchor = i;
269        }
270        let names: Vec<String> = self.marks.iter().filter_map(|&k| self.entry(k).map(|(n, _)| String::from(n))).collect();
271        self.name = match names.len() {
272            0 => String::new(),
273            1 => copy_cap(&names[0], FD_NAME_CAP),
274            _ => {
275                let mut s = String::new();
276                for n in &names {
277                    if !s.is_empty() {
278                        s.push(' ');
279                    }
280                    s.push('"');
281                    s.push_str(n);
282                    s.push('"');
283                }
284                s
285            }
286        };
287        self.msg = None;
288    }
289
290    fn set_dir(&mut self, s: &str) {
291        let mut d;
292        if is_abs(s) {
293            d = copy_cap(s, FD_PATH - 2);
294        } else {
295            // relative: below the current directory
296            d = norm(&copy_cap(&plat::cwd(), FD_PATH - 2));
297            join(&mut d, s);
298        }
299        let mut d = canon(&norm(&d));
300        loop {
301            if is_root(&d) {
302                break;
303            }
304            let b = d.as_bytes();
305            if b.len() == 2 && b[1] == b':' {
306                d.push('/'); // "C:" -> "C:/"
307                break;
308            }
309            if b.is_empty() {
310                d.push('/'); // empty -> "/"
311                break;
312            }
313            if b[b.len() - 1] != b'/' {
314                break;
315            }
316            d.pop();
317        }
318        self.dir = d;
319    }
320
321    // fd_parent: go to the parent folder (roots stay)
322    fn parent(&mut self) {
323        if is_root(&self.dir) {
324            return;
325        }
326        let Some(k) = self.dir.rfind('/') else { return };
327        if k == 0 || (k == 2 && self.dir.as_bytes()[1] == b':') {
328            self.dir.truncate(k + 1); // "/" or "C:/"
329        } else {
330            self.dir.truncate(k);
331        }
332    }
333
334    // fd_enter(name): open the sub folder name
335    fn enter(&mut self, name: &str) {
336        let mut d = core::mem::take(&mut self.dir);
337        join(&mut d, name);
338        self.dir = d;
339        self.name.clear();
340    }
341}
342
343impl Ui {
344    /// ui_filedlg(id, flags, open, path, cap, exts) -> EV_ACT when confirmed
345    #[allow(clippy::too_many_arguments)]
346    #[cfg_attr(jfgui_dev, track_caller)]
347    pub fn filedlg(&mut self, id: u32, flags: u32, open: &Shared<i32>, path: &mut String, cap: u32, exts: &[&str]) -> u32 {
348        let _src = self.src_scope();
349        let mut result = 0;
350        // first frame of an opening: load the start folder
351        if open.get() != 0 && self.m.filedlg.id != id {
352            // the system dialog, when there is one (modal: returns when the user is done)
353            let r = self.fd_system(flags, path, cap, exts);
354            if r == 1 {
355                // (E8) every chosen file: the system's dialog may choose several (FD_MULTI)
356                let all = plat::file_dialog_paths();
357                self.m.filedlg.paths = if flags & FD_MULTI != 0 && all.len() > 1 && all[0] == *path {
358                    all.iter().map(|p| copy_cap(p, cap as usize)).collect()
359                } else {
360                    alloc::vec![path.clone()]
361                };
362            }
363            if r == plat::FD_PENDING {
364                // the system's picker answers later (Android): wait for it
365                self.m.filedlg.id = id;
366                self.m.filedlg.pending = true;
367            } else if r != -1 {
368                open.set(0);
369                if r != 0 {
370                    result = EV_ACT;
371                }
372                self.repass();
373            } else {
374                self.fd_init(id, flags, path, exts);
375            }
376        }
377        if self.m.filedlg.pending && self.m.filedlg.id == id {
378            return self.fd_wait(open, path, cap);
379        }
380        let folder = flags & FD_FOLDER != 0;
381        let title = tr(if folder { "Ordner auswählen" } else if flags & FD_SAVE != 0 { "Speichern unter" } else { "Öffnen" });
382        if !self.dialog_begin(id, title, open) {
383            if self.m.filedlg.id == id {
384                self.m.filedlg.id = 0;
385            }
386            self.dialog_end();
387            if result == EV_ACT {
388                for p in self.m.filedlg.paths.clone() {
389                    super::perm::grant_user(&p); // the system dialog: the user chose it (fs.user)
390                }
391            }
392            return result;
393        }
394        self.fd_toolbar_row(id);
395        // folder contents
396        self.next_size(480, 300);
397        let mut sel = self.m.filedlg.sel;
398        let n = self.m.filedlg.idx.len() as i32;
399        let (first, end) = self.list_begin(id.wrapping_add(16), &mut sel, n, 32);
400        for i in first..end {
401            self.fd_row(i);
402        }
403        let (ev, sel) = self.list_end(&mut sel);
404        self.m.filedlg.sel = sel;
405        if ev & EV_DOUBLE != 0 {
406            if self.fd_activate(sel) && self.fd_confirm(flags, path, cap) {
407                // a file was double-clicked: confirm like the primary button
408                result = EV_ACT;
409                open.set(0);
410            }
411        } else if ev & EV_ACT != 0 {
412            // a file was selected: its name goes into the name box
413            let multi = flags & (FD_MULTI | FD_SAVE | FD_FOLDER) == FD_MULTI;
414            let mods = self.mods;
415            let f = &mut self.m.filedlg;
416            if multi {
417                f.mark(sel, mods);
418            } else if let Some((name, false)) = f.entry(sel) {
419                f.name = copy_cap(name, FD_NAME_CAP);
420                f.msg = None;
421            }
422        }
423        // file name (not when choosing a folder)
424        if !folder {
425            self.box_begin(0, BOX_H | BOX_XCENTER, 8);
426            self.text(TS_BODY, tr("Dateiname:"));
427            self.next_size(-1, 0);
428            let mut name = core::mem::take(&mut self.m.filedlg.name);
429            let ncap = if flags & FD_MULTI != 0 { 4096 } else { FD_NAME_CAP as u32 }; // (E8) several quoted names
430            let ev = self.textbox(id.wrapping_add(12), None, &mut name, ncap);
431            self.m.filedlg.name = name;
432            if ev & EV_ACT != 0 {
433                self.m.filedlg.msg = None;
434            }
435            self.box_end();
436        }
437        if let Some(m) = self.m.filedlg.msg {
438            self.text(TS_CAPTION | TX_SECONDARY, m);
439        }
440        let prim = tr(if folder { "Ordner auswählen" } else if flags & FD_SAVE != 0 { "Speichern" } else { "Öffnen" });
441        if self.dialog_buttons(Some(prim), None, Some(tr("Abbrechen"))) == DLG_PRIMARY {
442            if self.fd_confirm(flags, path, cap) {
443                result = EV_ACT;
444            } else {
445                open.set(1); // not done yet: keep the dialog open
446                self.repass();
447            }
448        }
449        self.dialog_end();
450        if result == EV_ACT {
451            for p in self.m.filedlg.paths.clone() {
452                super::perm::grant_user(&p); // the user chose it (fs.user)
453            }
454        }
455        result
456    }
457
458    /// (E8) the files the last confirmed file dialog chose: with FD_MULTI every one (the first
459    /// is the dialog's path), else the one (FD_FOLDER: the folder); empty before any
460    pub fn filedlg_paths(&self) -> Vec<String> {
461        self.m.filedlg.paths.clone()
462    }
463
464    // the system's picker is open (Android): its answer, once it comes
465    fn fd_wait(&mut self, open: &Shared<i32>, path: &mut String, cap: u32) -> u32 {
466        let Some((r, _, chosen)) = plat::file_dialog_poll() else { return 0 };
467        self.m.filedlg.pending = false;
468        self.m.filedlg.id = 0;
469        let wanted = open.get() != 0; // the app may have closed it meanwhile
470        open.set(0);
471        self.repass();
472        if r != 1 || !wanted {
473            return 0;
474        }
475        *path = copy_cap(&chosen, cap as usize);
476        self.m.filedlg.paths = alloc::vec![path.clone()];
477        super::perm::grant_user(path); // the user chose it (fs.user)
478        EV_ACT
479    }
480
481    // fd_system(flags, path, cap, exts) -> 1 chosen, 0 cancelled, -1 use the own dialog,
482    // FD_PENDING the system's picker answers later (fd_wait)
483    fn fd_system(&mut self, flags: u32, path: &mut String, cap: u32, exts: &[&str]) -> i32 {
484        if self.headless || plat::getenv("jfgui_FILEDLG").is_some_and(|v| v.as_bytes().first() == Some(&b'b')) {
485            return -1; // "builtin"
486        }
487        if self.hr_outside("the system's file dialog") {
488            return -1; // hot reload's replay: the own dialog (hotreload.rs)
489        }
490        let r = plat::file_dialog(flags, path, exts).0;
491        if r == 1 {
492            *path = copy_cap(path, cap as usize);
493        }
494        r
495    }
496
497    // fd_init(id, flags, path, exts): the dialog opens: choose the start folder and name, list it
498    fn fd_init(&mut self, id: u32, flags: u32, path: &str, exts: &[&str]) {
499        let f = &mut self.m.filedlg;
500        f.id = id;
501        f.folder = flags & FD_FOLDER != 0;
502        f.exts = exts.iter().map(|e| String::from(*e)).collect();
503        f.name.clear();
504        f.msg = None;
505        let path = super::basic::cstr(path);
506        if !path.is_empty() {
507            let mut tmp = norm(&copy_cap(path, FD_PATH));
508            if super::perm::trusted(|| plat::file_exists(&tmp)) == 2 {
509                self.m.filedlg.set_dir(&tmp);
510                self.fd_load();
511                return;
512            }
513            // a file (or a new name): folder part + name part
514            match tmp.rfind('/') {
515                Some(k) => {
516                    self.m.filedlg.name = copy_cap(&tmp[k + 1..], FD_NAME_CAP);
517                    // cut after the separator for roots ("/", "C:/"), at it otherwise
518                    if k == 0 || (k == 2 && tmp.as_bytes()[1] == b':') {
519                        tmp.truncate(k + 1);
520                    } else {
521                        tmp.truncate(k);
522                    }
523                    if super::perm::trusted(|| plat::file_exists(&tmp)) == 2 {
524                        self.m.filedlg.set_dir(&tmp);
525                        self.fd_load();
526                        return;
527                    }
528                }
529                None => self.m.filedlg.name = copy_cap(&tmp, FD_NAME_CAP),
530            }
531        }
532        if !self.m.filedlg.last.is_empty() {
533            let last = self.m.filedlg.last.clone();
534            self.m.filedlg.set_dir(&last);
535            self.fd_load();
536            return;
537        }
538        self.fd_go_home();
539    }
540
541    // fd_go_home: switch to the home folder and list it
542    fn fd_go_home(&mut self) {
543        let h = copy_cap(&plat::home().unwrap_or_else(|| String::from("/")), FD_PATH);
544        self.m.filedlg.set_dir(&h);
545        self.fd_load();
546    }
547
548    // fd_toolbar_row(id): up, home, drives, current folder
549    fn fd_toolbar_row(&mut self, id: u32) {
550        self.box_begin(0, BOX_H | BOX_XCENTER, 4);
551        if is_root(&self.m.filedlg.dir) {
552            self.next_flags(ST_DISABLED);
553        }
554        if self.icon_button(id.wrapping_add(1), IC_UP, tr("Übergeordneter Ordner")) & EV_ACT != 0 {
555            self.m.filedlg.parent();
556            self.fd_load();
557        }
558        if self.icon_button(id.wrapping_add(2), IC_HOME, tr("Startordner")) & EV_ACT != 0 {
559            self.fd_go_home();
560        }
561        // drives (at most 8, as many as a list of FD_DRIVES bytes holds)
562        let mut used = 0;
563        for (k, d) in plat::drives().iter().enumerate().take(8) {
564            used += d.len() + 1;
565            if used >= FD_DRIVES {
566                break;
567            }
568            // label without the trailing separator of "C:/"
569            let mut label = copy_cap(d, 16);
570            if label.len() == 3 && label.as_bytes()[1] == b':' {
571                label.truncate(2);
572            }
573            if self.button(id.wrapping_add(3 + k as u32), &label) & EV_ACT != 0 {
574                self.m.filedlg.set_dir(d);
575                self.fd_load();
576            }
577        }
578        self.spacer(8);
579        // the folder, with the native separator
580        let disp = copy_cap(&self.m.filedlg.dir, FD_PATH);
581        #[cfg(windows)]
582        let disp = disp.replace('/', "\\");
583        self.next_size(-1, 0);
584        self.text(TS_BODY | TX_ELLIPSIS | TX_SECONDARY, &disp);
585        self.box_end();
586    }
587
588    // fd_row(i): one list row: folder / file icon and the name
589    fn fd_row(&mut self, i: i32) {
590        self.list_item_begin(i);
591        let (name, dir) = match self.m.filedlg.entry(i) {
592            Some((n, d)) => (String::from(n), d),
593            None => (String::new(), false),
594        };
595        let marked = self.m.filedlg.marks.len() > 1 && self.m.filedlg.marks.contains(&i);
596        self.icon(if dir { IC_FOLDER } else if marked { IC_CHECK_MARK } else { IC_PAGE }, 16);
597        self.next_size(-1, 0);
598        self.text(TS_BODY | TX_ELLIPSIS, &name);
599        self.list_item_end();
600    }
601
602    // fd_activate(i) -> entry i is a file (folders are opened here)
603    fn fd_activate(&mut self, i: i32) -> bool {
604        let f = &mut self.m.filedlg;
605        let Some((name, dir)) = f.entry(i) else { return false };
606        let name = String::from(name);
607        if dir {
608            f.enter(&name);
609            self.fd_load();
610            return false;
611        }
612        f.name = copy_cap(&name, FD_NAME_CAP);
613        true
614    }
615
616    // fd_confirm(flags, path, cap) -> the chosen path was copied to path
617    fn fd_confirm(&mut self, flags: u32, path: &mut String, cap: u32) -> bool {
618        let f = &mut self.m.filedlg;
619        if flags & FD_FOLDER != 0 {
620            // the selected folder, else the one shown
621            let mut d = copy_cap(&f.dir, FD_PATH);
622            if let Some((n, true)) = f.entry(f.sel) {
623                let n = String::from(n);
624                join(&mut d, &n);
625            }
626            *path = copy_cap(&d, cap as usize);
627            f.paths = alloc::vec![path.clone()]; // filedlg_paths: the folder, not the last dialog's files
628            f.last = copy_cap(&f.dir, FD_PATH);
629            return true;
630        }
631        if flags & (FD_MULTI | FD_SAVE) == FD_MULTI && f.name.contains('"') {
632            return self.fd_confirm_multi(path, cap);
633        }
634        if f.name.is_empty() {
635            // no name: open the selected folder
636            if let Some((n, true)) = f.entry(f.sel) {
637                let n = String::from(n);
638                f.enter(&n);
639                self.fd_load();
640            }
641            return false;
642        }
643        // absolute name ("/x", "C:/x", "C:\x") or relative to the folder
644        let mut tmp = if is_abs(&f.name) {
645            copy_cap(&f.name, FD_PATH)
646        } else {
647            let mut t = copy_cap(&f.dir, FD_PATH);
648            join(&mut t, &f.name.clone());
649            t
650        };
651        // a path that filled the buffer was cut: refuse it
652        if tmp.len() >= FD_PATH - 16 {
653            f.msg = Some(tr("Der Pfad ist zu lang."));
654            return false;
655        }
656        tmp = canon(&norm(&tmp));
657        let ex = super::perm::trusted(|| plat::file_exists(&tmp));
658        if ex == 2 {
659            // a folder name: go there
660            self.m.filedlg.set_dir(&tmp);
661            self.fd_load();
662            self.m.filedlg.name.clear();
663            return false;
664        }
665        let f = &mut self.m.filedlg;
666        if flags & FD_SAVE == 0 {
667            if ex != 1 {
668                f.msg = Some(tr("Die Datei wurde nicht gefunden."));
669                return false;
670            }
671        } else if !f.exts.is_empty() && !f.exts[0].is_empty() {
672            // append the first extension when the name has none
673            let base = match tmp.rfind('/') {
674                Some(k) => &tmp[k..],
675                None => &tmp[..],
676            };
677            if !base.contains('.') {
678                cat_cap(&mut tmp, ".", FD_PATH);
679                // the selected extension (flags bits 8..15), without its "=description"
680                let k = ((flags >> FD_EXT_SHIFT) & 0xFF) as usize;
681                let e = f.exts.get(k).filter(|e| !e.is_empty()).unwrap_or(&f.exts[0]);
682                let e = e.split(['=', ';']).next().unwrap_or("");
683                if tmp.len() + e.len() >= FD_PATH - 1 {
684                    f.msg = Some(tr("Der Pfad ist zu lang.")); // the extension does not fit
685                    return false;
686                }
687                tmp.push_str(e);
688            }
689        }
690        *path = copy_cap(&tmp, cap as usize);
691        f.paths = alloc::vec![path.clone()];
692        f.last = copy_cap(&f.dir, FD_PATH);
693        true
694    }
695
696    // (E8) FD_MULTI: the quoted names of the name box ("a" "b") -> every one an existing file
697    fn fd_confirm_multi(&mut self, path: &mut String, cap: u32) -> bool {
698        let f = &mut self.m.filedlg;
699        let names: Vec<String> = f.name.split('"').map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from).collect();
700        let mut all = Vec::new();
701        for n in &names {
702            let mut t = if is_abs(n) {
703                copy_cap(n, FD_PATH)
704            } else {
705                let mut t = copy_cap(&f.dir, FD_PATH);
706                join(&mut t, n);
707                t
708            };
709            if t.len() >= FD_PATH - 16 {
710                f.msg = Some(tr("Der Pfad ist zu lang."));
711                return false;
712            }
713            t = canon(&norm(&t));
714            if super::perm::trusted(|| plat::file_exists(&t)) != 1 {
715                f.msg = Some(tr("Die Datei wurde nicht gefunden."));
716                return false;
717            }
718            all.push(copy_cap(&t, cap as usize));
719        }
720        if all.is_empty() {
721            return false;
722        }
723        *path = all[0].clone();
724        f.paths = all;
725        f.last = copy_cap(&f.dir, FD_PATH);
726        true
727    }
728
729    // fd_load: list the folder (folders first, then files matching the extensions)
730    fn fd_load(&mut self) {
731        let f = &mut self.m.filedlg;
732        f.idx.clear();
733        f.store.clear();
734        f.marks.clear();
735        f.anchor = -1;
736        f.sel = -1;
737        f.msg = None;
738        let dir = f.dir.clone();
739        let mut ents: Vec<(String, bool)> = Vec::new();
740        super::perm::trusted(|| plat::dir_list(&dir, &mut |name: &str, is_dir: bool, _size: u32| ents.push((String::from(name), is_dir))));
741        let f = &mut self.m.filedlg;
742        for (name, is_dir) in ents {
743            // hidden entries, "." and ".."; choosing a folder: folders only
744            if name.starts_with('.') || (!is_dir && (f.folder || !f.ext_ok(&name))) {
745                continue;
746            }
747            if f.idx.len() >= FD_MAX || f.store.len() + name.len() + 1 > FD_STORE {
748                continue;
749            }
750            let off = f.store.len() as u32;
751            f.store.extend_from_slice(name.as_bytes());
752            f.store.push(0);
753            f.idx.push(if is_dir { off | FD_DIRBIT } else { off });
754        }
755        f.sort();
756        self.repass();
757    }
758}