Skip to main content

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, ListItem, 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 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](super::FileDialogState::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            if let Ok(disks) = Disks::new_with_refreshed_list() {
996                for d in disks.iter() {
997                    self.roots
998                        .push((d.name().to_os_string(), d.mount_point().to_path_buf()));
999                }
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}