im_native_dialog/
lib.rs

1//! This crate is a wrapper around [FileDialog] for use with immediate mode gui
2//! libraries. See [ImNativeFileDialog] for more information.
3
4use std::path::PathBuf;
5
6use native_dialog::FileDialog;
7use thiserror::Error;
8
9/// Error associated with [NativeFileDialog].
10#[derive(Error, Debug)]
11pub enum ImNativeDialogError {
12    #[error("The dialog is already open.")]
13    AlreadyOpen,
14}
15
16/// A wrapper around [FileDialog] for use with immediate mode gui
17/// libraries. The `show*()` methods create a [FileDialog] in a new
18/// thread, and the result is returned to this object via
19/// [crossbeam_channel], ready to be polled by the ui using
20/// [ImNativeFileDialog::check()]
21pub struct ImNativeFileDialog<T> {
22    callback: Option<Box<dyn FnOnce(&Result<T, native_dialog::Error>) + Send>>,
23    receiver: Option<crossbeam_channel::Receiver<Result<T, native_dialog::Error>>>,
24}
25
26impl<T> Default for ImNativeFileDialog<T> {
27    fn default() -> Self {
28        Self { callback: None, receiver: None }
29    }
30}
31
32impl ImNativeFileDialog<Vec<PathBuf>> {
33    /// Shows a dialog that let users to open multiple files using [FileDialog::show_open_multiple_file()].
34    pub fn show_open_multiple_file(
35        &mut self,
36        location: Option<PathBuf>,
37    ) -> Result<(), ImNativeDialogError> {
38        self.show(|sender, dialog, callback| {
39            let dialog = match &location {
40                Some(location) => dialog.set_location(location),
41                None => dialog,
42            };
43            let result = dialog.show_open_multiple_file();
44            callback(&result);
45            sender
46                .send(result)
47                .expect("error sending show_open_multiple_file result to ui");
48            drop(location)
49        })
50    }
51}
52
53impl ImNativeFileDialog<Option<PathBuf>> {
54    /// Shows a dialog that let users to open one directory using [FileDialog::show_open_single_dir()].
55    pub fn open_single_dir(
56        &mut self,
57        location: Option<PathBuf>,
58    ) -> Result<(), ImNativeDialogError> {
59        self.show(|sender, dialog, callback| {
60            let dialog = match &location {
61                Some(location) => dialog.set_location(location),
62                None => dialog,
63            };
64            let result = dialog.show_open_single_dir();
65            callback(&result);
66            sender
67                .send(result)
68                .expect("error sending open_single_dir result to ui");
69            drop(location)
70        })
71    }
72
73    /// Shows a dialog that let users to open one file using [FileDialog::show_open_single_file()].
74    pub fn open_single_file(
75        &mut self,
76        location: Option<PathBuf>,
77    ) -> Result<(), ImNativeDialogError> {
78        self.show(|sender, dialog, callback| {
79            let dialog = match &location {
80                Some(location) => dialog.set_location(location),
81                None => dialog,
82            };
83            let result = dialog.show_open_single_file();
84            callback(&result);
85            sender
86                .send(result)
87                .expect("error sending open_single_file result to ui");
88            drop(location)
89        })
90    }
91
92    /// Shows a dialog that let users to save one file using [FileDialog::show_save_single_file()].
93    pub fn show_save_single_file(
94        &mut self,
95        location: Option<PathBuf>,
96    ) -> Result<(), ImNativeDialogError> {
97        self.show(|sender, dialog, callback| {
98            let dialog = match &location {
99                Some(location) => dialog.set_location(location),
100                None => dialog,
101            };
102            let result = dialog.show_save_single_file();
103            callback(&result);
104            sender
105                .send(result)
106                .expect("error sending show_save_single_file result to ui");
107            drop(location)
108        })
109    }
110}
111
112impl<T: Send + 'static + Default> ImNativeFileDialog<T> {
113    /// Set a callback to use for this dialog which will be called
114    /// immediately upon dialog close in the dialog monitoring thread.
115    pub fn with_callback<C>(&mut self, callback: C) -> &mut Self
116    where
117        C: FnOnce(&Result<T, native_dialog::Error>) + Send + 'static
118    {
119        self.callback = Some(Box::new(callback));
120        self
121    }
122
123    /// Show a customized version of [FileDialog], use the `run`
124    /// closure to customize the dialog and show the dialog. This
125    /// closure runs in its own thread.
126    pub fn show<
127        F: FnOnce(crossbeam_channel::Sender<Result<T, native_dialog::Error>>, FileDialog, Box<dyn FnOnce(&Result<T, native_dialog::Error>)>)
128            + Send
129            + 'static,
130    >(
131        &mut self,
132        run: F,
133    ) -> Result<(), ImNativeDialogError> {
134        if self.receiver.is_some() {
135            return Err(ImNativeDialogError::AlreadyOpen);
136        }
137
138        let (sender, receiver) = crossbeam_channel::bounded(1);
139
140        let callback = self.callback.take().unwrap_or_else(|| Box::new(|_| {}));
141        std::thread::spawn(move || {
142            let dialog = FileDialog::new();
143            run(sender, dialog, callback)
144        });
145
146        self.receiver = Some(receiver);
147
148        Ok(())
149    }
150
151    /// Check if the dialog is complete. If it is complete it will
152    /// return `Some` with the result of the dialog, otherwise will
153    /// return `None`. This will update the status of
154    /// [ImNativeFileDialog::is_open()].
155    pub fn check(&mut self) -> Option<Result<T, native_dialog::Error>> {
156        match self.receiver.take() {
157            Some(receiver) => match receiver.try_recv() {
158                Ok(result) => Some(result),
159                Err(crossbeam_channel::TryRecvError::Disconnected) => {
160                    log::warn!("OpenDialog channel disconnected");
161                    Some(Ok(T::default()))
162                }
163                Err(crossbeam_channel::TryRecvError::Empty) => {
164                    self.receiver = Some(receiver);
165                    None
166                }
167            },
168            None => None,
169        }
170    }
171
172    /// Returns `true` if the dialog is currently open, otherwise
173    /// returns `false`. Requires a previous call of
174    /// [ImNativeFileDialog::check()] to update the current status.
175    pub fn is_open(&self) -> bool {
176        self.receiver.is_some()
177    }
178}