rat_widget/
file_dialog.rs

1//!
2//! File dialog.
3//!
4
5use crate::_private::NonExhaustive;
6use crate::button::{Button, ButtonState, ButtonStyle};
7use crate::event::{ButtonOutcome, FileOutcome, TextOutcome};
8use crate::layout::{DialogItem, LayoutOuter, layout_as_grid};
9use crate::list::edit::{EditList, EditListState};
10use crate::list::selection::{RowSelection, RowSetSelection};
11use crate::list::{List, ListState, ListStyle};
12use crate::util::{block_padding2, reset_buf_area};
13use crossterm::event::{Event, MouseEvent};
14#[cfg(feature = "user_directories")]
15use dirs::{document_dir, home_dir};
16use log::debug;
17use rat_event::{Dialog, HandleEvent, MouseOnly, Outcome, Regular, ct_event, event_flow};
18use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus, Navigation, on_lost};
19use rat_ftable::event::EditOutcome;
20use rat_reloc::RelocatableState;
21use rat_scrolled::Scroll;
22use rat_text::text_input::{TextInput, TextInputState};
23use rat_text::{HasScreenCursor, TextStyle};
24use ratatui::buffer::Buffer;
25use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect, Size};
26use ratatui::style::Style;
27use ratatui::text::Text;
28use ratatui::widgets::{Block, ListItem};
29use ratatui::widgets::{StatefulWidget, Widget};
30use std::cmp::max;
31use std::collections::HashSet;
32use std::ffi::OsString;
33use std::fmt::{Debug, Formatter};
34use std::path::{Path, PathBuf};
35use std::{fs, io};
36#[cfg(feature = "user_directories")]
37use sysinfo::Disks;
38
39/// Shows a file dialog.
40///
41/// * Display modes
42///     * Open
43///     * Save
44///     * Directory
45///
46/// * Define your roots or let them provide by
47///   [dirs](https://docs.rs/dirs/6.0.0/dirs/) and
48///   [sysinfo](https://docs.rs/sysinfo/0.33.1/sysinfo/)
49///   You need the feature "user_directories" for the latter.
50///
51///   * Standard roots are
52///     * Last - The directory choosen the last time the dialog was opened.
53///     * Start - The start directory provided by the application.
54///
55/// * Create new directories.
56///
57/// * Quick jump between lists with F1..F5.
58///
59#[derive(Debug, Clone)]
60pub struct FileDialog<'a> {
61    style: Style,
62    block: Option<Block<'a>>,
63    list_style: Option<ListStyle>,
64    roots_style: Option<ListStyle>,
65    text_style: Option<TextStyle>,
66    button_style: Option<ButtonStyle>,
67
68    layout: LayoutOuter,
69
70    ok_text: &'a str,
71    cancel_text: &'a str,
72}
73
74/// Combined styles for the FileDialog.
75#[derive(Debug, Clone)]
76pub struct FileDialogStyle {
77    pub style: Style,
78    /// Outer border.
79    pub block: Option<Block<'static>>,
80    pub border_style: Option<Style>,
81    pub title_style: Option<Style>,
82    /// Placement
83    pub layout: Option<LayoutOuter>,
84    /// Lists
85    pub list: Option<ListStyle>,
86    /// FS roots
87    pub roots: Option<ListStyle>,
88    /// Text fields
89    pub text: Option<TextStyle>,
90    /// Buttons.
91    pub button: Option<ButtonStyle>,
92
93    pub non_exhaustive: NonExhaustive,
94}
95
96/// Open/Save or Directory dialog.
97#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
98#[allow(dead_code)]
99enum Mode {
100    #[default]
101    Open,
102    OpenMany,
103    Save,
104    Dir,
105}
106
107#[derive(Debug, Clone)]
108enum FileStateMode {
109    Open(ListState<RowSelection>),
110    OpenMany(ListState<RowSetSelection>),
111    Save(ListState<RowSelection>),
112    Dir(FocusFlag),
113}
114
115impl Default for FileStateMode {
116    fn default() -> Self {
117        Self::Open(Default::default())
118    }
119}
120
121impl HasFocus for FileStateMode {
122    fn build(&self, builder: &mut FocusBuilder) {
123        match self {
124            FileStateMode::Open(st) => {
125                builder.widget(st);
126            }
127            FileStateMode::OpenMany(st) => {
128                builder.widget(st);
129            }
130            FileStateMode::Save(st) => {
131                builder.widget(st);
132            }
133            FileStateMode::Dir(f) => {
134                builder.widget_navigate(f, Navigation::None);
135            }
136        }
137    }
138
139    fn focus(&self) -> FocusFlag {
140        match self {
141            FileStateMode::Open(st) => st.focus(),
142            FileStateMode::OpenMany(st) => st.focus(),
143            FileStateMode::Save(st) => st.focus(),
144            FileStateMode::Dir(f) => f.clone(),
145        }
146    }
147
148    fn area(&self) -> Rect {
149        match self {
150            FileStateMode::Open(st) => st.area(),
151            FileStateMode::OpenMany(st) => st.area(),
152            FileStateMode::Save(st) => st.area(),
153            FileStateMode::Dir(_) => Rect::default(),
154        }
155    }
156}
157
158impl FileStateMode {
159    pub(crate) fn is_double_click(&self, m: &MouseEvent) -> bool {
160        match self {
161            FileStateMode::Open(st) => st.mouse.doubleclick(st.inner, m),
162            FileStateMode::OpenMany(st) => st.mouse.doubleclick(st.inner, m),
163            FileStateMode::Save(st) => st.mouse.doubleclick(st.inner, m),
164            FileStateMode::Dir(_) => false,
165        }
166    }
167
168    pub(crate) fn set_offset(&mut self, n: usize) {
169        match self {
170            FileStateMode::Open(st) => {
171                st.set_offset(n);
172            }
173            FileStateMode::OpenMany(st) => {
174                st.set_offset(n);
175            }
176            FileStateMode::Save(st) => {
177                st.set_offset(n);
178            }
179            FileStateMode::Dir(_) => {}
180        }
181    }
182
183    pub(crate) fn first_selected(&self) -> Option<usize> {
184        match self {
185            FileStateMode::Open(st) => st.selected(),
186            FileStateMode::OpenMany(st) => st.lead(),
187            FileStateMode::Save(st) => st.selected(),
188            FileStateMode::Dir(_) => None,
189        }
190    }
191
192    pub(crate) fn selected(&self) -> HashSet<usize> {
193        match self {
194            FileStateMode::Open(st) => {
195                let mut sel = HashSet::new();
196                if let Some(v) = st.selected() {
197                    sel.insert(v);
198                }
199                sel
200            }
201            FileStateMode::OpenMany(st) => st.selected(),
202            FileStateMode::Save(st) => {
203                let mut sel = HashSet::new();
204                if let Some(v) = st.selected() {
205                    sel.insert(v);
206                }
207                sel
208            }
209            FileStateMode::Dir(_) => Default::default(),
210        }
211    }
212
213    pub(crate) fn select(&mut self, select: Option<usize>) {
214        match self {
215            FileStateMode::Open(st) => {
216                st.select(select);
217            }
218            FileStateMode::OpenMany(st) => {
219                st.set_lead(select, false);
220            }
221            FileStateMode::Save(st) => {
222                st.select(select);
223            }
224            FileStateMode::Dir(_) => {}
225        }
226    }
227
228    pub(crate) fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
229        match self {
230            FileStateMode::Open(st) => st.relocate(shift, clip),
231            FileStateMode::OpenMany(st) => st.relocate(shift, clip),
232            FileStateMode::Save(st) => st.relocate(shift, clip),
233            FileStateMode::Dir(_) => {}
234        }
235    }
236}
237
238/// State & event-handling.
239#[expect(clippy::type_complexity)]
240pub struct FileDialogState {
241    /// Area
242    /// __read only__ renewed with each render.
243    pub area: Rect,
244    /// Dialog is active.
245    pub active: bool,
246
247    mode: Mode,
248
249    path: PathBuf,
250    save_name: Option<OsString>,
251    save_ext: Option<OsString>,
252    dirs: Vec<OsString>,
253    filter: Option<Box<dyn Fn(&Path) -> bool + 'static>>,
254    files: Vec<OsString>,
255    no_default_roots: bool,
256    roots: Vec<(OsString, PathBuf)>,
257
258    path_state: TextInputState,
259    root_state: ListState<RowSelection>,
260    dir_state: EditListState<EditDirNameState>,
261    file_state: FileStateMode,
262    save_name_state: TextInputState,
263    new_state: ButtonState,
264    cancel_state: ButtonState,
265    ok_state: ButtonState,
266}
267
268pub(crate) mod event {
269    use rat_event::{ConsumedEvent, Outcome};
270    use std::path::PathBuf;
271
272    /// Result for the FileDialog.
273    #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
274    pub enum FileOutcome {
275        /// The given event has not been used at all.
276        Continue,
277        /// The event has been recognized, but the result was nil.
278        /// Further processing for this event may stop.
279        Unchanged,
280        /// The event has been recognized and there is some change
281        /// due to it.
282        /// Further processing for this event may stop.
283        /// Rendering the ui is advised.
284        Changed,
285        /// Cancel
286        Cancel,
287        /// Ok
288        Ok(PathBuf),
289        ///
290        OkList(Vec<PathBuf>),
291    }
292
293    impl ConsumedEvent for FileOutcome {
294        fn is_consumed(&self) -> bool {
295            !matches!(self, FileOutcome::Continue)
296        }
297    }
298
299    impl From<FileOutcome> for Outcome {
300        fn from(value: FileOutcome) -> Self {
301            match value {
302                FileOutcome::Continue => Outcome::Continue,
303                FileOutcome::Unchanged => Outcome::Unchanged,
304                FileOutcome::Changed => Outcome::Changed,
305                FileOutcome::Ok(_) => Outcome::Changed,
306                FileOutcome::Cancel => Outcome::Changed,
307                FileOutcome::OkList(_) => Outcome::Changed,
308            }
309        }
310    }
311
312    impl From<Outcome> for FileOutcome {
313        fn from(value: Outcome) -> Self {
314            match value {
315                Outcome::Continue => FileOutcome::Continue,
316                Outcome::Unchanged => FileOutcome::Unchanged,
317                Outcome::Changed => FileOutcome::Changed,
318            }
319        }
320    }
321
322    // Useful for converting most navigation/edit results.
323    impl From<bool> for FileOutcome {
324        fn from(value: bool) -> Self {
325            if value {
326                FileOutcome::Changed
327            } else {
328                FileOutcome::Unchanged
329            }
330        }
331    }
332}
333
334impl Clone for FileDialogState {
335    fn clone(&self) -> Self {
336        Self {
337            area: self.area,
338            active: self.active,
339            mode: self.mode,
340            path: self.path.clone(),
341            save_name: self.save_name.clone(),
342            save_ext: self.save_ext.clone(),
343            dirs: self.dirs.clone(),
344            filter: None,
345            files: self.files.clone(),
346            no_default_roots: self.no_default_roots,
347            roots: self.roots.clone(),
348            path_state: self.path_state.clone(),
349            root_state: self.root_state.clone(),
350            dir_state: self.dir_state.clone(),
351            file_state: self.file_state.clone(),
352            save_name_state: self.save_name_state.clone(),
353            new_state: self.new_state.clone(),
354            cancel_state: self.cancel_state.clone(),
355            ok_state: self.ok_state.clone(),
356        }
357    }
358}
359
360impl Debug for FileDialogState {
361    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
362        f.debug_struct("FileOpenState")
363            .field("active", &self.active)
364            .field("mode", &self.mode)
365            .field("path", &self.path)
366            .field("save_name", &self.save_name)
367            .field("dirs", &self.dirs)
368            .field("files", &self.files)
369            .field("no_default_roots", &self.no_default_roots)
370            .field("roots", &self.roots)
371            .field("path_state", &self.path_state)
372            .field("root_state", &self.root_state)
373            .field("dir_state", &self.dir_state)
374            .field("file_state", &self.file_state)
375            .field("name_state", &self.save_name_state)
376            .field("cancel_state", &self.cancel_state)
377            .field("ok_state", &self.ok_state)
378            .finish()
379    }
380}
381
382impl Default for FileDialogStyle {
383    fn default() -> Self {
384        FileDialogStyle {
385            style: Default::default(),
386            layout: Default::default(),
387            list: Default::default(),
388            roots: Default::default(),
389            button: Default::default(),
390            block: Default::default(),
391            border_style: Default::default(),
392            text: Default::default(),
393            non_exhaustive: NonExhaustive,
394            title_style: Default::default(),
395        }
396    }
397}
398
399impl Default for FileDialogState {
400    fn default() -> Self {
401        Self {
402            area: Default::default(),
403            active: Default::default(),
404            mode: Default::default(),
405            path: Default::default(),
406            save_name: Default::default(),
407            save_ext: Default::default(),
408            dirs: Default::default(),
409            filter: Default::default(),
410            files: Default::default(),
411            no_default_roots: Default::default(),
412            roots: Default::default(),
413            path_state: Default::default(),
414            root_state: Default::default(),
415            dir_state: Default::default(),
416            file_state: Default::default(),
417            save_name_state: Default::default(),
418            new_state: Default::default(),
419            cancel_state: Default::default(),
420            ok_state: Default::default(),
421        }
422    }
423}
424
425impl<'a> Default for FileDialog<'a> {
426    fn default() -> Self {
427        Self {
428            block: Default::default(),
429            style: Default::default(),
430            layout: Default::default(),
431            list_style: Default::default(),
432            roots_style: Default::default(),
433            text_style: Default::default(),
434            button_style: Default::default(),
435            ok_text: "Ok",
436            cancel_text: "Cancel",
437        }
438    }
439}
440
441impl<'a> FileDialog<'a> {
442    /// New dialog
443    pub fn new() -> Self {
444        Self::default()
445    }
446
447    /// Text for the ok button.
448    pub fn ok_text(mut self, txt: &'a str) -> Self {
449        self.ok_text = txt;
450        self
451    }
452
453    /// Text for the cancel button.
454    pub fn cancel_text(mut self, txt: &'a str) -> Self {
455        self.cancel_text = txt;
456        self
457    }
458
459    /// Block
460    pub fn block(mut self, block: Block<'a>) -> Self {
461        self.block = Some(block);
462        self.block = self.block.map(|v| v.style(self.style));
463        self
464    }
465
466    /// Base style
467    pub fn style(mut self, style: Style) -> Self {
468        self.style = style;
469        self.block = self.block.map(|v| v.style(style));
470        self
471    }
472
473    /// Style for the lists.
474    pub fn list_style(mut self, style: ListStyle) -> Self {
475        self.list_style = Some(style);
476        self
477    }
478
479    /// Filesystem roots style.
480    pub fn roots_style(mut self, style: ListStyle) -> Self {
481        self.roots_style = Some(style);
482        self
483    }
484
485    /// Textfield style.
486    pub fn text_style(mut self, style: TextStyle) -> Self {
487        self.text_style = Some(style);
488        self
489    }
490
491    /// Button style.
492    pub fn button_style(mut self, style: ButtonStyle) -> Self {
493        self.button_style = Some(style);
494        self
495    }
496
497    /// Margin constraint for the left side.
498    pub fn left(mut self, left: Constraint) -> Self {
499        self.layout = self.layout.left(left);
500        self
501    }
502
503    /// Margin constraint for the top side.
504    pub fn top(mut self, top: Constraint) -> Self {
505        self.layout = self.layout.top(top);
506        self
507    }
508
509    /// Margin constraint for the right side.
510    pub fn right(mut self, right: Constraint) -> Self {
511        self.layout = self.layout.right(right);
512        self
513    }
514
515    /// Margin constraint for the bottom side.
516    pub fn bottom(mut self, bottom: Constraint) -> Self {
517        self.layout = self.layout.bottom(bottom);
518        self
519    }
520
521    /// Put at a fixed position.
522    pub fn position(mut self, pos: Position) -> Self {
523        self.layout = self.layout.position(pos);
524        self
525    }
526
527    /// Constraint for the width.
528    pub fn width(mut self, width: Constraint) -> Self {
529        self.layout = self.layout.width(width);
530        self
531    }
532
533    /// Constraint for the height.
534    pub fn height(mut self, height: Constraint) -> Self {
535        self.layout = self.layout.height(height);
536        self
537    }
538
539    /// Set at a fixed size.
540    pub fn size(mut self, size: Size) -> Self {
541        self.layout = self.layout.size(size);
542        self
543    }
544
545    /// All styles.
546    pub fn styles(mut self, styles: FileDialogStyle) -> Self {
547        self.style = styles.style;
548        if styles.block.is_some() {
549            self.block = styles.block;
550        }
551        if let Some(border_style) = styles.border_style {
552            self.block = self.block.map(|v| v.border_style(border_style));
553        }
554        if let Some(title_style) = styles.title_style {
555            self.block = self.block.map(|v| v.title_style(title_style));
556        }
557        self.block = self.block.map(|v| v.style(self.style));
558        if let Some(layout) = styles.layout {
559            self.layout = layout;
560        }
561        if styles.list.is_some() {
562            self.list_style = styles.list;
563        }
564        if styles.roots.is_some() {
565            self.roots_style = styles.roots;
566        }
567        if styles.text.is_some() {
568            self.text_style = styles.text;
569        }
570        if styles.button.is_some() {
571            self.button_style = styles.button;
572        }
573        self
574    }
575}
576
577#[derive(Debug, Default)]
578struct EditDirName<'a> {
579    edit_dir: TextInput<'a>,
580}
581
582#[derive(Debug, Default, Clone)]
583struct EditDirNameState {
584    edit_dir: TextInputState,
585}
586
587impl StatefulWidget for EditDirName<'_> {
588    type State = EditDirNameState;
589
590    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
591        self.edit_dir.render(area, buf, &mut state.edit_dir);
592    }
593}
594
595impl HasScreenCursor for EditDirNameState {
596    fn screen_cursor(&self) -> Option<(u16, u16)> {
597        self.edit_dir.screen_cursor()
598    }
599}
600
601impl RelocatableState for EditDirNameState {
602    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
603        self.edit_dir.relocate(shift, clip);
604    }
605}
606
607impl HandleEvent<Event, Regular, EditOutcome> for EditDirNameState {
608    fn handle(&mut self, event: &Event, qualifier: Regular) -> EditOutcome {
609        match self.edit_dir.handle(event, qualifier) {
610            TextOutcome::Continue => EditOutcome::Continue,
611            TextOutcome::Unchanged => EditOutcome::Unchanged,
612            TextOutcome::Changed => EditOutcome::Changed,
613            TextOutcome::TextChanged => EditOutcome::Changed,
614        }
615    }
616}
617
618impl HandleEvent<Event, MouseOnly, EditOutcome> for EditDirNameState {
619    fn handle(&mut self, event: &Event, qualifier: MouseOnly) -> EditOutcome {
620        match self.edit_dir.handle(event, qualifier) {
621            TextOutcome::Continue => EditOutcome::Continue,
622            TextOutcome::Unchanged => EditOutcome::Unchanged,
623            TextOutcome::Changed => EditOutcome::Changed,
624            TextOutcome::TextChanged => EditOutcome::Changed,
625        }
626    }
627}
628
629impl HasFocus for EditDirNameState {
630    fn build(&self, builder: &mut FocusBuilder) {
631        builder.leaf_widget(self);
632    }
633
634    fn focus(&self) -> FocusFlag {
635        self.edit_dir.focus()
636    }
637
638    fn area(&self) -> Rect {
639        self.edit_dir.area()
640    }
641}
642
643impl StatefulWidget for FileDialog<'_> {
644    type State = FileDialogState;
645
646    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
647        state.area = area;
648
649        if !state.active {
650            return;
651        }
652
653        let block;
654        let block = if let Some(block) = self.block.as_ref() {
655            block
656        } else {
657            block = Block::bordered()
658                .title(match state.mode {
659                    Mode::Open => " Open ",
660                    Mode::OpenMany => " Open ",
661                    Mode::Save => " Save ",
662                    Mode::Dir => " Directory ",
663                })
664                .style(self.style);
665            &block
666        };
667
668        let layout = self.layout.layout_dialog(
669            area,
670            block_padding2(block),
671            [
672                Constraint::Percentage(20),
673                Constraint::Percentage(30),
674                Constraint::Percentage(50),
675            ],
676            0,
677            Flex::Center,
678        );
679        state.area = layout.area();
680
681        reset_buf_area(layout.area(), buf);
682        block.render(area, buf);
683
684        match state.mode {
685            Mode::Open => {
686                render_open(&self, layout.widget_for(DialogItem::Content), buf, state);
687            }
688            Mode::OpenMany => {
689                render_open_many(&self, layout.widget_for(DialogItem::Content), buf, state);
690            }
691            Mode::Save => {
692                render_save(&self, layout.widget_for(DialogItem::Content), buf, state);
693            }
694            Mode::Dir => {
695                render_open_dir(&self, layout.widget_for(DialogItem::Content), buf, state);
696            }
697        }
698
699        let mut l_n = layout.widget_for(DialogItem::Button(1));
700        l_n.width = 10;
701        Button::new(Text::from("New").alignment(Alignment::Center))
702            .styles_opt(self.button_style.clone())
703            .render(l_n, buf, &mut state.new_state);
704
705        let l_oc = Layout::horizontal([Constraint::Length(10), Constraint::Length(10)])
706            .spacing(1)
707            .flex(Flex::End)
708            .split(layout.widget_for(DialogItem::Button(2)));
709
710        Button::new(Text::from(self.cancel_text).alignment(Alignment::Center))
711            .styles_opt(self.button_style.clone())
712            .render(l_oc[0], buf, &mut state.cancel_state);
713
714        Button::new(Text::from(self.ok_text).alignment(Alignment::Center))
715            .styles_opt(self.button_style.clone())
716            .render(l_oc[1], buf, &mut state.ok_state);
717    }
718}
719
720fn render_open_dir(
721    widget: &FileDialog<'_>,
722    area: Rect,
723    buf: &mut Buffer,
724    state: &mut FileDialogState,
725) {
726    let l_grid = layout_as_grid(
727        area,
728        Layout::horizontal([
729            Constraint::Percentage(20), //
730            Constraint::Percentage(80),
731        ]),
732        Layout::vertical([
733            Constraint::Length(1), //
734            Constraint::Fill(1),
735        ]),
736    );
737
738    //
739    let mut l_path = l_grid.widget_for((1, 0));
740    l_path.width = l_path.width.saturating_sub(1);
741    TextInput::new()
742        .styles_opt(widget.text_style.clone())
743        .render(l_path, buf, &mut state.path_state);
744
745    List::default()
746        .items(state.roots.iter().map(|v| {
747            let s = v.0.to_string_lossy();
748            ListItem::from(format!("{}", s))
749        }))
750        .scroll(Scroll::new())
751        .styles_opt(widget.roots_style.clone())
752        .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
753
754    EditList::new(
755        List::default()
756            .items(state.dirs.iter().map(|v| {
757                let s = v.to_string_lossy();
758                ListItem::from(s)
759            }))
760            .scroll(Scroll::new())
761            .styles_opt(widget.list_style.clone()),
762        EditDirName {
763            edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
764        },
765    )
766    .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
767}
768
769fn render_open(widget: &FileDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut FileDialogState) {
770    let l_grid = layout_as_grid(
771        area,
772        Layout::horizontal([
773            Constraint::Percentage(20),
774            Constraint::Percentage(30),
775            Constraint::Percentage(50),
776        ]),
777        Layout::new(
778            Direction::Vertical,
779            [Constraint::Length(1), Constraint::Fill(1)],
780        ),
781    );
782
783    //
784    let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
785    l_path.width = l_path.width.saturating_sub(1);
786    TextInput::new()
787        .styles_opt(widget.text_style.clone())
788        .render(l_path, buf, &mut state.path_state);
789
790    List::default()
791        .items(state.roots.iter().map(|v| {
792            let s = v.0.to_string_lossy();
793            ListItem::from(format!("{}", s))
794        }))
795        .scroll(Scroll::new())
796        .styles_opt(widget.roots_style.clone())
797        .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
798
799    EditList::new(
800        List::default()
801            .items(state.dirs.iter().map(|v| {
802                let s = v.to_string_lossy();
803                ListItem::from(s)
804            }))
805            .scroll(Scroll::new())
806            .styles_opt(widget.list_style.clone()),
807        EditDirName {
808            edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
809        },
810    )
811    .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
812
813    let FileStateMode::Open(file_state) = &mut state.file_state else {
814        panic!("invalid mode");
815    };
816    List::default()
817        .items(state.files.iter().map(|v| {
818            let s = v.to_string_lossy();
819            ListItem::from(s)
820        }))
821        .scroll(Scroll::new())
822        .styles_opt(widget.list_style.clone())
823        .render(l_grid.widget_for((2, 1)), buf, file_state);
824}
825
826fn render_open_many(
827    widget: &FileDialog<'_>,
828    area: Rect,
829    buf: &mut Buffer,
830    state: &mut FileDialogState,
831) {
832    let l_grid = layout_as_grid(
833        area,
834        Layout::horizontal([
835            Constraint::Percentage(20),
836            Constraint::Percentage(30),
837            Constraint::Percentage(50),
838        ]),
839        Layout::new(
840            Direction::Vertical,
841            [Constraint::Length(1), Constraint::Fill(1)],
842        ),
843    );
844
845    //
846    let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
847    l_path.width = l_path.width.saturating_sub(1);
848    TextInput::new()
849        .styles_opt(widget.text_style.clone())
850        .render(l_path, buf, &mut state.path_state);
851
852    List::default()
853        .items(state.roots.iter().map(|v| {
854            let s = v.0.to_string_lossy();
855            ListItem::from(format!("{}", s))
856        }))
857        .scroll(Scroll::new())
858        .styles_opt(widget.roots_style.clone())
859        .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
860
861    EditList::new(
862        List::default()
863            .items(state.dirs.iter().map(|v| {
864                let s = v.to_string_lossy();
865                ListItem::from(s)
866            }))
867            .scroll(Scroll::new())
868            .styles_opt(widget.list_style.clone()),
869        EditDirName {
870            edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
871        },
872    )
873    .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
874
875    let FileStateMode::OpenMany(file_state) = &mut state.file_state else {
876        panic!("invalid mode");
877    };
878    List::default()
879        .items(state.files.iter().map(|v| {
880            let s = v.to_string_lossy();
881            ListItem::from(s)
882        }))
883        .scroll(Scroll::new())
884        .styles_opt(widget.list_style.clone())
885        .render(l_grid.widget_for((2, 1)), buf, file_state);
886}
887
888fn render_save(widget: &FileDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut FileDialogState) {
889    let l_grid = layout_as_grid(
890        area,
891        Layout::horizontal([
892            Constraint::Percentage(20),
893            Constraint::Percentage(30),
894            Constraint::Percentage(50),
895        ]),
896        Layout::new(
897            Direction::Vertical,
898            [
899                Constraint::Length(1),
900                Constraint::Fill(1),
901                Constraint::Length(1),
902            ],
903        ),
904    );
905
906    //
907    let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
908    l_path.width = l_path.width.saturating_sub(1);
909    TextInput::new()
910        .styles_opt(widget.text_style.clone())
911        .render(l_path, buf, &mut state.path_state);
912
913    List::default()
914        .items(state.roots.iter().map(|v| {
915            let s = v.0.to_string_lossy();
916            ListItem::from(format!("{}", s))
917        }))
918        .scroll(Scroll::new())
919        .styles_opt(widget.roots_style.clone())
920        .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
921
922    EditList::new(
923        List::default()
924            .items(state.dirs.iter().map(|v| {
925                let s = v.to_string_lossy();
926                ListItem::from(s)
927            }))
928            .scroll(Scroll::new())
929            .styles_opt(widget.list_style.clone()),
930        EditDirName {
931            edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
932        },
933    )
934    .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
935
936    let FileStateMode::Save(file_state) = &mut state.file_state else {
937        panic!("invalid mode");
938    };
939    List::default()
940        .items(state.files.iter().map(|v| {
941            let s = v.to_string_lossy();
942            ListItem::from(s)
943        }))
944        .scroll(Scroll::new())
945        .styles_opt(widget.list_style.clone())
946        .render(l_grid.widget_for((2, 1)), buf, file_state);
947
948    TextInput::new()
949        .styles_opt(widget.text_style.clone())
950        .render(l_grid.widget_for((2, 2)), buf, &mut state.save_name_state);
951}
952
953impl FileDialogState {
954    pub fn new() -> Self {
955        Self::default()
956    }
957
958    pub fn active(&self) -> bool {
959        self.active
960    }
961
962    /// Set a filter.
963    pub fn set_filter(&mut self, filter: impl Fn(&Path) -> bool + 'static) {
964        self.filter = Some(Box::new(filter));
965    }
966
967    /// Set the last path. This will be shown in the roots list.
968    /// And it will be the preferred start directory instead of
969    /// the one given [Self::open_dialog], [Self::directory_dialog]
970    /// and [Self::save_dialog].
971    pub fn set_last_path(&mut self, last: &Path) {
972        self.path = last.into();
973    }
974
975    /// Use the default set of roots.
976    pub fn use_default_roots(&mut self, roots: bool) {
977        self.no_default_roots = !roots;
978    }
979
980    /// Don't use default set of roots.
981    pub fn no_default_roots(&mut self) {
982        self.no_default_roots = true;
983    }
984
985    /// Add a root path.
986    pub fn add_root(&mut self, name: impl AsRef<str>, path: impl Into<PathBuf>) {
987        self.roots
988            .push((OsString::from(name.as_ref()), path.into()))
989    }
990
991    /// Clear all roots.
992    pub fn clear_roots(&mut self) {
993        self.roots.clear();
994    }
995
996    /// Append the default roots.
997    pub fn default_roots(&mut self, start: &Path, last: &Path) {
998        if last.exists() {
999            self.roots.push((
1000                OsString::from("Last"), //
1001                last.into(),
1002            ));
1003        }
1004        self.roots.push((
1005            OsString::from("Start"), //
1006            start.into(),
1007        ));
1008
1009        #[cfg(feature = "user_directories")]
1010        {
1011            if let Some(home) = home_dir() {
1012                self.roots.push((OsString::from("Home"), home));
1013            }
1014            if let Some(documents) = document_dir() {
1015                self.roots.push((OsString::from("Documents"), documents));
1016            }
1017        }
1018
1019        #[cfg(feature = "user_directories")]
1020        {
1021            let disks = Disks::new_with_refreshed_list();
1022            for d in disks.list() {
1023                self.roots
1024                    .push((d.name().to_os_string(), d.mount_point().to_path_buf()));
1025            }
1026        }
1027
1028        self.root_state.select(Some(0));
1029    }
1030
1031    /// Show as directory-dialog.
1032    pub fn directory_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
1033        let path = path.as_ref();
1034        let old_path = self.path.clone();
1035
1036        self.active = true;
1037        self.mode = Mode::Dir;
1038        self.file_state = FileStateMode::Dir(FocusFlag::new());
1039        self.save_name = None;
1040        self.save_ext = None;
1041        self.dirs.clear();
1042        self.files.clear();
1043        self.path = Default::default();
1044        if !self.no_default_roots {
1045            self.clear_roots();
1046            self.default_roots(path, &old_path);
1047            if old_path.exists() {
1048                self.set_path(&old_path)?;
1049            } else {
1050                self.set_path(path)?;
1051            }
1052        } else {
1053            self.set_path(path)?;
1054        }
1055        self.build_focus().focus(&self.dir_state);
1056        Ok(())
1057    }
1058
1059    /// Show as open-dialog.
1060    pub fn open_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
1061        let path = path.as_ref();
1062        let old_path = self.path.clone();
1063
1064        self.active = true;
1065        self.mode = Mode::Open;
1066        self.file_state = FileStateMode::Open(Default::default());
1067        self.save_name = None;
1068        self.save_ext = None;
1069        self.dirs.clear();
1070        self.files.clear();
1071        self.path = Default::default();
1072        if !self.no_default_roots {
1073            self.clear_roots();
1074            self.default_roots(path, &old_path);
1075            if old_path.exists() {
1076                self.set_path(&old_path)?;
1077            } else {
1078                self.set_path(path)?;
1079            }
1080        } else {
1081            self.set_path(path)?;
1082        }
1083        self.build_focus().focus(&self.file_state);
1084        Ok(())
1085    }
1086
1087    /// Show as open-dialog with multiple selection
1088    pub fn open_many_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
1089        let path = path.as_ref();
1090        let old_path = self.path.clone();
1091
1092        self.active = true;
1093        self.mode = Mode::OpenMany;
1094        self.file_state = FileStateMode::OpenMany(Default::default());
1095        self.save_name = None;
1096        self.save_ext = None;
1097        self.dirs.clear();
1098        self.files.clear();
1099        self.path = Default::default();
1100        if !self.no_default_roots {
1101            self.clear_roots();
1102            self.default_roots(path, &old_path);
1103            if old_path.exists() {
1104                self.set_path(&old_path)?;
1105            } else {
1106                self.set_path(path)?;
1107            }
1108        } else {
1109            self.set_path(path)?;
1110        }
1111        self.build_focus().focus(&self.file_state);
1112        Ok(())
1113    }
1114
1115    /// Show as save-dialog.
1116    pub fn save_dialog(
1117        &mut self,
1118        path: impl AsRef<Path>,
1119        name: impl AsRef<str>,
1120    ) -> Result<(), io::Error> {
1121        self.save_dialog_ext(path, name, "")
1122    }
1123
1124    /// Show as save-dialog.
1125    pub fn save_dialog_ext(
1126        &mut self,
1127        path: impl AsRef<Path>,
1128        name: impl AsRef<str>,
1129        ext: impl AsRef<str>,
1130    ) -> Result<(), io::Error> {
1131        let path = path.as_ref();
1132        let old_path = self.path.clone();
1133
1134        self.active = true;
1135        self.mode = Mode::Save;
1136        self.file_state = FileStateMode::Save(Default::default());
1137        self.save_name = Some(OsString::from(name.as_ref()));
1138        self.save_ext = Some(OsString::from(ext.as_ref()));
1139        self.dirs.clear();
1140        self.files.clear();
1141        self.path = Default::default();
1142        if !self.no_default_roots {
1143            self.clear_roots();
1144            self.default_roots(path, &old_path);
1145            if old_path.exists() {
1146                self.set_path(&old_path)?;
1147            } else {
1148                self.set_path(path)?;
1149            }
1150        } else {
1151            self.set_path(path)?;
1152        }
1153        self.build_focus().focus(&self.save_name_state);
1154        Ok(())
1155    }
1156
1157    fn find_parent(&self, path: &Path) -> Option<PathBuf> {
1158        if path == Path::new(".") || path.file_name().is_none() {
1159            let parent = path.join("..");
1160            let canon_parent = parent.canonicalize().ok();
1161            let canon_path = path.canonicalize().ok();
1162            if canon_parent == canon_path {
1163                None
1164            } else if parent.exists() && parent.is_dir() {
1165                Some(parent)
1166            } else {
1167                None
1168            }
1169        } else if let Some(parent) = path.parent() {
1170            if parent.exists() && parent.is_dir() {
1171                Some(parent.to_path_buf())
1172            } else {
1173                None
1174            }
1175        } else {
1176            None
1177        }
1178    }
1179
1180    // change the path
1181    fn set_path(&mut self, path: &Path) -> Result<FileOutcome, io::Error> {
1182        let old = self.path.clone();
1183        let path = path.to_path_buf();
1184
1185        if old != path {
1186            let mut dirs = Vec::new();
1187            let mut files = Vec::new();
1188
1189            if self.find_parent(&path).is_some() {
1190                dirs.push(OsString::from(".."));
1191            }
1192
1193            for r in path.read_dir()? {
1194                let Ok(r) = r else {
1195                    continue;
1196                };
1197
1198                if let Ok(meta) = r.metadata() {
1199                    if meta.is_dir() {
1200                        dirs.push(r.file_name());
1201                    } else if meta.is_file() {
1202                        if let Some(filter) = self.filter.as_ref() {
1203                            if filter(&r.path()) {
1204                                files.push(r.file_name());
1205                            }
1206                        } else {
1207                            files.push(r.file_name());
1208                        }
1209                    }
1210                }
1211            }
1212
1213            self.path = path;
1214            self.dirs = dirs;
1215            self.files = files;
1216
1217            self.path_state.set_text(self.path.to_string_lossy());
1218            self.path_state.move_to_line_end(false);
1219
1220            self.dir_state.cancel();
1221            if !self.dirs.is_empty() {
1222                self.dir_state.list.select(Some(0));
1223            } else {
1224                self.dir_state.list.select(None);
1225            }
1226            self.dir_state.list.set_offset(0);
1227            if !self.files.is_empty() {
1228                self.file_state.select(Some(0));
1229                if let Some(name) = &self.save_name {
1230                    self.save_name_state.set_text(name.to_string_lossy());
1231                } else {
1232                    self.save_name_state
1233                        .set_text(self.files[0].to_string_lossy());
1234                }
1235            } else {
1236                self.file_state.select(None);
1237                if let Some(name) = &self.save_name {
1238                    self.save_name_state.set_text(name.to_string_lossy());
1239                } else {
1240                    self.save_name_state.set_text("");
1241                }
1242            }
1243            self.file_state.set_offset(0);
1244
1245            Ok(FileOutcome::Changed)
1246        } else {
1247            Ok(FileOutcome::Unchanged)
1248        }
1249    }
1250
1251    fn use_path_input(&mut self) -> Result<FileOutcome, io::Error> {
1252        let path = PathBuf::from(self.path_state.text());
1253        if !path.exists() || !path.is_dir() {
1254            self.path_state.invalid = true;
1255        } else {
1256            self.path_state.invalid = false;
1257            self.set_path(&path)?;
1258        }
1259
1260        Ok(FileOutcome::Changed)
1261    }
1262
1263    fn chdir(&mut self, dir: &OsString) -> Result<FileOutcome, io::Error> {
1264        if dir == &OsString::from("..") {
1265            if let Some(parent) = self.find_parent(&self.path) {
1266                self.set_path(&parent)
1267            } else {
1268                Ok(FileOutcome::Unchanged)
1269            }
1270        } else {
1271            self.set_path(&self.path.join(dir))
1272        }
1273    }
1274
1275    fn chroot_selected(&mut self) -> Result<FileOutcome, io::Error> {
1276        if let Some(select) = self.root_state.selected() {
1277            if let Some(d) = self.roots.get(select).cloned() {
1278                self.set_path(&d.1)?;
1279                return Ok(FileOutcome::Changed);
1280            }
1281        }
1282        Ok(FileOutcome::Unchanged)
1283    }
1284
1285    fn chdir_selected(&mut self) -> Result<FileOutcome, io::Error> {
1286        if let Some(select) = self.dir_state.list.selected() {
1287            if let Some(dir) = self.dirs.get(select).cloned() {
1288                self.chdir(&dir)?;
1289                return Ok(FileOutcome::Changed);
1290            }
1291        }
1292        Ok(FileOutcome::Unchanged)
1293    }
1294
1295    /// Set the selected file to the new name field.
1296    fn name_selected(&mut self) -> Result<FileOutcome, io::Error> {
1297        if let Some(select) = self.file_state.first_selected() {
1298            if let Some(file) = self.files.get(select).cloned() {
1299                let name = file.to_string_lossy();
1300                self.save_name_state.set_text(name);
1301                return Ok(FileOutcome::Changed);
1302            }
1303        }
1304        Ok(FileOutcome::Continue)
1305    }
1306
1307    /// Start creating a directory.
1308    fn start_edit_dir(&mut self) -> FileOutcome {
1309        if !self.dir_state.is_editing() {
1310            self.build_focus().focus(&self.dir_state);
1311
1312            self.dirs.push(OsString::from(""));
1313            self.dir_state.editor.edit_dir.set_text("");
1314            self.dir_state.edit_new(self.dirs.len() - 1);
1315
1316            FileOutcome::Changed
1317        } else {
1318            FileOutcome::Continue
1319        }
1320    }
1321
1322    fn cancel_edit_dir(&mut self) -> FileOutcome {
1323        if self.dir_state.is_editing() {
1324            self.dir_state.cancel();
1325            self.dirs.remove(self.dirs.len() - 1);
1326            FileOutcome::Changed
1327        } else {
1328            FileOutcome::Continue
1329        }
1330    }
1331
1332    fn commit_edit_dir(&mut self) -> Result<FileOutcome, io::Error> {
1333        if self.dir_state.is_editing() {
1334            let name = self.dir_state.editor.edit_dir.text().trim();
1335            let path = self.path.join(name);
1336            if fs::create_dir(&path).is_err() {
1337                self.dir_state.editor.edit_dir.invalid = true;
1338                Ok(FileOutcome::Changed)
1339            } else {
1340                self.dir_state.commit();
1341                if self.mode == Mode::Save {
1342                    self.build_focus().focus_no_lost(&self.save_name_state);
1343                }
1344                self.set_path(&path)
1345            }
1346        } else {
1347            Ok(FileOutcome::Unchanged)
1348        }
1349    }
1350
1351    /// Cancel the dialog.
1352    fn close_cancel(&mut self) -> FileOutcome {
1353        self.active = false;
1354        self.dir_state.cancel();
1355        FileOutcome::Cancel
1356    }
1357
1358    /// Choose the selected and close the dialog.
1359    fn choose_selected(&mut self) -> FileOutcome {
1360        match self.mode {
1361            Mode::Open => {
1362                if let Some(select) = self.file_state.first_selected() {
1363                    if let Some(file) = self.files.get(select).cloned() {
1364                        self.active = false;
1365                        return FileOutcome::Ok(self.path.join(file));
1366                    }
1367                }
1368            }
1369            Mode::OpenMany => {
1370                let sel = self
1371                    .file_state
1372                    .selected()
1373                    .iter()
1374                    .map(|&idx| self.path.join(self.files.get(idx).expect("file")))
1375                    .collect::<Vec<_>>();
1376                self.active = false;
1377                return FileOutcome::OkList(sel);
1378            }
1379            Mode::Save => {
1380                let mut path = self.path.join(self.save_name_state.text().trim());
1381                if let Some(ext) = &self.save_ext {
1382                    if !ext.is_empty() {
1383                        path.set_extension(ext);
1384                    }
1385                }
1386                self.active = false;
1387                return FileOutcome::Ok(path);
1388            }
1389            Mode::Dir => {
1390                if let Some(select) = self.dir_state.list.selected() {
1391                    if let Some(dir) = self.dirs.get(select).cloned() {
1392                        self.active = false;
1393                        if dir != ".." {
1394                            return FileOutcome::Ok(self.path.join(dir));
1395                        } else {
1396                            return FileOutcome::Ok(self.path.clone());
1397                        }
1398                    }
1399                }
1400            }
1401        }
1402        FileOutcome::Continue
1403    }
1404}
1405
1406impl HasScreenCursor for FileDialogState {
1407    fn screen_cursor(&self) -> Option<(u16, u16)> {
1408        if self.active {
1409            self.path_state
1410                .screen_cursor()
1411                .or_else(|| self.save_name_state.screen_cursor())
1412                .or_else(|| self.dir_state.screen_cursor())
1413        } else {
1414            None
1415        }
1416    }
1417}
1418
1419impl HasFocus for FileDialogState {
1420    fn build(&self, _builder: &mut FocusBuilder) {
1421        // don't expose our inner workings.
1422    }
1423
1424    fn focus(&self) -> FocusFlag {
1425        unimplemented!("not available")
1426    }
1427
1428    fn area(&self) -> Rect {
1429        unimplemented!("not available")
1430    }
1431}
1432
1433impl RelocatableState for FileDialogState {
1434    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
1435        self.area.relocate(shift, clip);
1436        self.path_state.relocate(shift, clip);
1437        self.root_state.relocate(shift, clip);
1438        self.dir_state.relocate(shift, clip);
1439        self.file_state.relocate(shift, clip);
1440        self.save_name_state.relocate(shift, clip);
1441        self.new_state.relocate(shift, clip);
1442        self.cancel_state.relocate(shift, clip);
1443        self.ok_state.relocate(shift, clip);
1444    }
1445}
1446
1447impl FileDialogState {
1448    fn build_focus(&self) -> Focus {
1449        let mut fb = FocusBuilder::default();
1450        fb.widget(&self.dir_state);
1451        fb.widget(&self.file_state);
1452        if self.mode == Mode::Save {
1453            fb.widget(&self.save_name_state);
1454        }
1455        fb.widget(&self.ok_state);
1456        fb.widget(&self.cancel_state);
1457        fb.widget(&self.new_state);
1458        fb.widget(&self.root_state);
1459        fb.widget(&self.path_state);
1460        fb.build()
1461    }
1462}
1463
1464impl HandleEvent<Event, Dialog, Result<FileOutcome, io::Error>> for FileDialogState {
1465    fn handle(&mut self, event: &Event, _qualifier: Dialog) -> Result<FileOutcome, io::Error> {
1466        if !self.active {
1467            return Ok(FileOutcome::Continue);
1468        }
1469
1470        let mut focus = self.build_focus();
1471        let mut f: FileOutcome = focus.handle(event, Regular).into();
1472        let next_focus: Option<&dyn HasFocus> = match event {
1473            ct_event!(keycode press F(1)) => Some(&self.root_state),
1474            ct_event!(keycode press F(2)) => Some(&self.dir_state),
1475            ct_event!(keycode press F(3)) => Some(&self.file_state),
1476            ct_event!(keycode press F(4)) => Some(&self.path_state),
1477            ct_event!(keycode press F(5)) => Some(&self.save_name_state),
1478            _ => None,
1479        };
1480        if let Some(next_focus) = next_focus {
1481            focus.focus(next_focus);
1482            f = FileOutcome::Changed;
1483        }
1484
1485        let r = 'f: {
1486            event_flow!(break 'f handle_path(self, event)?);
1487            event_flow!(
1488                break 'f if self.mode == Mode::Save {
1489                    handle_name(self, event)?
1490                } else {
1491                    FileOutcome::Continue
1492                }
1493            );
1494            event_flow!(break 'f handle_files(self, event)?);
1495            event_flow!(break 'f handle_dirs(self, event)?);
1496            event_flow!(break 'f handle_roots(self, event)?);
1497            event_flow!(break 'f handle_new(self, event)?);
1498            event_flow!(break 'f handle_cancel(self, event)?);
1499            event_flow!(break 'f handle_ok(self, event)?);
1500            FileOutcome::Continue
1501        };
1502
1503        event_flow!(max(f, r));
1504        // capture events
1505        Ok(FileOutcome::Unchanged)
1506    }
1507}
1508
1509fn handle_new(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1510    event_flow!(match state.new_state.handle(event, Regular) {
1511        ButtonOutcome::Pressed => {
1512            state.start_edit_dir()
1513        }
1514        r => Outcome::from(r).into(),
1515    });
1516    event_flow!(match event {
1517        ct_event!(key press CONTROL-'n') => {
1518            state.start_edit_dir()
1519        }
1520        _ => FileOutcome::Continue,
1521    });
1522    Ok(FileOutcome::Continue)
1523}
1524
1525fn handle_ok(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1526    event_flow!(match state.ok_state.handle(event, Regular) {
1527        ButtonOutcome::Pressed => state.choose_selected(),
1528        r => Outcome::from(r).into(),
1529    });
1530    Ok(FileOutcome::Continue)
1531}
1532
1533fn handle_cancel(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1534    event_flow!(match state.cancel_state.handle(event, Regular) {
1535        ButtonOutcome::Pressed => {
1536            state.close_cancel()
1537        }
1538        r => Outcome::from(r).into(),
1539    });
1540    event_flow!(match event {
1541        ct_event!(keycode press Esc) => {
1542            state.close_cancel()
1543        }
1544        _ => FileOutcome::Continue,
1545    });
1546    Ok(FileOutcome::Continue)
1547}
1548
1549fn handle_name(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1550    event_flow!(Outcome::from(state.save_name_state.handle(event, Regular)));
1551    if state.save_name_state.is_focused() {
1552        event_flow!(match event {
1553            ct_event!(keycode press Enter) => {
1554                state.choose_selected()
1555            }
1556            _ => FileOutcome::Continue,
1557        });
1558    }
1559    Ok(FileOutcome::Continue)
1560}
1561
1562fn handle_path(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1563    event_flow!(Outcome::from(state.path_state.handle(event, Regular)));
1564    if state.path_state.is_focused() {
1565        event_flow!(match event {
1566            ct_event!(keycode press Enter) => {
1567                state.use_path_input()?;
1568                state.build_focus().focus_no_lost(&state.dir_state.list);
1569                FileOutcome::Changed
1570            }
1571            _ => FileOutcome::Continue,
1572        });
1573    }
1574    on_lost!(
1575        state.path_state => {
1576            state.use_path_input()?
1577        }
1578    );
1579    Ok(FileOutcome::Continue)
1580}
1581
1582fn handle_roots(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1583    event_flow!(match state.root_state.handle(event, Regular) {
1584        Outcome::Changed => {
1585            state.chroot_selected()?
1586        }
1587        r => r.into(),
1588    });
1589    Ok(FileOutcome::Continue)
1590}
1591
1592fn handle_dirs(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1593    // capture F2. starts edit/selects dir otherwise.
1594    if matches!(event, ct_event!(keycode press F(2))) {
1595        return Ok(FileOutcome::Continue);
1596    }
1597
1598    event_flow!(match state.dir_state.handle(event, Regular) {
1599        EditOutcome::Edit => {
1600            state.chdir_selected()?
1601        }
1602        EditOutcome::Cancel => {
1603            state.cancel_edit_dir()
1604        }
1605        EditOutcome::Commit | EditOutcome::CommitAndAppend | EditOutcome::CommitAndEdit => {
1606            state.commit_edit_dir()?
1607        }
1608        r => {
1609            Outcome::from(r).into()
1610        }
1611    });
1612    if state.dir_state.list.is_focused() {
1613        event_flow!(handle_nav(&mut state.dir_state.list, &state.dirs, event)?);
1614    }
1615    Ok(FileOutcome::Continue)
1616}
1617
1618fn handle_files(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1619    if state.file_state.is_focused() {
1620        event_flow!(log f0: match event {
1621            ct_event!(mouse any for m) if state.file_state.is_double_click(m) => {
1622                state.choose_selected()
1623            }
1624            ct_event!(keycode press Enter) => {
1625                state.choose_selected()
1626            }
1627            _ => FileOutcome::Continue,
1628        });
1629        event_flow!(log f1: {
1630            match &mut state.file_state {
1631                FileStateMode::Open(st) => handle_nav(st, &state.files, event)?,
1632                FileStateMode::OpenMany(st) => handle_nav_many(st, &state.files, event)?,
1633                FileStateMode::Save(st) => match handle_nav(st, &state.files, event)? {
1634                    FileOutcome::Changed => state.name_selected()?,
1635                    r => r,
1636                },
1637                FileStateMode::Dir(_) => FileOutcome::Continue,
1638            }
1639        });
1640    }
1641    event_flow!(match &mut state.file_state {
1642        FileStateMode::Open(st) => {
1643            st.handle(event, Regular).into()
1644        }
1645        FileStateMode::OpenMany(st) => {
1646            st.handle(event, Regular).into()
1647        }
1648        FileStateMode::Save(st) => {
1649            match st.handle(event, Regular) {
1650                Outcome::Changed => state.name_selected()?.into(),
1651                r => r.into(),
1652            }
1653        }
1654        FileStateMode::Dir(_) => FileOutcome::Continue,
1655    });
1656
1657    Ok(FileOutcome::Continue)
1658}
1659
1660fn handle_nav(
1661    list: &mut ListState<RowSelection>,
1662    nav: &[OsString],
1663    event: &Event,
1664) -> Result<FileOutcome, io::Error> {
1665    event_flow!(match event {
1666        ct_event!(key press c) => {
1667            let next = find_next_by_key(*c, list.selected().unwrap_or(0), nav);
1668            if let Some(next) = next {
1669                list.move_to(next).into()
1670            } else {
1671                FileOutcome::Unchanged
1672            }
1673        }
1674        _ => FileOutcome::Continue,
1675    });
1676    Ok(FileOutcome::Continue)
1677}
1678
1679fn handle_nav_many(
1680    list: &mut ListState<RowSetSelection>,
1681    nav: &[OsString],
1682    event: &Event,
1683) -> Result<FileOutcome, io::Error> {
1684    event_flow!(match event {
1685        ct_event!(key press c) => {
1686            let next = find_next_by_key(*c, list.lead().unwrap_or(0), nav);
1687            if let Some(next) = next {
1688                list.move_to(next, false).into()
1689            } else {
1690                FileOutcome::Unchanged
1691            }
1692        }
1693        _ => FileOutcome::Continue,
1694    });
1695    Ok(FileOutcome::Continue)
1696}
1697
1698#[allow(clippy::question_mark)]
1699fn find_next_by_key(c: char, start: usize, names: &[OsString]) -> Option<usize> {
1700    let Some(c) = c.to_lowercase().next() else {
1701        return None;
1702    };
1703
1704    let mut idx = start;
1705    let mut selected = None;
1706    loop {
1707        idx += 1;
1708        if idx >= names.len() {
1709            idx = 0;
1710        }
1711        if idx == start {
1712            break;
1713        }
1714
1715        let nav = names[idx].to_string_lossy();
1716
1717        let initials = nav
1718            .split([' ', '_', '-'])
1719            .flat_map(|v| v.chars().next())
1720            .flat_map(|c| c.to_lowercase().next())
1721            .collect::<Vec<_>>();
1722        if initials.contains(&c) {
1723            selected = Some(idx);
1724            break;
1725        }
1726    }
1727
1728    selected
1729}
1730
1731/// Handle events for the popup.
1732/// Call before other handlers to deal with intersections
1733/// with other widgets.
1734pub fn handle_events(
1735    state: &mut FileDialogState,
1736    _focus: bool,
1737    event: &Event,
1738) -> Result<FileOutcome, io::Error> {
1739    HandleEvent::handle(state, event, Dialog)
1740}