#![warn(missing_docs)]
use std::io;
use std::marker::PhantomData;
use std::path::Path;
use bevy_app::prelude::*;
use bevy_derive::Deref;
use bevy_ecs::prelude::*;
use bevy_tasks::prelude::*;
use bevy_winit::{EventLoopProxy, EventLoopProxyWrapper, WinitUserEvent};
use crossbeam_channel::{bounded, Receiver, Sender};
use rfd::AsyncFileDialog;
#[cfg(not(target_arch = "wasm32"))]
mod pick;
#[cfg(not(target_arch = "wasm32"))]
pub use pick::*;
pub mod prelude {
pub use crate::{
DialogFileLoadCanceled, DialogFileLoaded, DialogFileSaveCanceled, DialogFileSaved,
FileDialogExt, FileDialogPlugin,
};
#[cfg(not(target_arch = "wasm32"))]
pub use crate::{
DialogDirectoryPickCanceled, DialogDirectoryPicked, DialogFilePickCanceled,
DialogFilePicked,
};
}
#[derive(Default)]
pub struct FileDialogPlugin(Vec<RegisterIntent>);
type RegisterIntent = Box<dyn Fn(&mut App) + Send + Sync + 'static>;
pub trait SaveContents: Send + Sync + 'static {}
pub trait LoadContents: Send + Sync + 'static {}
impl<T> SaveContents for T where T: Send + Sync + 'static {}
impl<T> LoadContents for T where T: Send + Sync + 'static {}
impl FileDialogPlugin {
pub fn new() -> Self {
Default::default()
}
pub fn with_save_file<T: SaveContents>(mut self) -> Self {
self.0.push(Box::new(|app| {
let (tx, rx) = bounded::<DialogResult<DialogFileSaved<T>>>(1);
app.insert_resource(StreamSender(tx));
app.insert_resource(StreamReceiver(rx));
app.add_message::<DialogFileSaved<T>>();
app.add_message::<DialogFileSaveCanceled<T>>();
app.add_systems(
First,
handle_dialog_result::<DialogFileSaved<T>, DialogFileSaveCanceled<T>>,
);
}));
self
}
pub fn with_load_file<T: LoadContents>(mut self) -> Self {
self.0.push(Box::new(|app| {
let (tx, rx) = bounded::<DialogResult<DialogFileLoaded<T>>>(1);
app.insert_resource(StreamSender(tx));
app.insert_resource(StreamReceiver(rx));
app.add_message::<DialogFileLoaded<T>>();
app.add_message::<DialogFileLoadCanceled<T>>();
app.add_systems(
First,
handle_dialog_result::<DialogFileLoaded<T>, DialogFileLoadCanceled<T>>,
);
}));
self
}
}
#[derive(Resource, Deref)]
struct StreamReceiver<T>(Receiver<T>);
#[derive(Resource, Deref)]
struct StreamSender<T>(Sender<T>);
enum DialogResult<T> {
Single(T),
Batch(Vec<T>),
Canceled,
}
fn handle_dialog_result<E: Message, C: Message + Default>(
receiver: Res<StreamReceiver<DialogResult<E>>>,
mut ev_done: MessageWriter<E>,
mut ev_canceled: MessageWriter<C>,
) {
for result in receiver.try_iter() {
match result {
DialogResult::Single(event) => {
ev_done.write(event);
}
DialogResult::Batch(events) => {
ev_done.write_batch(events);
}
DialogResult::Canceled => {
ev_canceled.write_default();
}
}
}
}
#[derive(Message)]
pub struct DialogFileSaved<T: SaveContents> {
pub file_name: String,
pub result: io::Result<()>,
#[cfg(not(target_arch = "wasm32"))]
pub path: std::path::PathBuf,
marker: PhantomData<T>,
}
#[derive(Message)]
pub struct DialogFileLoaded<T: LoadContents> {
pub file_name: String,
pub contents: Vec<u8>,
#[cfg(not(target_arch = "wasm32"))]
pub path: std::path::PathBuf,
marker: PhantomData<T>,
}
#[derive(Message)]
pub struct DialogFileLoadCanceled<T: LoadContents>(PhantomData<T>);
impl<T: LoadContents> Default for DialogFileLoadCanceled<T> {
fn default() -> Self {
Self(Default::default())
}
}
#[derive(Message)]
pub struct DialogFileSaveCanceled<T: SaveContents>(PhantomData<T>);
impl<T: SaveContents> Default for DialogFileSaveCanceled<T> {
fn default() -> Self {
Self(Default::default())
}
}
impl Plugin for FileDialogPlugin {
fn build(&self, app: &mut App) {
assert!(
!self.0.is_empty(),
"File dialog not initialized, use at least one FileDialogPlugin::with_*"
);
for action in &self.0 {
action(app);
}
}
}
pub struct FileDialog<'w, 's, 'a> {
commands: &'a mut Commands<'w, 's>,
dialog: AsyncFileDialog,
}
impl FileDialog<'_, '_, '_> {
pub fn add_filter(mut self, name: impl Into<String>, extensions: &[impl ToString]) -> Self {
self.dialog = self.dialog.add_filter(name, extensions);
self
}
pub fn set_directory<P: AsRef<Path>>(mut self, path: P) -> Self {
self.dialog = self.dialog.set_directory(path);
self
}
pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
self.dialog = self.dialog.set_file_name(file_name);
self
}
pub fn set_title(mut self, title: impl Into<String>) -> Self {
self.dialog = self.dialog.set_title(title);
self
}
pub fn save_file<T: SaveContents>(self, contents: Vec<u8>) {
self.commands.queue(|world: &mut World| {
let sender = world
.get_resource::<StreamSender<DialogResult<DialogFileSaved<T>>>>()
.expect("FileDialogPlugin not initialized with 'with_save_file::<T>()'")
.0
.clone();
let event_loop_proxy = world
.get_resource::<EventLoopProxyWrapper>()
.map(|proxy| EventLoopProxy::clone(&**proxy));
AsyncComputeTaskPool::get()
.spawn(async move {
let file = self.dialog.save_file().await;
let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
let Some(file) = file else {
sender.send(DialogResult::Canceled).unwrap();
return;
};
let event = DialogFileSaved {
file_name: file.file_name(),
result: file.write(&contents).await,
#[cfg(not(target_arch = "wasm32"))]
path: file.path().to_path_buf(),
marker: PhantomData,
};
sender.send(DialogResult::Single(event)).unwrap();
})
.detach();
});
}
pub fn load_file<T: LoadContents>(self) {
self.commands.queue(|world: &mut World| {
let sender = world
.get_resource::<StreamSender<DialogResult<DialogFileLoaded<T>>>>()
.expect("FileDialogPlugin not initialized with 'with_load_file::<T>()'")
.0
.clone();
let event_loop_proxy = world
.get_resource::<EventLoopProxyWrapper>()
.map(|proxy| EventLoopProxy::clone(&**proxy));
AsyncComputeTaskPool::get()
.spawn(async move {
let file = self.dialog.pick_file().await;
let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
let Some(file) = file else {
sender.send(DialogResult::Canceled).unwrap();
return;
};
let event = DialogFileLoaded {
file_name: file.file_name(),
contents: file.read().await,
#[cfg(not(target_arch = "wasm32"))]
path: file.path().to_path_buf(),
marker: PhantomData,
};
sender.send(DialogResult::Single(event)).unwrap();
})
.detach();
});
}
pub fn load_multiple_files<T: LoadContents>(self) {
self.commands.queue(|world: &mut World| {
let sender = world
.get_resource::<StreamSender<DialogResult<DialogFileLoaded<T>>>>()
.expect("FileDialogPlugin not initialized with 'with_load_file::<T>()'")
.0
.clone();
let event_loop_proxy = world
.get_resource::<EventLoopProxyWrapper>()
.map(|proxy| EventLoopProxy::clone(&**proxy));
AsyncComputeTaskPool::get()
.spawn(async move {
let files = AsyncFileDialog::new().pick_files().await;
let _wake_up = event_loop_proxy.as_ref().map(WakeUpOnDrop);
let Some(files) = files else {
sender.send(DialogResult::Canceled).unwrap();
return;
};
let mut events = Vec::new();
for file in files {
events.push(DialogFileLoaded {
file_name: file.file_name(),
contents: file.read().await,
#[cfg(not(target_arch = "wasm32"))]
path: file.path().to_path_buf(),
marker: PhantomData,
});
}
sender.send(DialogResult::Batch(events)).unwrap();
})
.detach();
});
}
}
pub trait FileDialogExt<'w, 's> {
#[must_use]
fn dialog<'a>(&'a mut self) -> FileDialog<'w, 's, 'a>;
}
impl<'w, 's> FileDialogExt<'w, 's> for Commands<'w, 's> {
fn dialog<'a>(&'a mut self) -> FileDialog<'w, 's, 'a> {
FileDialog {
commands: self,
dialog: AsyncFileDialog::new(),
}
}
}
struct WakeUpOnDrop<'a>(&'a EventLoopProxy<WinitUserEvent>);
impl Drop for WakeUpOnDrop<'_> {
fn drop(&mut self) {
self.0.send_event(WinitUserEvent::WakeUp).unwrap();
}
}