rat_dialog/widgets/
file_dialog.rs

1use crate::{DialogState, DialogWidget, RenderContext};
2use rat_salsa::{AppContext, Control};
3use rat_widget::event::{Dialog, FileOutcome, HandleEvent};
4use rat_widget::file_dialog::FileDialogStyle;
5use rat_widget::layout::layout_middle;
6use rat_widget::text::HasScreenCursor;
7use ratatui::buffer::Buffer;
8use ratatui::layout::Rect;
9use ratatui::prelude::Constraint;
10use ratatui::widgets::StatefulWidget;
11use std::marker::PhantomData;
12use std::path::{Path, PathBuf};
13
14pub struct FileDialog {
15    widget: rat_widget::file_dialog::FileDialog<'static>,
16}
17
18pub struct FileDialogState<Global, Event, Error> {
19    state: rat_widget::file_dialog::FileDialogState,
20    tr: Box<dyn Fn(FileOutcome) -> Control<Event> + 'static>,
21    phantom: PhantomData<(Global, Event, Error)>,
22}
23
24impl FileDialog {
25    pub fn new() -> Self {
26        Self {
27            widget: Default::default(),
28        }
29    }
30
31    pub fn styles(mut self, styles: FileDialogStyle) -> Self {
32        self.widget = self.widget.styles(styles);
33        self
34    }
35}
36
37impl<Global, Event, Error> DialogWidget<Global, Event, Error> for FileDialog
38where
39    for<'a> &'a crossterm::event::Event: TryFrom<&'a Event>,
40    Global: 'static,
41    Event: Send + 'static,
42    Error: Send + 'static + From<std::io::Error>,
43{
44    type State = dyn DialogState<Global, Event, Error>;
45
46    fn render(
47        &self,
48        area: Rect,
49        buf: &mut Buffer,
50        state: &mut Self::State,
51        ctx: &mut RenderContext<'_, Global>,
52    ) -> Result<(), Error> {
53        let state = state
54            .downcast_mut::<FileDialogState<Global, Event, Error>>()
55            .expect("state");
56
57        let dlg_area = layout_middle(
58            area,
59            Constraint::Percentage(19),
60            Constraint::Percentage(19),
61            Constraint::Length(2),
62            Constraint::Length(2),
63        );
64
65        self.widget.clone().render(dlg_area, buf, &mut state.state);
66
67        ctx.set_screen_cursor(state.state.screen_cursor());
68
69        Ok(())
70    }
71}
72
73impl<Global, Event, Error> FileDialogState<Global, Event, Error>
74where
75    for<'a> &'a crossterm::event::Event: TryFrom<&'a Event>,
76    Global: 'static,
77    Event: Send + 'static,
78    Error: Send + From<std::io::Error> + 'static,
79{
80    pub fn new() -> Self {
81        Self {
82            state: rat_widget::file_dialog::FileDialogState::new(),
83            tr: Box::new(|f| Control::from(f)),
84            phantom: Default::default(),
85        }
86    }
87
88    pub fn open_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
89        self.state.open_dialog(path)?;
90        Ok(())
91    }
92
93    pub fn save_dialog(
94        &mut self,
95        path: impl AsRef<Path>,
96        name: impl AsRef<str>,
97    ) -> Result<(), Error> {
98        self.state.save_dialog(path, name)?;
99        Ok(())
100    }
101
102    pub fn save_dialog_ext(
103        &mut self,
104        path: impl AsRef<Path>,
105        name: impl AsRef<str>,
106        ext: impl AsRef<str>,
107    ) -> Result<(), Error> {
108        self.state.save_dialog_ext(path, name, ext)?;
109        Ok(())
110    }
111
112    pub fn map_outcome(&mut self, m: impl Fn(FileOutcome) -> Control<Event> + 'static) {
113        self.tr = Box::new(m);
114    }
115
116    pub fn directory_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
117        self.state.directory_dialog(path)?;
118        Ok(())
119    }
120
121    /// Set a filter.
122    pub fn set_filter(&mut self, filter: impl Fn(&Path) -> bool + 'static) {
123        self.state.set_filter(filter);
124    }
125
126    /// Use the default set of roots.
127    pub fn use_default_roots(&mut self, roots: bool) {
128        self.state.use_default_roots(roots);
129    }
130
131    /// Add a root path.
132    pub fn add_root(&mut self, name: impl AsRef<str>, path: impl Into<PathBuf>) {
133        self.state.add_root(name, path);
134    }
135
136    /// Clear all roots.
137    pub fn clear_roots(&mut self) {
138        self.state.clear_roots();
139    }
140
141    /// Append the default roots.
142    pub fn default_roots(&mut self, start: &Path, last: &Path) {
143        self.state.default_roots(start, last);
144    }
145}
146
147impl<Global, Event, Error> DialogState<Global, Event, Error>
148    for FileDialogState<Global, Event, Error>
149where
150    for<'a> &'a crossterm::event::Event: TryFrom<&'a Event>,
151    Global: 'static,
152    Event: Send + 'static,
153    Error: Send + 'static + From<std::io::Error>,
154{
155    fn active(&self) -> bool {
156        self.state.active()
157    }
158
159    fn event(
160        &mut self,
161        event: &Event,
162        _ctx: &mut AppContext<'_, Global, Event, Error>,
163    ) -> Result<Control<Event>, Error> {
164        let r = if let Ok(event) = event.try_into() {
165            let r = self.state.handle(event, Dialog)?.into();
166            (self.tr)(r)
167        } else {
168            Control::Continue
169        };
170
171        Ok(r)
172    }
173}