bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Feature request: File dialog handling

Open vihdzp opened this issue 3 years ago • 14 comments

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.

vihdzp avatar Apr 27 '21 21:04 vihdzp

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.

Moxinilian avatar Apr 27 '21 22:04 Moxinilian

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.

FrankenApps avatar Apr 28 '21 05:04 FrankenApps

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.

vihdzp avatar Apr 28 '21 05:04 vihdzp

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.

Moxinilian avatar Apr 28 '21 10:04 Moxinilian

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 avatar Apr 28 '21 12:04 PolyMeilex

@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.

cart avatar Apr 28 '21 18:04 cart

Long term, we should probably have a way to display both native dialogues and custom so that they can be themed to the game

mockersf avatar Apr 28 '21 19:04 mockersf

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.

bjorn3 avatar Apr 28 '21 19:04 bjorn3

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);
}

samcarey avatar Jul 24 '21 04:07 samcarey

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.

samcarey avatar Jul 27 '21 22:07 samcarey

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>();
        }
    }
}

RobotSnowOtter avatar Sep 05 '22 17:09 RobotSnowOtter

But I wonder how Blender did it with a beautiful UI and cross platform File dialog? Can we learn and make it like Blender?

itfanr avatar Nov 14 '22 09:11 itfanr

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...

mockersf avatar Nov 14 '22 10:11 mockersf

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.

bjorn3 avatar Nov 14 '22 14:11 bjorn3

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.

richardhozak avatar Nov 24 '23 22:11 richardhozak

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.

antoineMoPa avatar Nov 27 '23 03:11 antoineMoPa