1use alloc::string::String;
24use alloc::vec::Vec;
25
26use super::consts::*;
27use super::i18n::tr;
28use super::keys::*;
29use super::win::Shared;
30use super::Ui;
31
32const FD_MAX: usize = 8192; const FD_STORE: usize = 512 * 1024; const FD_PATH: usize = 1040;
35const FD_DRIVES: usize = 256; const FD_NAME_CAP: usize = 256;
37const FD_DIRBIT: u32 = 0x8000_0000;
38
39#[derive(Default)]
41pub(crate) struct S {
42 store: Vec<u8>,
44 idx: Vec<u32>,
46 exts: Vec<String>,
48 msg: Option<&'static str>,
49 id: u32,
50 sel: i32,
51 pending: bool,
53 folder: bool,
55 dir: String,
56 last: String,
57 name: String,
58}
59
60fn copy_cap(src: &str, cap: usize) -> String {
62 let src = super::basic::cstr(src);
63 if cap == 0 {
64 return String::new();
65 }
66 let mut n = src.len().min(cap - 1);
67 while !src.is_char_boundary(n) {
68 n -= 1;
69 }
70 String::from(&src[..n])
71}
72
73fn cat_cap(dst: &mut String, src: &str, cap: usize) {
75 if dst.len() + 1 >= cap {
76 return;
77 }
78 let room = cap - 1 - dst.len();
79 let src = super::basic::cstr(src);
80 let mut n = src.len().min(room);
81 while !src.is_char_boundary(n) {
82 n -= 1;
83 }
84 dst.push_str(&src[..n]);
85}
86
87fn norm(s: &str) -> String {
89 s.replace('\\', "/")
90}
91
92fn is_root(s: &str) -> bool {
94 let b = s.as_bytes();
95 match b.len() {
96 1 => b[0] == b'/',
97 3 => b[1] == b':' && b[2] == b'/',
98 _ => false,
99 }
100}
101
102fn is_abs(s: &str) -> bool {
104 let b = s.as_bytes();
105 !b.is_empty() && (b[0] == b'/' || b[0] == b'\\' || (b.len() > 1 && b[1] == b':'))
106}
107
108fn canon(p: &str) -> String {
111 let b = p.as_bytes();
112 let mut root = 0;
113 if !b.is_empty() && b[0] == b'/' {
114 root = 1;
115 } else if b.len() > 1 && b[1] == b':' {
116 root = 2;
117 if b.len() > 2 && b[2] == b'/' {
118 root = 3;
119 }
120 }
121 let mut out: Vec<u8> = b[..root].to_vec();
122 let mut r = root;
123 while r < b.len() {
124 if b[r] == b'/' {
125 r += 1;
126 continue;
127 }
128 let mut e = r;
129 while e < b.len() && b[e] != b'/' {
130 e += 1;
131 }
132 let seg = &b[r..e];
133 if seg == b"." {
134 } else if seg == b".." {
136 while out.len() > root {
138 let c = out.pop();
139 if c == Some(b'/') {
140 break;
141 }
142 }
143 } else {
144 if out.len() > root {
145 out.push(b'/');
146 }
147 out.extend_from_slice(seg);
148 }
149 r = e;
150 }
151 String::from_utf8(out).unwrap_or_default()
152}
153
154fn join(dst: &mut String, name: &str) {
156 if !dst.is_empty() && !dst.ends_with('/') {
157 cat_cap(dst, "/", FD_PATH);
158 }
159 cat_cap(dst, name, FD_PATH);
160}
161
162fn ieq(a: &[u8], b: &[u8]) -> bool {
164 let mut i = 0;
165 loop {
166 let x = a.get(i).copied().unwrap_or(0).to_ascii_lowercase();
167 let mut y = b.get(i).copied().unwrap_or(0);
168 if y == b'=' || y == b';' {
169 y = 0;
170 }
171 let y = y.to_ascii_lowercase();
172 if x != y {
173 return false;
174 }
175 if x == 0 {
176 return true;
177 }
178 i += 1;
179 }
180}
181
182impl S {
183 fn entry(&self, i: i32) -> Option<(&str, bool)> {
185 if i < 0 || i as usize >= self.idx.len() {
186 return None;
187 }
188 let v = self.idx[i as usize];
189 let off = (v & !FD_DIRBIT) as usize;
190 let end = self.store[off..].iter().position(|&c| c == 0).map_or(self.store.len(), |n| off + n);
191 Some((core::str::from_utf8(&self.store[off..end]).unwrap_or(""), v & FD_DIRBIT != 0))
192 }
193
194 fn ext_ok(&self, name: &str) -> bool {
196 if self.exts.is_empty() || self.exts[0].is_empty() {
197 return true;
198 }
199 let Some(dot) = name.rfind('.') else { return false };
200 let ext = &name.as_bytes()[dot + 1..];
201 for e in &self.exts {
202 if e.is_empty() {
203 break;
204 }
205 let b = e.as_bytes();
207 let mut k = 0;
208 loop {
209 if ieq(ext, &b[k..]) {
210 return true;
211 }
212 while k < b.len() && b[k] != b';' && b[k] != b'=' {
214 k += 1;
215 }
216 if k >= b.len() || b[k] == b'=' {
217 break;
218 }
219 k += 1;
220 }
221 }
222 false
223 }
224
225 fn sort(&mut self) {
228 let coll = super::i18n::collator().numeric(true);
229 let store = &self.store;
230 self.idx.sort_by_cached_key(|&v| {
231 let off = (v & !FD_DIRBIT) as usize;
232 let end = store[off..].iter().position(|&c| c == 0).map_or(store.len(), |n| off + n);
233 (v & FD_DIRBIT == 0, coll.key(core::str::from_utf8(&store[off..end]).unwrap_or("")))
234 });
235 }
236
237 fn set_dir(&mut self, s: &str) {
239 let mut d;
240 if is_abs(s) {
241 d = copy_cap(s, FD_PATH - 2);
242 } else {
243 d = norm(©_cap(&plat::cwd(), FD_PATH - 2));
245 join(&mut d, s);
246 }
247 let mut d = canon(&norm(&d));
248 loop {
249 if is_root(&d) {
250 break;
251 }
252 let b = d.as_bytes();
253 if b.len() == 2 && b[1] == b':' {
254 d.push('/'); break;
256 }
257 if b.is_empty() {
258 d.push('/'); break;
260 }
261 if b[b.len() - 1] != b'/' {
262 break;
263 }
264 d.pop();
265 }
266 self.dir = d;
267 }
268
269 fn parent(&mut self) {
271 if is_root(&self.dir) {
272 return;
273 }
274 let Some(k) = self.dir.rfind('/') else { return };
275 if k == 0 || (k == 2 && self.dir.as_bytes()[1] == b':') {
276 self.dir.truncate(k + 1); } else {
278 self.dir.truncate(k);
279 }
280 }
281
282 fn enter(&mut self, name: &str) {
284 let mut d = core::mem::take(&mut self.dir);
285 join(&mut d, name);
286 self.dir = d;
287 self.name.clear();
288 }
289}
290
291impl Ui {
292 #[allow(clippy::too_many_arguments)]
294 #[cfg_attr(jfgui_dev, track_caller)]
295 pub fn filedlg(&mut self, id: u32, flags: u32, open: &Shared<i32>, path: &mut String, cap: u32, exts: &[&str]) -> u32 {
296 let _src = self.src_scope();
297 let mut result = 0;
298 if open.get() != 0 && self.m.filedlg.id != id {
300 let r = self.fd_system(flags, path, cap, exts);
302 if r == plat::FD_PENDING {
303 self.m.filedlg.id = id;
305 self.m.filedlg.pending = true;
306 } else if r != -1 {
307 open.set(0);
308 if r != 0 {
309 result = EV_ACT;
310 }
311 self.repass();
312 } else {
313 self.fd_init(id, flags, path, exts);
314 }
315 }
316 if self.m.filedlg.pending && self.m.filedlg.id == id {
317 return self.fd_wait(open, path, cap);
318 }
319 let folder = flags & FD_FOLDER != 0;
320 let title = tr(if folder { "Ordner auswählen" } else if flags & FD_SAVE != 0 { "Speichern unter" } else { "Öffnen" });
321 if !self.dialog_begin(id, title, open) {
322 if self.m.filedlg.id == id {
323 self.m.filedlg.id = 0;
324 }
325 self.dialog_end();
326 if result == EV_ACT {
327 super::perm::grant_user(path); }
329 return result;
330 }
331 self.fd_toolbar_row(id);
332 self.next_size(480, 300);
334 let mut sel = self.m.filedlg.sel;
335 let n = self.m.filedlg.idx.len() as i32;
336 let (first, end) = self.list_begin(id.wrapping_add(16), &mut sel, n, 32);
337 for i in first..end {
338 self.fd_row(i);
339 }
340 let (ev, sel) = self.list_end(&mut sel);
341 self.m.filedlg.sel = sel;
342 if ev & EV_DOUBLE != 0 {
343 if self.fd_activate(sel) && self.fd_confirm(flags, path, cap) {
344 result = EV_ACT;
346 open.set(0);
347 }
348 } else if ev & EV_ACT != 0 {
349 let f = &mut self.m.filedlg;
351 if let Some((name, false)) = f.entry(sel) {
352 f.name = copy_cap(name, FD_NAME_CAP);
353 f.msg = None;
354 }
355 }
356 if !folder {
358 self.box_begin(0, BOX_H | BOX_XCENTER, 8);
359 self.text(TS_BODY, tr("Dateiname:"));
360 self.next_size(-1, 0);
361 let mut name = core::mem::take(&mut self.m.filedlg.name);
362 let ev = self.textbox(id.wrapping_add(12), None, &mut name, FD_NAME_CAP as u32);
363 self.m.filedlg.name = name;
364 if ev & EV_ACT != 0 {
365 self.m.filedlg.msg = None;
366 }
367 self.box_end();
368 }
369 if let Some(m) = self.m.filedlg.msg {
370 self.text(TS_CAPTION | TX_SECONDARY, m);
371 }
372 let prim = tr(if folder { "Ordner auswählen" } else if flags & FD_SAVE != 0 { "Speichern" } else { "Öffnen" });
373 if self.dialog_buttons(Some(prim), None, Some(tr("Abbrechen"))) == DLG_PRIMARY {
374 if self.fd_confirm(flags, path, cap) {
375 result = EV_ACT;
376 } else {
377 open.set(1); self.repass();
379 }
380 }
381 self.dialog_end();
382 if result == EV_ACT {
383 super::perm::grant_user(path); }
385 result
386 }
387
388 fn fd_wait(&mut self, open: &Shared<i32>, path: &mut String, cap: u32) -> u32 {
390 let Some((r, _, chosen)) = plat::file_dialog_poll() else { return 0 };
391 self.m.filedlg.pending = false;
392 self.m.filedlg.id = 0;
393 let wanted = open.get() != 0; open.set(0);
395 self.repass();
396 if r != 1 || !wanted {
397 return 0;
398 }
399 *path = copy_cap(&chosen, cap as usize);
400 super::perm::grant_user(path); EV_ACT
402 }
403
404 fn fd_system(&mut self, flags: u32, path: &mut String, cap: u32, exts: &[&str]) -> i32 {
407 if self.headless || plat::getenv("jfgui_FILEDLG").is_some_and(|v| v.as_bytes().first() == Some(&b'b')) {
408 return -1; }
410 if self.hr_outside("the system's file dialog") {
411 return -1; }
413 let r = plat::file_dialog(flags, path, exts).0;
414 if r == 1 {
415 *path = copy_cap(path, cap as usize);
416 }
417 r
418 }
419
420 fn fd_init(&mut self, id: u32, flags: u32, path: &str, exts: &[&str]) {
422 let f = &mut self.m.filedlg;
423 f.id = id;
424 f.folder = flags & FD_FOLDER != 0;
425 f.exts = exts.iter().map(|e| String::from(*e)).collect();
426 f.name.clear();
427 f.msg = None;
428 let path = super::basic::cstr(path);
429 if !path.is_empty() {
430 let mut tmp = norm(©_cap(path, FD_PATH));
431 if super::perm::trusted(|| plat::file_exists(&tmp)) == 2 {
432 self.m.filedlg.set_dir(&tmp);
433 self.fd_load();
434 return;
435 }
436 match tmp.rfind('/') {
438 Some(k) => {
439 self.m.filedlg.name = copy_cap(&tmp[k + 1..], FD_NAME_CAP);
440 if k == 0 || (k == 2 && tmp.as_bytes()[1] == b':') {
442 tmp.truncate(k + 1);
443 } else {
444 tmp.truncate(k);
445 }
446 if super::perm::trusted(|| plat::file_exists(&tmp)) == 2 {
447 self.m.filedlg.set_dir(&tmp);
448 self.fd_load();
449 return;
450 }
451 }
452 None => self.m.filedlg.name = copy_cap(&tmp, FD_NAME_CAP),
453 }
454 }
455 if !self.m.filedlg.last.is_empty() {
456 let last = self.m.filedlg.last.clone();
457 self.m.filedlg.set_dir(&last);
458 self.fd_load();
459 return;
460 }
461 self.fd_go_home();
462 }
463
464 fn fd_go_home(&mut self) {
466 let h = copy_cap(&plat::home().unwrap_or_else(|| String::from("/")), FD_PATH);
467 self.m.filedlg.set_dir(&h);
468 self.fd_load();
469 }
470
471 fn fd_toolbar_row(&mut self, id: u32) {
473 self.box_begin(0, BOX_H | BOX_XCENTER, 4);
474 if is_root(&self.m.filedlg.dir) {
475 self.next_flags(ST_DISABLED);
476 }
477 if self.icon_button(id.wrapping_add(1), IC_UP, tr("Übergeordneter Ordner")) & EV_ACT != 0 {
478 self.m.filedlg.parent();
479 self.fd_load();
480 }
481 if self.icon_button(id.wrapping_add(2), IC_HOME, tr("Startordner")) & EV_ACT != 0 {
482 self.fd_go_home();
483 }
484 let mut used = 0;
486 for (k, d) in plat::drives().iter().enumerate().take(8) {
487 used += d.len() + 1;
488 if used >= FD_DRIVES {
489 break;
490 }
491 let mut label = copy_cap(d, 16);
493 if label.len() == 3 && label.as_bytes()[1] == b':' {
494 label.truncate(2);
495 }
496 if self.button(id.wrapping_add(3 + k as u32), &label) & EV_ACT != 0 {
497 self.m.filedlg.set_dir(d);
498 self.fd_load();
499 }
500 }
501 self.spacer(8);
502 let disp = copy_cap(&self.m.filedlg.dir, FD_PATH);
504 #[cfg(windows)]
505 let disp = disp.replace('/', "\\");
506 self.next_size(-1, 0);
507 self.text(TS_BODY | TX_ELLIPSIS | TX_SECONDARY, &disp);
508 self.box_end();
509 }
510
511 fn fd_row(&mut self, i: i32) {
513 self.list_item_begin(i);
514 let (name, dir) = match self.m.filedlg.entry(i) {
515 Some((n, d)) => (String::from(n), d),
516 None => (String::new(), false),
517 };
518 self.icon(if dir { IC_FOLDER } else { IC_PAGE }, 16);
519 self.next_size(-1, 0);
520 self.text(TS_BODY | TX_ELLIPSIS, &name);
521 self.list_item_end();
522 }
523
524 fn fd_activate(&mut self, i: i32) -> bool {
526 let f = &mut self.m.filedlg;
527 let Some((name, dir)) = f.entry(i) else { return false };
528 let name = String::from(name);
529 if dir {
530 f.enter(&name);
531 self.fd_load();
532 return false;
533 }
534 f.name = copy_cap(&name, FD_NAME_CAP);
535 true
536 }
537
538 fn fd_confirm(&mut self, flags: u32, path: &mut String, cap: u32) -> bool {
540 let f = &mut self.m.filedlg;
541 if flags & FD_FOLDER != 0 {
542 let mut d = copy_cap(&f.dir, FD_PATH);
544 if let Some((n, true)) = f.entry(f.sel) {
545 let n = String::from(n);
546 join(&mut d, &n);
547 }
548 *path = copy_cap(&d, cap as usize);
549 f.last = copy_cap(&f.dir, FD_PATH);
550 return true;
551 }
552 if f.name.is_empty() {
553 if let Some((n, true)) = f.entry(f.sel) {
555 let n = String::from(n);
556 f.enter(&n);
557 self.fd_load();
558 }
559 return false;
560 }
561 let mut tmp = if is_abs(&f.name) {
563 copy_cap(&f.name, FD_PATH)
564 } else {
565 let mut t = copy_cap(&f.dir, FD_PATH);
566 join(&mut t, &f.name.clone());
567 t
568 };
569 if tmp.len() >= FD_PATH - 16 {
571 f.msg = Some(tr("Der Pfad ist zu lang."));
572 return false;
573 }
574 tmp = canon(&norm(&tmp));
575 let ex = super::perm::trusted(|| plat::file_exists(&tmp));
576 if ex == 2 {
577 self.m.filedlg.set_dir(&tmp);
579 self.fd_load();
580 self.m.filedlg.name.clear();
581 return false;
582 }
583 let f = &mut self.m.filedlg;
584 if flags & FD_SAVE == 0 {
585 if ex != 1 {
586 f.msg = Some(tr("Die Datei wurde nicht gefunden."));
587 return false;
588 }
589 } else if !f.exts.is_empty() && !f.exts[0].is_empty() {
590 let base = match tmp.rfind('/') {
592 Some(k) => &tmp[k..],
593 None => &tmp[..],
594 };
595 if !base.contains('.') {
596 cat_cap(&mut tmp, ".", FD_PATH);
597 let k = ((flags >> FD_EXT_SHIFT) & 0xFF) as usize;
599 let e = f.exts.get(k).filter(|e| !e.is_empty()).unwrap_or(&f.exts[0]);
600 let e = e.split(['=', ';']).next().unwrap_or("");
601 if tmp.len() + e.len() >= FD_PATH - 1 {
602 f.msg = Some(tr("Der Pfad ist zu lang.")); return false;
604 }
605 tmp.push_str(e);
606 }
607 }
608 *path = copy_cap(&tmp, cap as usize);
609 f.last = copy_cap(&f.dir, FD_PATH);
610 true
611 }
612
613 fn fd_load(&mut self) {
615 let f = &mut self.m.filedlg;
616 f.idx.clear();
617 f.store.clear();
618 f.sel = -1;
619 f.msg = None;
620 let dir = f.dir.clone();
621 let mut ents: Vec<(String, bool)> = Vec::new();
622 super::perm::trusted(|| plat::dir_list(&dir, &mut |name: &str, is_dir: bool, _size: u32| ents.push((String::from(name), is_dir))));
623 let f = &mut self.m.filedlg;
624 for (name, is_dir) in ents {
625 if name.starts_with('.') || (!is_dir && (f.folder || !f.ext_ok(&name))) {
627 continue;
628 }
629 if f.idx.len() >= FD_MAX || f.store.len() + name.len() + 1 > FD_STORE {
630 continue;
631 }
632 let off = f.store.len() as u32;
633 f.store.extend_from_slice(name.as_bytes());
634 f.store.push(0);
635 f.idx.push(if is_dir { off | FD_DIRBIT } else { off });
636 }
637 f.sort();
638 self.repass();
639 }
640}