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