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