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#[derive(Event)]
18pub struct DialogDirectoryPicked<T: PickDirectoryPath> {
19 pub path: PathBuf,
21
22 marker: PhantomData<T>,
23}
24
25#[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
35pub trait PickDirectoryPath: Send + Sync + 'static {}
37
38impl<T> PickDirectoryPath for T where T: Send + Sync + 'static {}
39
40#[derive(Event)]
42pub struct DialogFilePicked<T: PickFilePath> {
43 pub path: PathBuf,
45
46 marker: PhantomData<T>,
47}
48
49#[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
59pub trait PickFilePath: Send + Sync + 'static {}
61
62impl<T> PickFilePath for T where T: Send + Sync + 'static {}
63
64impl FileDialogPlugin {
65 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 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 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 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 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 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}