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