bevy_file_dialog/
pick.rs

1use std::marker::PhantomData;
2use std::path::PathBuf;
3
4use bevy_app::prelude::*;
5use bevy_ecs::prelude::*;
6use bevy_tasks::prelude::*;
7use bevy_winit::{EventLoopProxy, EventLoopProxyWrapper, WakeUp};
8use crossbeam_channel::bounded;
9use rfd::AsyncFileDialog;
10
11use crate::{
12    handle_dialog_result, DialogResult, FileDialog, FileDialogPlugin, StreamReceiver, StreamSender,
13    WakeUpOnDrop,
14};
15
16/// Event that gets sent when directory path gets selected from file system.
17#[derive(Event)]
18pub struct DialogDirectoryPicked<T: PickDirectoryPath> {
19    /// Path of picked directory.
20    pub path: PathBuf,
21
22    marker: PhantomData<T>,
23}
24
25/// Event that gets sent when user closes pick directory dialog without picking any directory.
26#[derive(Event)]
27pub struct DialogDirectoryPickCanceled<T: PickDirectoryPath>(PhantomData<T>);
28
29impl<T: PickDirectoryPath> Default for DialogDirectoryPickCanceled<T> {
30    fn default() -> Self {
31        Self(Default::default())
32    }
33}
34
35/// Marker trait saying what directory path are we picking.
36pub trait PickDirectoryPath: Send + Sync + 'static {}
37
38impl<T> PickDirectoryPath for T where T: Send + Sync + 'static {}
39
40/// Event that gets sent when file path gets selected from file system.
41#[derive(Event)]
42pub struct DialogFilePicked<T: PickFilePath> {
43    /// Path of picked file.
44    pub path: PathBuf,
45
46    marker: PhantomData<T>,
47}
48
49/// Event that gets sent when user closes pick file dialog without picking any file.
50#[derive(Event)]
51pub struct DialogFilePickCanceled<T: PickFilePath>(PhantomData<T>);
52
53impl<T: PickFilePath> Default for DialogFilePickCanceled<T> {
54    fn default() -> Self {
55        Self(Default::default())
56    }
57}
58
59/// Marker trait saying what file path are we picking.
60pub trait PickFilePath: Send + Sync + 'static {}
61
62impl<T> PickFilePath for T where T: Send + Sync + 'static {}
63
64impl FileDialogPlugin {
65    /// Allow picking directory paths. This allows you to call
66    /// [`FileDialog::pick_directory_path`] and
67    /// [`FileDialog::pick_multiple_directory_paths`] on [`Commands`]. For each
68    /// `with_pick_directory` you will receive [`DialogDirectoryPicked<T>`] in your
69    /// systems when picking completes.
70    ///
71    /// Does not exist in `WASM32`.
72    pub fn with_pick_directory<T: PickDirectoryPath>(mut self) -> Self {
73        self.0.push(Box::new(|app| {
74            let (tx, rx) = bounded::<DialogResult<DialogDirectoryPicked<T>>>(1);
75            app.insert_resource(StreamSender(tx));
76            app.insert_resource(StreamReceiver(rx));
77            app.add_event::<DialogDirectoryPicked<T>>();
78            app.add_event::<DialogDirectoryPickCanceled<T>>();
79            app.add_systems(
80                First,
81                handle_dialog_result::<DialogDirectoryPicked<T>, DialogDirectoryPickCanceled<T>>,
82            );
83        }));
84        self
85    }
86
87    /// Allow picking file paths. This allows you to call
88    /// [`FileDialog::pick_file_path`] and
89    /// [`FileDialog::pick_multiple_file_paths`] on [`Commands`]. For each
90    /// `with_pick_file` you will receive [`DialogFilePicked<T>`] in your
91    /// systems when picking completes.
92    ///
93    /// Does not exist in `WASM32`. If you want cross-platform solution for
94    /// files, you need to use [`FileDialogPlugin::with_load_file`], which
95    /// allows picking and loading in one step which is compatible with wasm.
96    pub fn with_pick_file<T: PickFilePath>(mut self) -> Self {
97        self.0.push(Box::new(|app| {
98            let (tx, rx) = bounded::<DialogResult<DialogFilePicked<T>>>(1);
99            app.insert_resource(StreamSender(tx));
100            app.insert_resource(StreamReceiver(rx));
101            app.add_event::<DialogFilePicked<T>>();
102            app.add_event::<DialogFilePickCanceled<T>>();
103            app.add_systems(
104                First,
105                handle_dialog_result::<DialogFilePicked<T>, DialogFilePickCanceled<T>>,
106            );
107        }));
108        self
109    }
110}
111
112impl FileDialog<'_, '_, '_> {
113    /// Open pick directory dialog and send [`DialogDirectoryPicked<T>`]
114    /// event. You can read this event with Bevy's
115    /// [`EventReader<DialogDirectoryPicked<T>>`].
116    ///
117    /// Does not exist in `wasm32`.
118    pub fn pick_directory_path<T: PickDirectoryPath>(self) {
119        self.commands.queue(|world: &mut World| {
120            let sender = world
121                .get_resource::<StreamSender<DialogResult<DialogDirectoryPicked<T>>>>()
122                .expect("FileDialogPlugin not initialized with 'with_pick_directory::<T>()'")
123                .0
124                .clone();
125
126            let event_loop_proxy = world
127                .get_resource::<EventLoopProxyWrapper<WakeUp>>()
128                .map(|proxy| EventLoopProxy::clone(&**proxy));
129
130            AsyncComputeTaskPool::get()
131                .spawn(async move {
132                    let file = self.dialog.pick_folder().await;
133                    let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
134
135                    let Some(file) = file else {
136                        sender.send(DialogResult::Canceled).unwrap();
137                        return;
138                    };
139
140                    let event = DialogDirectoryPicked {
141                        path: file.path().to_path_buf(),
142                        marker: PhantomData,
143                    };
144
145                    sender.send(DialogResult::Single(event)).unwrap();
146                })
147                .detach();
148        });
149    }
150
151    /// Open pick multiple directories dialog and send
152    /// [`DialogDirectoryPicked<T>`] for each selected directory path. You
153    /// can get each path by reading every event received with with Bevy's
154    /// [`EventReader<DialogDirectoryPicked<T>>`].
155    ///
156    /// Does not exist in `wasm32`.
157    pub fn pick_multiple_directory_paths<T: PickDirectoryPath>(self) {
158        self.commands.queue(|world: &mut World| {
159            let sender = world
160                .get_resource::<StreamSender<DialogResult<DialogDirectoryPicked<T>>>>()
161                .expect("FileDialogPlugin not initialized with 'with_pick_directory::<T>()'")
162                .0
163                .clone();
164
165            let event_loop_proxy = world
166                .get_resource::<EventLoopProxyWrapper<WakeUp>>()
167                .map(|proxy| EventLoopProxy::clone(&**proxy));
168
169            AsyncComputeTaskPool::get()
170                .spawn(async move {
171                    let files = AsyncFileDialog::new().pick_folders().await;
172                    let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
173
174                    let Some(files) = files else {
175                        sender.send(DialogResult::Canceled).unwrap();
176                        return;
177                    };
178
179                    let events = files
180                        .into_iter()
181                        .map(|file| DialogDirectoryPicked {
182                            path: file.path().to_path_buf(),
183                            marker: PhantomData,
184                        })
185                        .collect();
186
187                    sender.send(DialogResult::Batch(events)).unwrap();
188                })
189                .detach();
190        });
191    }
192
193    /// Open pick file dialog and send [`DialogFilePicked<T>`]
194    /// event. You can read this event with Bevy's
195    /// [`EventReader<DialogFilePicked<T>>`].
196    ///
197    /// Does not exist in `wasm32`. If you want cross-platform solution, you
198    /// need to use [`FileDialog::load_file`], which does picking and loading in
199    /// one step which is compatible with wasm.
200    pub fn pick_file_path<T: PickFilePath>(self) {
201        self.commands.queue(|world: &mut World| {
202            let sender = world
203                .get_resource::<StreamSender<DialogResult<DialogFilePicked<T>>>>()
204                .expect("FileDialogPlugin not initialized with 'with_pick_file::<T>()'")
205                .0
206                .clone();
207
208            let event_loop_proxy = world
209                .get_resource::<EventLoopProxyWrapper<WakeUp>>()
210                .map(|proxy| EventLoopProxy::clone(&**proxy));
211
212            AsyncComputeTaskPool::get()
213                .spawn(async move {
214                    let file = self.dialog.pick_file().await;
215                    let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
216
217                    let Some(file) = file else {
218                        sender.send(DialogResult::Canceled).unwrap();
219                        return;
220                    };
221
222                    let event = DialogFilePicked {
223                        path: file.path().to_path_buf(),
224                        marker: PhantomData,
225                    };
226
227                    sender.send(DialogResult::Single(event)).unwrap();
228                })
229                .detach();
230        });
231    }
232
233    /// Open pick multiple files dialog and send
234    /// [`DialogFilePicked<T>`] for each selected file path. You
235    /// can get each path by reading every event received with with Bevy's
236    /// [`EventReader<DialogFilePicked<T>>`].
237    ///
238    /// Does not exist in `wasm32`. If you want cross-platform solution, you
239    /// need to use [`FileDialog::load_multiple_files`], which does picking and
240    /// loading in one step which is compatible with wasm.
241    pub fn pick_multiple_file_paths<T: PickDirectoryPath>(self) {
242        self.commands.queue(|world: &mut World| {
243            let sender = world
244                .get_resource::<StreamSender<DialogResult<DialogFilePicked<T>>>>()
245                .expect("FileDialogPlugin not initialized with 'with_pick_file::<T>()'")
246                .0
247                .clone();
248
249            let event_loop_proxy = world
250                .get_resource::<EventLoopProxyWrapper<WakeUp>>()
251                .map(|proxy| EventLoopProxy::clone(&**proxy));
252
253            AsyncComputeTaskPool::get()
254                .spawn(async move {
255                    let files = AsyncFileDialog::new().pick_files().await;
256                    let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
257
258                    let Some(files) = files else {
259                        sender.send(DialogResult::Canceled).unwrap();
260                        return;
261                    };
262
263                    let events = files
264                        .into_iter()
265                        .map(|file| DialogFilePicked {
266                            path: file.path().to_path_buf(),
267                            marker: PhantomData,
268                        })
269                        .collect();
270
271                    sender.send(DialogResult::Batch(events)).unwrap();
272                })
273                .detach();
274        });
275    }
276}