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

Spawn future in the slint event loop (Was: Async callbacks in Rust) #747

Closed
secana opened this issue Dec 16, 2021 · 7 comments · Fixed by #2845
Closed

Spawn future in the slint event loop (Was: Async callbacks in Rust) #747

secana opened this issue Dec 16, 2021 · 7 comments · Fixed by #2845
Labels
enhancement New feature or request

Comments

@secana
Copy link

secana commented Dec 16, 2021

First: Thank you for creating sixtyfps, it's the best GUI framework I've worked with for Rust!

I started my first project with sixtyfps in Rust and hit a problem. I need to use async code in the on_... callback functions.

The callback expects an synchronous lambda, which I can workaround by starting a a thread with tokio::spawn, but I'm still not able to work with the model data from the UI, as the model data is Rc<VecModel<...>> and Rc cannot be send between threads. I'm sure there is a way around that with some wrappers, I guess there is an easier option.

What is the recommended way of using async code with sixtyfps?

@ogoffart
Copy link
Member

Hi,

It really depends on what the callback function are going to be doing.

Starting a thread with tokio::spawn should work.
Another way to do it is to send a message to a worker thread: that's the option we used in Cargo-UI
https://github.com/sixtyfpsui/cargo-ui/blob/master/src/main.rs#L37

Regarding the fact that the model can't be Send, this is because all the UI state needs to be consistent in the rendering thread, so you must do all your updates from that thread. But the easier way to do it is to call sixtyfps::invoke_from_event_loop and do that update in the closure. Now the question is how to capture the model since you need to send the model handle first to the ui thread, and then back with invoke_from_event_loop. One way to do that is having a global thread_local! that stores the model.
The other way is to get the ModelHandle from one property in your UI, and then get use as_any() to cast it to your model. An example of this is there: https://github.com/sixtyfpsui/cargo-ui/blob/b8619e01cd77ea85905df832803f576a82fae1e4/src/cargo.rs#L402-L405

We are also looking for ways to make this API nicer. We considered adding some data to the sixtyfps::Weak so that more things could be then send to another thread and then accessed from the eventloop again. But we are still thinking about what our options are.

Yet another way to spawn an async function would be not to use thread at all, and run the future on the UI event loop.
There is currently no public API for that, but maybe we should provide one given that we already have two implementation using our private API:
https://github.com/sixtyfpsui/sixtyfps/blob/4c7ecc57d8763c7b8a6c117014480b2a1ac20188/tools/lsp/preview.rs#L67
https://github.com/sixtyfpsui/sixtyfps/blob/82a1cc1e8308db5652b22b9a60ca70efa52b2a70/tools/viewer/main.rs#L362

It should be possible to implement something similar with the public API only. Something like (untested)

use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::Wake;

fn run_async(fut: Pin<Box<dyn Future<Output = ()>>>) {
    struct FutureRunner {
        fut: Mutex<Option<Pin<Box<dyn Future<Output = ()>>>>>,
    }
    /// Safety: the future is only going to be run in the UI thread
    unsafe impl Send for FutureRunner {}
    unsafe impl Sync for FutureRunner {}

    impl Wake for FutureRunner {
        fn wake(self: Arc<Self>) {
            sixtyfps::invoke_from_event_loop(move || {
                let waker = self.clone().into();
                let mut cx = std::task::Context::from_waker(&waker);
                let mut fut_opt = self.fut.lock().unwrap();
                if let Some(fut) = &mut *fut_opt {
                    match fut.as_mut().poll(&mut cx) {
                        std::task::Poll::Ready(_) => *fut_opt = None,
                        std::task::Poll::Pending => {}
                    }
                }
            })
        }
    }
    Arc::new(FutureRunner { fut: Mutex::new(Some(fut)) }).wake()
}

@secana
Copy link
Author

secana commented Dec 16, 2021

Hi @ogoffart thanks for your fast and extensive answer! I got it working with a combination of let handle = ui.as_weak() and handle.upgrade_in_event_loop(...).

@secana secana closed this as completed Dec 16, 2021
@Be-ing
Copy link
Contributor

Be-ing commented Dec 27, 2021

@secana can you reopen this issue to keep discussion going of how to improve the API?

@Be-ing
Copy link
Contributor

Be-ing commented Dec 27, 2021

run the future on the UI event loop ... It should be possible to implement something similar with the public API only.

I think this would be the nicest to use option. It would be great to not have to write boilerplate for message passing or data synchronization to integrate async Rust with the SixtyFPS GUI.

I am a bit unclear how the API @ogoffart proposed above with run_async would be used. Would it be something like:

main_window.on_some_callback(run_async(async {
   let data = some_expensive_operation().await();
   main_window.set_some_property(data);
}));

If Rust manages to implement async overloading in the future it would be great if that could be used for the autogenerated on_callback functions that the SixtyFPS compiler generates.

@secana secana reopened this Dec 27, 2021
@Be-ing
Copy link
Contributor

Be-ing commented Dec 28, 2021

FWIW, iced can run std::future::Futures.

@DaMilyutin
Copy link

@secana, thank you for the question. Did you manage to succeed?
Can you share your code as an example, please?
More better, will be minimal viable example. But I can not insist.

@secana
Copy link
Author

secana commented Jan 3, 2022

@DaMilyutin I got it to work with the following code (simplified)

use sixtyfps::Model;
sixtyfps::include_modules!();

#[tokio::main]
async fn main() {
    // ... code ...

    let model = Rc::new(sixtyfps::VecModel::<Item>::from(items.clone()));
    let ui = Ui::new();
    ui.set_model(sixtyfps::ModelHandle::new(model));

    let handle_weak = ui.as_weak();
    ui.on_scan({
        move || {
            // Spawn thread to be able to call async functions.
            tokio::spawn(async move {

                    // Call async function
                    let item = my_async_func().await;

                    // Update UI model state
                    update_model(
                        handle_weak.clone(),
                        item,
                    );
                }
            });
        }
    });

    ui.run();
}

fn update_model(handle: sixtyfps::Weak<Ui>, fi: Item) {
    handle.upgrade_in_event_loop(move |handle| {
        let fm = handle.get_model();
        fm.set_row_data((fi.id - 1) as usize, fi)
    });
}

@ogoffart ogoffart added the enhancement New feature or request label Nov 29, 2022
@ogoffart ogoffart changed the title Question: Async callbacks in Rust Spawn future in the slint event loop (Was: Async callbacks in Rust) Nov 29, 2022
@Gibbz Gibbz mentioned this issue Apr 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants