1use 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; const FD_STORE: usize = 512 * 1024; const FD_PATH: usize = 1040;
41const FD_DRIVES: usize = 256; const FD_NAME_CAP: usize = 256;
43const FD_DIRBIT: u32 = 0x8000_0000;
44
45#[derive(Default)]
47pub(crate) struct S {
48 store: Vec<u8>,
50 idx: Vec<u32>,
52 exts: Vec<String>,
54 msg: Option<&'static str>,
55 id: u32,
56 sel: i32,
57 pending: bool,
59 folder: bool,
61 dir: String,
62 last: String,
63 name: String,
64 marks: Vec<i32>,
66 anchor: i32,
67 paths: Vec<String>,
69}
70
71fn 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
84fn 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
98fn norm(s: &str) -> String {
100 s.replace('\\', "/")
101}
102
103fn 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
113fn 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
119fn 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 } else if seg == b".." {
147 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
165fn 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
173fn 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 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 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 let b = e.as_bytes();
218 let mut k = 0;
219 loop {
220 if ieq(ext, &b[k..]) {
221 return true;
222 }
223 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 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 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 d = norm(©_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('/'); break;
308 }
309 if b.is_empty() {
310 d.push('/'); break;
312 }
313 if b[b.len() - 1] != b'/' {
314 break;
315 }
316 d.pop();
317 }
318 self.dir = d;
319 }
320
321 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); } else {
330 self.dir.truncate(k);
331 }
332 }
333
334 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 #[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 if open.get() != 0 && self.m.filedlg.id != id {
352 let r = self.fd_system(flags, path, cap, exts);
354 if r == 1 {
355 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 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); }
391 }
392 return result;
393 }
394 self.fd_toolbar_row(id);
395 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 result = EV_ACT;
409 open.set(0);
410 }
411 } else if ev & EV_ACT != 0 {
412 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 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 }; 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); 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); }
454 }
455 result
456 }
457
458 pub fn filedlg_paths(&self) -> Vec<String> {
461 self.m.filedlg.paths.clone()
462 }
463
464 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; 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); EV_ACT
479 }
480
481 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; }
487 if self.hr_outside("the system's file dialog") {
488 return -1; }
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 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(©_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 match tmp.rfind('/') {
515 Some(k) => {
516 self.m.filedlg.name = copy_cap(&tmp[k + 1..], FD_NAME_CAP);
517 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 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 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 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 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 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 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 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 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 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()]; 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 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 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 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 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 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 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.")); 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 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 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 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}