geom3trik / tuix

Cross-platform GUI toolkit written in Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unable to send an Event outside of the main thread

OrionNebula opened this issue · comments

Currently, sending an event requires a &mut State, which can't be obtained outside of event handlers. This makes it difficult to write any code that requires external triggering. A minimal example would be a timer app - an external time-keeping thread can't trigger a redraw, so the displayed time can't be updated.

The workaround I'm currently using is to push a WindowEvent::Redraw during the on_draw handler, which will initiate a manual redraw right after the last one is finished. This isn't ideal though - a lot of the renders are redundant.

Recapping discussions on Rust Audio Discord:

  • Need some way to send events to the event queue from outside. This could be an MPSC.
  • However, MPSC as a one-size-fits-all solution is a poor fit for communication from a real-time audio thread
  • Instead, allow a widget to register itself for poll events, which are sent each frame. The user then implements whatever inter-thread communication is needed

The poll events can be done manually for now, but ultimately the ability to submit async tasks seems useful overall:

state.spawn(|handle| async move {
    let timer = ...;
    timer.await;
    handle.insert_event(Event::new(TickEvent::new()).direct(entity));
});

Needs some design work though, and unclear how an async executor can be integrated with the main event loop.

EDIT: Most likely, for now, we want the async executor to run on a separate thread. This way the user can set up an executor of their choice. For example:

// Use a multi-threaded executor. If a single-threaded one is desired (which - it probably is),
// it needs to be started on a separate thread.
// We can also of course provide feature-gated helpers for spinning up some executor.
#[tokio::main]
fn main() {
    let app = Application::new(|state, window| {
        // Invent a better name for this. The purpose of this "handle" is only to be able to
        // send events to the main event loop.
        // Internally clones either an EventLoopProxy or an MPSC producer handle.
        let handle = state.get_async_handle();
        tokio::spawn(async move {
            let mut timer = time::interval(Duration::from_secs(5));
            loop {
                timer.tick().await;
                handle.insert_event(Event::new(TickEvent::new()).propagate(Propagation::All));
            }
        });
    });
    // Run the actual app synchronously. This is why we need a multi-threaded or separate executor.
    app.run();
}

So my thinking for a solution to this would be to add timers. In the future I think it would be great to have some way to send events from another thread or to use async, but for the time being my suggested stopgap solution would be something like entity.add_timer(state, Timer::new(Duration::from_secs(5))). After the interval duration the entity would receive a TimerEvent with the id of the timer and could then call for a redraw or whatever. The tricky part will be figuring out how to get the timers to trigger. Currently a defualt app is set up to wait for events from winit. It's possible we might be able to use the event loop proxy to inject an event during the interval but it might also require the timers to run on another thread. I will investigate and report back.