bevy
bevy copied to clipboard
Feature request: File dialog handling
What problem does this solve or what need does it fill?
Native open/save file dialogs are surprisingly hard to do in Rust, and Bevy makes them even harder. The only crate I'm aware is able to show file dialogs across all main platforms is rfd
. However, due to some bug in winit
, a dependency of Bevy, only asynchronous file dialogs seem to work (see the discussion in this issue). Getting asynchronous code to work with Bevy would probably require either changing some Bevy internals, or fixing code further down the dependency chain.
In other words, file dialogs are currently very hard in Bevy, and having them already implemented would be very convenient.
What solution would you like?
If possible, I'd like that there were something like a resource that somehow handled opening a file dialog across platforms and returning its result. It could be gated behind a feature if this doesn't work across all platforms, or requires many extra dependencies. If this turns out to be infeasible, an optional plugin for a window simulating a file dialog would still be very useful.
What alternative(s) have you considered?
I considered using other dependencies, but this doesn't seem to work due to the reasons outlined above.
A good solution to native file browsing dialogs is necessary. However, to solve your immediate problem, have you tried polling the future manually in a system? It does not look good but it may solve your problem.
There is also tinyfiledialogs which is not pure Rust (wraps a C library) but I found it to work reliable. It also shouldn't be too hard to port the underlying C code to Rust.
I can't figure out a way to poll a future from a synchronous thread, so I haven't figured out how to get the async code to work.
I should mention: the mystery winit
bug I mentioned seems to have something to do with bevy_audio
. After disabling it, I was able to get synchronous file dialogs working on both Windows and Linux. However, MacOS has the limitation that all GUI operations must be run on the main thread, which implies the same for file dialogs. So our current options seem to be either to figure out what makes async
(purportedly) not break, or to figure out how to get file dialogs to not clash with audio.
If you can reproduce the bug with rodio alone, consider reporting it upstream at rodio's issue tracker. Otherwise, there is something to investigate in bevy_audio.
I made an example of polling a future from a system in case you want to go down that route. It's a bit scary-looking but not very complicated. You will ned the futures
crate as a dependency.
use bevy::prelude::*;
use futures::pin_mut;
use futures_timer::Delay;
use std::ops::DerefMut;
use std::future::Future;
use std::time::Duration;
pub fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.add_system(poller.system())
.run();
}
// this defines the future we will want to poll
// it's a simple example, replace it with an actually
// useful future in your context
type FutureType = Delay;
fn setup(mut commands: Commands) {
let future = Delay::new(Duration::from_millis(500));
commands.spawn().insert(future);
}
fn poller(mut commands: Commands, mut query: Query<(Entity, &mut FutureType)>) {
for (entity, mut future) in query.iter_mut() {
println!("Frame!");
let context = &mut futures::task::Context::from_waker(futures::task::noop_waker_ref());
let future = future.deref_mut();
pin_mut!(future);
// do the polling
if let std::task::Poll::Ready(result) = future.poll(context) {
// Do something with the result of your future
println!("Timer elapsed.");
commands.entity(entity).despawn();
}
}
}
It's not a very efficient polling strategy as it will poll on every frame no matter what. But I hope that in your context it's not that bad.
I'd expect something to turn a future into an event or something to be a part of bevy however. That sounds like a reasonable feature request.
Hi! RFD dev here. I get an impression that there is a little bit of misunderstanding here.
The only winit bug I know of related to file dialogs is: https://github.com/rust-windowing/winit/issues/1779 It cheapens only on MacOs and only with sync dialogs.
The bevy_audio
thing is totally unrelated to winit, my guess is that it is because cpal
(dependency of rodio
) initializes COM (in multi threaded mode).
It caused some issues before already, for example it is a cause of existence of this fn in winit with_drag_and_drop(bool)
rfd
initializes COM too so it probably causes some conflicts. It is typical CoInitializeEx
error.
I will investigate it further when I have some free time.
If you decide that bevy needs its own file dialog solution let me know, I can help with that. Maybe as an author or rfd I'm a little bit biased, but I believe that duplicating efforts does not help anyone, I would be in favor of using rfd as a dependency of bevy implementation. I'm open to any changes that would benefit such a integration
@PolyMeilex thanks for the expert opinion!
It sounds like we should probably focus on fixing this issue in winit as thats our chosen "windowing abstraction" and it already has the feature and its just broken on a specific platform.
I see this as a good chance for Bevy contributors to start familiarizing themselves with winit internals. We should have some expertise in that area / know how to drive our requirements upstream.
Long term, we should probably have a way to display both native dialogues and custom so that they can be themed to the game
Custom dialogues are impossible when running inside a sandbox like flatpak or snap unless you grant it blanket permission for all the user files. You need to use the file chooser xdg desktop portal for flatpak or snap to be able to access any file outside the sandbox.
Is there a working example of using rfd
with bevy
?
I tried the following and it blocks on the call to poll
until I close the dialog. What am I doing wrong?
use bevy::prelude::*;
use futures::pin_mut;
use rfd::AsyncFileDialog;
use std::future::Future;
fn main() {
App::build().add_startup_system(dialog.system()).run();
}
fn dialog() {
let future = AsyncFileDialog::new().pick_file();
let context = &mut futures::task::Context::from_waker(futures::task::noop_waker_ref());
pin_mut!(future);
println!("Start Polling");
let poll_result = future.poll(context);
println!("Polling Result: {:?}", poll_result);
}
I figured out how to use FileDialog
asynchronously using the new async_compute example. I didn't see it before because I was looking at the 0.5.0
tag, which doesn't have it.
use futures_lite::future;
use std::path::PathBuf;
use bevy::{
prelude::*,
tasks::{AsyncComputeTaskPool, Task},
};
use rfd::FileDialog;
fn main() {
App::default()
.add_plugins(DefaultPlugins)
.add_startup_system(dialog.system())
.add_system(poll.system())
.run();
}
fn dialog(mut commands: Commands, thread_pool: Res<AsyncComputeTaskPool>) {
println!("Start Polling");
let task = thread_pool.spawn(async move { FileDialog::new().pick_file() });
commands.spawn().insert(task);
}
fn poll(mut commands: Commands, mut tasks: Query<(Entity, &mut Task<Option<PathBuf>>)>) {
println!("Polling");
for (entity, mut task) in tasks.iter_mut() {
if let Some(result) = future::block_on(future::poll_once(&mut *task)) {
println!("{:?}", result);
commands.entity(entity).remove::<Task<Option<PathBuf>>>();
}
}
}
I tried replacing async move { FileDialog::new().pick_file() }
with AsyncFileDialog::new().pick_file()
and the future never returns as ready, even after I pick a file.
I was also searching for a file dialog solution. The example above that @samcarey provided was the shortest path to success I could find.
Some updates were needed to work with bevy 8.1; shared below, in case others are bumping into this. I'm no Bevy/Rust expert, so if there's a better way to do this, please, let me know.
use futures_lite::future;
use std::path::PathBuf;
use bevy::{
prelude::*,
tasks::{AsyncComputeTaskPool, Task},
};
use rfd::FileDialog;
fn main() {
App::default()
.add_plugins(DefaultPlugins)
.add_startup_system(dialog)
.add_system(poll)
.run();
}
#[derive(Component)]
struct SelectedFile(Task<Option<PathBuf>>);
fn dialog(mut commands: Commands) {
let thread_pool = AsyncComputeTaskPool::get();
println!("Start Polling");
let task = thread_pool.spawn(async move { FileDialog::new().pick_file() });
commands.spawn().insert(SelectedFile(task));
}
fn poll(mut commands: Commands, mut tasks: Query<(Entity, &mut SelectedFile)>) {
println!("Polling");
for (entity, mut selected_file) in tasks.iter_mut() {
if let Some(result) = future::block_on(future::poll_once(&mut selected_file.0)) {
println!("{:?}", result);
commands.entity(entity).remove::<SelectedFile>();
}
}
}
But I wonder how Blender did it with a beautiful UI and cross platform File dialog? Can we learn and make it like Blender?
At least on macOS, blender file UI is a pain to use and doesn't use anything from the system ui/ux.
It's painful enough that I'm using Python scripts for my most common import scenarios rather than use it...
And for flatpak you have to use the org.freedesktop.portal.FileChooser dbus interface to use the native file dialog if your app doesn't have blanket permission for every file the user may want to open.
I've created bevy_file_dialog
plugin for file dialogs using rfd
and Bevy's AsyncComputeTaskPool
.
I've only tested it on Linux, but it uses cross-platform apis from rfd
including rfd
's wrappers for wasm, so it should work everywhere.
I was also having issues with rfd::FileDialog
, but it's possible to make it work by making sure the system where it's called runs on the main thread.
We can achieve this by passing any NonSend resource to the system (source).
Example:
fn my_system_which_asks_a_file_at_every_update(
mut _windows: NonSend<WinitWindows>
) {
// ...
let files = FileDialog::new().pick_file();
// ...
}
Quite hacky, but it seems to work.