Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File dialogs and drag-and-drop of files #270

Closed
2 of 3 tasks
emilk opened this issue Apr 2, 2021 · 56 comments
Closed
2 of 3 tasks

File dialogs and drag-and-drop of files #270

emilk opened this issue Apr 2, 2021 · 56 comments
Labels
eframe Relates to epi and eframe egui-winit porblems related to winit feature New feature or request

Comments

@emilk
Copy link
Owner

emilk commented Apr 2, 2021

Tracking issue for

@emilk emilk added the feature New feature or request label Apr 2, 2021
@DrOptix
Copy link
Contributor

DrOptix commented Apr 2, 2021

I would like to contribute on this one. Going with something based on std::fs it would be consistent maybe except for egui_web.

On the other hand if we go with platform specific dialogs then that would show a familiar open dialog to the user.

  • On Windows we can use Win32 API GetOpenFileNameW and GetSaveFileNameW.
  • For Linux, here I think there is no standard file open dialog. I need to study. Currently I'm using KDE, but I have a mix of file dialogs from KDE and Gnome, I guess it depends on with which toolkit the app was built.
  • On MacOS I have no idea, but I expect the situation to be similar as on Windows.

Maybe we should just go with this native-dialog crate?

@emilk emilk added the eframe Relates to epi and eframe label Apr 3, 2021
@emilk
Copy link
Owner Author

emilk commented Apr 3, 2021

If opening a file dialog on web is easy then maybe it can just be added to native-dialog instead (i.e. add a web-sys backend to native-dialog). The less code in this repository the better!

@vihdzp
Copy link
Contributor

vihdzp commented Apr 26, 2021

I should mention: I tried using native-dialog on Windows in a Bevy+egui application. About half of times, the dialogs failed with ImplementationError("CoInitializeEx"). I haven't yet filed an issue over there, as I need more testing to figure out what's going on, but it might be something worth being wary of.

@OvermindDL1
Copy link

OvermindDL1 commented Apr 27, 2021

About half of times, the dialogs failed with ImplementationError("CoInitializeEx").

For note, this is caused by the windows COM, it has to be initialized in either, essentially, single-threaded or multi-threaded mode, however some COM objects only work properly in one mode or the other (usually sound annoyingly, needs multithreaded), and dragging and dropping with 'extended' clipboard attributes is one such annoyance (needs single-threaded).

To see details about the issue and how winit deals with it then see: rust-windowing/winit#1255

But in short, if you don't need extended clipboard attributes (OLE Clipboard rather than standard clipboard) then drop the OLE stuff and it would work regardless, otherwise you have to discard drag/drop when CoInitializeEx is in the wrong mode.

@CjS77
Copy link

CjS77 commented May 14, 2021

If you want to use native windows, perhaps using this crate is the way to go. Then you don't need to add anything here, or perhaps at most, a very thin wrapper.

@kellpossible
Copy link

Using native-dialog in a new thread upon button click, disabling the ui while waiting for result, using crossbeam-channel to send the result back to the ui seems to be working pretty well for me at least on linux. Still figuring out how to make it more re-usable, perhaps something to share.

@kellpossible
Copy link

Here was my first stab at it in a crate: https://crates.io/crates/im-native-dialog

@sourcebox
Copy link

sourcebox commented Jun 13, 2021

I'd like to use the rfd crate to open a file dialog. This crate offers a pure Rust solution and an async mode, which is required to work in macOS. It is shown in an example here:

Example async code

Im my application, the dialog should open after pressing a button.

Where I'm struggling is how to integrate this into egui. I guess there has to be some executor and other stuff. My knowledge of Rust and async is still not sufficient to get this running, so any help would appreciated.

@vihdzp
Copy link
Contributor

vihdzp commented Jun 13, 2021

I'd like to use the rfd crate to open a file dialog. This crate offers a pure Rust solution and an async mode, which is required to work in macOS. It is shown in an example here:

I couldn't get async to work with my egui integration (bevy). Instead, what I did was make a synchronous flie dialog that was guaranteed to run on the main thread. I did this by putting its code in a system taking in a !Send resource. You might want to try that out instead.

@sourcebox
Copy link

Some good news: I was able to get rfd working with async at least in macOS and Linux. Can't do any Windows testing at the moment though. I used the demo code with minimal changes. The trickiest part was to get the selected file back to the application because self can't be accessed inside the async context. So I used an mpsc channel to get this working.

@imaitland
Copy link

@sourcebox could you share what you did?

@sourcebox
Copy link

Sure. My project is not published yet, so I tried to extract the relevant parts. By doing this, it's possible that I introduced some errors or forgot something.

pub enum Message {
    FileOpen(std::path::PathBuf),
    // Other messages
}

pub struct App {
    message_channel: (
        std::sync::mpsc::Sender<Message>,
        std::sync::mpsc::Receiver<Message>,
    )
}

impl Default for App {
    fn default() -> Self {
        Self {
            message_channel: std::sync::mpsc::channel(),
        }
    }
}

impl epi::App for App {
    fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
        // This is important, otherwise file dialog can hang
        // and messages are not processed
        ctx.request_repaint();

        loop {
            match self.message_channel.1.try_recv() {
                Ok(message) => {
                    // Process FileOpen and other messages
                }
                Err(_) => {
                    break;
                }
            }
        }

        egui::CentralPanel::default().show(ctx, |ui| {
            let open_button = ui.add(egui::widgets::Button::new("Open...");

            if open_button.clicked() {
                let task = rfd::AsyncFileDialog::new()
                    .add_filter("Text files", &["txt"])
                    .set_directory("/")
                    .pick_file();

                let message_sender = self.message_channel.0.clone();

                execute(async move {
                    let file = task.await;

                    if let Some(file) = file {
                        let file_path = std::path::PathBuf::from(file.path());
                        message_sender.send(Message::FileOpen(file_path)).ok();
                    }
                });
            }
        });
    }
}

fn execute<F: std::future::Future<Output = ()> + Send + 'static>(f: F) {
    std::thread::spawn(move || {
        futures::executor::block_on(f);
    });
}

@imaitland
Copy link

@sourcebox I had a chance to use your code today, some minor tweaks but overall snippet worked great!

use eframe::{egui, epi};
use rfd;

pub enum Message {
    FileOpen(std::path::PathBuf),
    // Other messages
}

pub struct FileApp {
    message_channel: (
        std::sync::mpsc::Sender<Message>,
        std::sync::mpsc::Receiver<Message>,
    )
}

impl Default for FileApp {
    fn default() -> Self {
        Self {
            message_channel: std::sync::mpsc::channel(),
        }
    }
}

impl epi::App for FileApp {
    fn name(&self) -> &str {
        "file dialog app"
    }
    fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
        // This is important, otherwise file dialog can hang
        // and messages are not processed
        ctx.request_repaint();

        loop {
            match self.message_channel.1.try_recv() {
                Ok(_message) => {
                    // Process FileOpen and other messages
                }
                Err(_) => {
                    break;
                }
            }
        }

        egui::CentralPanel::default().show(ctx, |ui| {
            let open_button = ui.add(egui::widgets::Button::new("Open..."));

            if open_button.clicked() {
                let task = rfd::AsyncFileDialog::new()
                    .add_filter("Text files", &["txt"])
                    .set_directory("/")
                    .pick_file();

                let message_sender = self.message_channel.0.clone();

                execute(async move {
                    let file = task.await;

                    if let Some(file) = file {
                        //let file_path = file;
                        let _tentative_file = file;
                        let file_path = std::path::PathBuf::from("idk");
                        message_sender.send(Message::FileOpen(file_path)).ok();
                    }
                });

            }
        });
    }
}

use std::future::Future;

#[cfg(not(target_arch = "wasm32"))]
fn execute<F: Future<Output = ()> + Send + 'static>(f: F) {
    // this is stupid... use any executor of your choice instead
    std::thread::spawn(move || futures::executor::block_on(f));
}
#[cfg(target_arch = "wasm32")]
fn execute<F: Future<Output = ()> + 'static>(f: F) {
    wasm_bindgen_futures::spawn_local(f);
}

@sourcebox
Copy link

Fine. There's one thing I want to point out for the discussion, the ctx.request_repaint() call inside update(). This forces the update on every frame, something that not everyone likes. Without this call, e.g. on macos, after selecting a file, the dialog is closed but the ui is not updated until you move the mouse or do something else that requests an update.

IMO, it would be nice, to have some function that is called periodically even when no ui updates are necessary. This is where the message processing and other things could be done without any cost of repainting.

@sagebind
Copy link
Contributor

Wouldn't cloning ctx and then calling request_repaint from the background thread work? Then you wouldn't have to request repaint all the time.

@emilk
Copy link
Owner Author

emilk commented Aug 19, 2021

For load/save on native we should probably use https://github.com/EmbarkStudios/nfd2 since nfd-rs is unmaintained, or even better: contribute a web-sys backend to nfd2

@sourcebox
Copy link

@emilk Please also take rfd into account because it's a pure Rust solution. This should make cross-compiling much easier than binding to a C library and would be nice for supporting the new ARM Macs.

@emilk
Copy link
Owner Author

emilk commented Aug 19, 2021

@sourcebox Oh wow, I missed rfd - seems perfect, since it also supports wasm!

I just made a PR that fixes hanging native file dialogs on mac, and I have tested that it works with both rfd and nfd2: #631

@emilk
Copy link
Owner Author

emilk commented Aug 20, 2021

Drag-and-drop of files is working, and the rdf crate works for opening file save/open dialogs on native.

Opening a file dialog on web is still unimplemented, but I think we can start here and add that later if someone really wants it.

@emilk emilk closed this as completed Aug 20, 2021
@PolyMeilex
Copy link

PolyMeilex commented Aug 23, 2021

I know that I'm kinda late, but I think that there is a little bit of misunderstanding about main thread of macos dialogs in my RFD crate. RFD uses macos eventloop to automatically redirect dialogs to main thread (as long as eventloop is running for example internaly in winit), if you find that this is not the case you should report it as a bug. Other crates are mostly focusing on CLI use so they can't just expect the EventLoop to be started, but RFD supports it as long as all the requirements are met.

TLDR: as long as you are in winit or sdl app you can spawn dialogs whenever and RFD should do everything for you.

EDIT: @OfficialURL Also the Windows COM issue was lately adresed in cpal so you should no longer come across it (as cpal was one of the only crates that used COM in multithreaded mode)

@emilk
Copy link
Owner Author

emilk commented Aug 23, 2021

@PolyMeilex before #631 got merged rdf and nfd2 would lock up on my mac, I'm guessing due to the native event pump already being locked (rust-windowing/winit#1779)

@PolyMeilex
Copy link

Yep that's a know issue that I was struggling with for quite some time, and thank you for your workaround it's a lot easier that using my fork of winit, I should probably mention it in RFD repo so future users know that it is possible to avoid winit deadlock

@sourcebox
Copy link

Is there an example how the dialogs now should be used? I'm on macOS 10.13 and had a working solution with an async dialog before. After the latest update I got the message about unsupported environment. So I changed the dialog to the sync version. Now the dialog windows just flashes up and closes immediately again returning None as result. On Linux, everything works without a problem.

@PolyMeilex
Copy link

Oh, that's an unexpected side effect of this change, so when using winit's run_return rfd is started outside of winit event loop, this works around winit bug and thanks to this sync dialogs no longer freeze, but when winit is returning it stops NSApplication.

But rfd checks if NSApp is running whenever it is starts async dialog to make sure that it is running in supported env:
https://github.com/PolyMeilex/rfd/blob/c2dbd0fc7e5c8fad4cad8f4f502016432a9804c9/src/backend/macos/modal_future.rs#L69

Maybe I could add a flag to force RFD to try to start a dialog even in this case, but I kinda feel like we are stacking workaround on top of workarounds just to make winit happy.

Now the dialog windows just flashes up and closes immediately

Could you check if this is also the case for nfd2?

@PolyMeilex
Copy link

Currently, the file dialogs do not work at all on macOS with latest revision of egui. Is there some temporary workaround that can be used until the issue is addressed correctly in winit?

https://github.com/PolyMeilex/winit is your best bet, I'm using it for for all releases of Neothesia and I never heard any complaints since I switched to it, if you need it for any specific version of winit just backport it, it is a single commit change

@sourcebox
Copy link

sourcebox commented Sep 9, 2021

I had a look into your Neothesia project, winit is a direct dependency there. I thought that you have to force an underlying crate to use your fork. When do a cargo tree in my project, I see that glutin is dependent on it.

@emilk
Copy link
Owner Author

emilk commented Sep 9, 2021

@sourcebox try adding this to your Cargo.toml:

[patch.crates-io]
winit = { git = "https://github.com/PolyMeilex/winit ", branch = "master" }

@sourcebox
Copy link

@emilk Thanks, that will probably do it ;-)

@vihdzp
Copy link
Contributor

vihdzp commented Sep 9, 2021

I backported PolyMeilex's fix to the 0.25 release version: https://github.com/OfficialURL/winit

@sourcebox
Copy link

I had a little bit of time today and tried both forks from vihdzp and PolyMeilex. I still get an immediately disappearing dialog with those. Do I have to switch back to the async version when using these fixes?

@PolyMeilex
Copy link

Oh, it's probably because I and vihdzp are not using run_return function of winit, and egui is.

Once again, I'm sorry that I can't dedicate any time to this.

@sourcebox
Copy link

I just request to reopen this issue, so it does not get lost.

@emilk emilk reopened this Sep 29, 2021
@emilk emilk added the egui-winit porblems related to winit label Sep 29, 2021
@emilk
Copy link
Owner Author

emilk commented Sep 29, 2021

Related: #756

@emilk
Copy link
Owner Author

emilk commented Sep 29, 2021

I have submitted @PolyMeilex winit fix as a PR now: rust-windowing/winit#2027

Once that is merged and egui_glium have updated to latest stable winit, I will consider this issue closed.

@emilk
Copy link
Owner Author

emilk commented Sep 30, 2021

Actually, I'll consider this closed - the feature is there, but there are some bugs and problems left. Let's open separate issues for those as needed.

@emilk emilk closed this as completed Sep 30, 2021
@sourcebox
Copy link

Thanks. macOS file dialogs now work when using PolyMeilex fork.

@PolyMeilex
Copy link

yep, the run_return commit in egui was reverted, so my fork should work as expected now

@archuser555
Copy link

just wanna say that we are in 2022

@kirjavascript
Copy link

kirjavascript commented Mar 14, 2023

I have a project that requires loading a file on web and native

Here's my attempt: https://github.com/kirjavascript/trueLMAO/blob/master/frontend/src/widgets/file.rs

usage:

if ui.button("Open file…").clicked() {
    self.file.open();
}

if let Some(file) = self.file.get() {
    println!("Vec<u8>: {:#?}", file);
}

The advantage over the rfd wasm version is that this version doesn't rely on having the user click a file element and handles the creation and clicking for you, making the dialogs presented to the user match on web and native.

Is this an idiomatic approach to egui file dialogs, and could it be improved? The example usage relies on request_repaint still to "wait" for the file, but there is probably a better approach, and certainly the <input> handling would be cleaner if it was part of egui_web. It also probably doesnt have to be stateful.

(it's also definitely possible to support saving files in this way too but I haven't gotten there yet)

Added a web/native way of saving;

if ui.button("Save file").clicked() {
       self.file.save("test.txt", vec![0, 1, 2, 3, 33]);
}

@JiveyGuy
Copy link

Does anyone know where we can find example code using a file drag and drop?

@rousbound
Copy link

Hi, I'm interested in knowing if we have some improvements on the item:
"File browser dialog for egui_web ("Select a file to upload")"

If it's not the case, I would also like to know if someone could give me a few pointers so I can see if I can implement this.

@woelper
Copy link

woelper commented Jul 11, 2023

It is presently possible to select and upload files using rfd and egui.

@JumpyLionnn
Copy link

is it also possible to drag files from the app outside to other apps?

@anilbey
Copy link

anilbey commented Nov 2, 2023

Hello, does the file picker work on the web now? If yes can someone share an example please? Thanks

@woelper
Copy link

woelper commented Nov 2, 2023

Hey @anilbey I made one for you - does this help?
https://github.com/woelper/egui_pick_file

@anilbey
Copy link

anilbey commented Nov 2, 2023

Wow! many many thanks @woelper! I am trying it

@anilbey
Copy link

anilbey commented Nov 2, 2023

Fantastic thanks a lot @woelper

@bitbrain-za
Copy link

Thank you @woelper! Google brought me here and this is exactly what I was after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
eframe Relates to epi and eframe egui-winit porblems related to winit feature New feature or request
Projects
None yet
Development

No branches or pull requests