Launch other event-loop without blocking current

Hello

I have a Gtk-rs (v4) application. The application has multiple windows and also uses Tokio to initialize a websocket, Tokio runs on a separate thread.

The most important code of my Gtk:

fn main() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2) // Set the number of Tokio worker threads
        .enable_all()
        .build()
        .unwrap();
            
        let _guard = rt.enter();
        
        gtk();
}

fn gtk() {
     gtk::init().expect("Failed to initialize GTK.");
    let app = gtk::Application::builder().application_id("123").build();

    // Connect to "activate" signal of `app`
    app.connect_startup(|_| load_css());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(app: &gtk::Application) {
    // Show login form if not logged in
    match futures::executor::block_on(session::restaurantuser()) {
        Some(u) => {
            let join_handle = tokio::spawn(initialize_tokio());
            ui::dashboard_mainview(app);
        },
        None => ui::login_view(app),
    };
}

I am also using Wry to show a webview to the user. The Wry-webview is opened in a separate window:

pub fn dashboard_webview() -> wry::Result<()> {

    //...

    let event_loop = EventLoop::new();
    let window = WindowBuilder::new()
        .with_title("Dashboard webview")
        .with_maximized(true)
        .with_window_icon(Some(icon))
        .build(&event_loop).unwrap();
    let _webview = WebViewBuilder::new(window).unwrap()
        .with_url_and_headers(&*format!("https://{}/order-dashboard/panel?desktop_app=true", *config::URL), headers).unwrap()
        .build()
        .unwrap();
    _webview.clear_all_browsing_data().unwrap();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

    match event {
      Event::NewEvents(StartCause::Init) => {
        println!("Wry has started!");
      },
    Event::WindowEvent {
        event: WindowEvent::CloseRequested,
        ..
      } => {
    _webview.clear_all_browsing_data().unwrap();
          *control_flow = ControlFlow::Exit
      },
      _ => (),
    }
  });

}

The window with the Wry-webview is opened from a Gtk-window when a button is clicked.

    item_dashboard.connect_clicked(move | _button| {
            ui::dashboard_webview().unwrap(); 
    });

When the button item_dashboard is clicked the window with Wry opens and works fine. But the problem is that now my Gtk-windows are totally frozen. They do not respond at all. Closing the Wry-window closes the whole app as well, which is not the expected behavior.

I want the user to be able to open the Wry-window while still be able to use the Gtk-window from where it was opened. And closing the Wry-window should not terminate the application.

I think the reason the Gtk-part of my application blocks when Wry is opened, is because Wry uses some kind of event-loop:

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

    match event {
      Event::NewEvents(StartCause::Init) => {
        println!("Wry has started!");
      },
    Event::WindowEvent {
        event: WindowEvent::CloseRequested,
        ..
      } => {
    _webview.clear_all_browsing_data().unwrap();
          *control_flow = ControlFlow::Exit
      },
      _ => (),
    }
  });

I think that stalls the rest of my application. Note: Opening other Gtk-windows from my app does not stall my application.

I tried multiple solutions. I thought that I could launch ui::dashboard_webview() in a thread so it would run separately from my app:

    item_dashboard.connect_clicked(move | _button| {
          std::thread::spawn(|| {
            ui::dashboard_webview().unwrap();
        });
    });

Runtime error:

thread '<unnamed>' panicked at 'Initializing the event loop outside of the main thread is a significant cross-platform compatibility hazard. If you absolutely need to create an EventLoop on a different thread, you can use the `EventLoopBuilderExtWindows::any_thread` function.', C:\Users\vboxuser\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tao-0.22.2\src\platform_impl\windows\event_loop.rs:172:7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

I also tried making ui::dashboard_webview() async and calling it like this:

    item_dashboard.connect_clicked(move | _button| {
        glib::MainContext::default().spawn_local(async move {
            let join_handle = tokio::spawn(ui::dashboard_webview());
        });
    });

It yields the same runtime-error. How would I fix this?

This seems to get closer to what I want, but I don't know if it's ideal:

pub  fn dashboard_webview() -> wry::Result<()> {

    use wry::application::platform::windows::EventLoopBuilderExtWindows;
    use wry::application::event_loop::EventLoopBuilder;

  //...

    // WITH_ANY_THREAD
    let event_loop: EventLoop<()> = EventLoopBuilder::default()
        .with_any_thread(true)
        .build();
    let window = WindowBuilder::new()
        .with_title("Dashboard webview")
        .with_maximized(true)
        .with_window_icon(Some(icon))
        .build(&event_loop).unwrap();
    let _webview = WebViewBuilder::new(window).unwrap()
        .with_url_and_headers(&*format!("https://{}/order-dashboard/panel?desktop_app=true", *config::URL), headers).unwrap()
        .build()
        .unwrap();
    _webview.clear_all_browsing_data().unwrap();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

    match event {
      Event::NewEvents(StartCause::Init) => {
        println!("Wry has started!");
      },
    Event::WindowEvent {
        event: WindowEvent::CloseRequested,
        ..
      } => {
    _webview.clear_all_browsing_data().unwrap();
          *control_flow = ControlFlow::Exit
      },
      _ => (),
    }
  });

}

that looks like a winit event loop, which to my knowledge, can only be used as the master event loop, i.e. not embedded inside another GUI framework's event loop.

I don't know how the wry library works, but can you run the webview in another thread, or even another process? I think that's the easiest solution, much easier than, say, try to combine two different event loops into one.

2 Likes

Isn't that what I am doing in my last reply?
I am calling it like this:

 item_dashboard.connect_clicked(move | _button| {
          std::thread::spawn(|| {
            ui::dashboard_webview().unwrap();
        });
    });

woops I glossed over this piece. because it's gtk, I thought you were on Linux. from the error message, it seems you hit the platform limitation. on Linux, generally you can run your event loop on any thread. on Windows, although it is possible to run event loop on different thread, it is generally advised to only run GUI code on the "main" thread. and winit by default prevent you from doing so.

to run a winit event loop from different thread other than the "main" thread on platforms that support this, you need to explicitly enable it using platform specific extension traits. on windows, it's EventLoopBuilderExtWindows; for Linux, there's similar extension trait for X11, and I believe wayland too, but I have not used wayland personally. be aware, Mac and mobile platforms don't support this feature.

use winit::platform::windows::EventLoopBuilderExtWindows;
let event_loop = EventLoopBuilder::new().with_any_thread(true).build();

There is a fundamental limitation, which is that event loops do not compose. It is in fact a well-known problem with no well-known solution. I say no well-known solution because frameworks still insist on inversion of control; they want to take over the main loop. Don't call us, we'll call you.

The actual solution which should be well-known is to not let a framework take over the event loop in the first place. Your application has the only main loop that needs to exist. You have multiple event sources to consume from, but these sources can be composed as streams much more easily than you can compose multiple event loops. Consume from multiple event streams, produce into multiple event streams (most of which are running on separate threads). Your main loop can call into the UI with no fuss, and everything works as it's supposed to.

But the reality is that platforms like iOS and web browsers implement the main loop, and as an application developer you are forced to give up control and make a mess of control flow with a callback soup. Asynchronous callbacks are just runtime-defined Go To statements. This sets the lowest common denominator that winit is even remotely interested in supporting [1].

This is a bit of a rant, but the TL;DR is that frameworks like winit and GTK are doing it wrong, and there is very little you are going to be able to do to course-correct. The only thing you can do is just not use them [2].


  1. In fact, I pitched the idea that winit should un-invert control of the event loop, and the response was rather cold. The value of the proposal was completely overlooked. So, you are stuck trying to compose event loops. ↩︎

  2. This is only slightly hyperbolic. You can use them together with process isolation and IPC between the two. But it won't work on all platforms (iOS and web browsers, I'm looking at you) and it's a flaky hack to a problem that should not exist, period. Avoiding dependencies that perpetuate the issue is practical advice. ↩︎

2 Likes