slint icon indicating copy to clipboard operation
slint copied to clipboard

async run()

Open frehberg opened this issue 2 years ago • 16 comments

instead of blocking call into slint event-loop

HelloWorld::new().unwrap().run().unwrap();

there should be an option to invoke an async run function, so the underlying event loop (embassy or tokio) might have a chance to process other pending operations asynchronously.

frehberg avatar May 30 '23 10:05 frehberg

I think as a generic solution that works with winit and Qt backends, that's going to be very hard.

I'd prefer having public API for the winit backend and then folks could use this together with winit-async for example - if that's somehow feasible.

For Tokio, we used a thread in cargo-ui: The Tokio runtime is run in a thread and a channel is used to exchange messages between Slint and async functions: https://github.com/slint-ui/cargo-ui/blob/master/src/main.rs

For embassy and MCU: I think for the time being that may be possible of you run your own superloop instead of using the run() function Slint generates. Then you control the loop entirely.

tronical avatar May 30 '23 11:05 tronical

I believe winit need to take control of the event loop, so there can't be an underlying event loop like Tokio.

What we should support however is the ability to run any future in the Slint's event loop https://github.com/slint-ui/slint/issues/747

ogoffart avatar May 30 '23 17:05 ogoffart

Hmmm, I don't see how the winit event loop might share CPU time with another async-event loop such as embassy.

Right now I am evaluating best solution to manage a HMI on stm32f4 and handling async events from USB and buttons.

Instead of using kind of RTOS/C++ to handle different preempt tasks, I would prefer using Rust and its async-feature to handle the IO events from USB, hardware-buttons, HMI-touch events etc. in a single event loop.

Here is a comparison https://tweedegolf.nl/en/blog/65/async-rust-vs-rtos-showdown

AFAICS, an embassy "task" starting a slint Window event loop would block and would starve all other embassy tasks.

https://docs.rs/embassy-executor/latest/embassy_executor/attr.task.html

So far I dont see how embassy and slint could be combined on stm32f4 :(

frehberg avatar May 30 '23 20:05 frehberg

I think the way you'd combine Slint into am embassy task is to write the loop like here in the task and use a timer like Timer::after(duration).await; and combine that with a future that waits on a channel for incoming user input events.

I wonder if there's a way to create an example of this that runs on desktop (for testing purposes).

tronical avatar May 31 '23 06:05 tronical

I wonder how much this also relates to https://github.com/slint-ui/slint/issues/2763

ogoffart avatar Jun 05 '23 10:06 ogoffart

Yes, https://github.com/slint-ui/slint/issues/2763 is related (just its use case is C++). My use case is using Rust "embassy" xor "tokio" event-engine based on the Rust async feature.

frehberg avatar Jun 19 '23 10:06 frehberg

It would be usefull to know the exact use-case and what we are trying to solve. If you are using embassy, you are probably using your own slint::Platform, so you are not using winit and you can have the full control of the event loop.

If you never call slint::run_event_loop, you can just control on when you draw by calling the slint API to handle event and draw from something managed by embassy or tokio.

I am not super faimilar with these framework, but i guess the loop from https://docs.rs/slint/latest/slint/docs/mcu/index.html#the-event-loop can be converted to an async loop. Something like that

async fn my_event_loop() {
    //...  initialize the window and stuff
    loop {


       select!{
            event = the_touch_driver.query_event() => { 
                  slint::platform::update_timers_and_animations();
                  window.dispatch_event(event);
             }
             _ = timer_wait(slint::platform::duration_until_next_timer_update()) => {
                 slint::platform::update_timers_and_animations();
             }
       }

       // Draw the scene if something needs to be drawn.
       window.draw_if_needed(|renderer| {
                todo!("render the scene")
       });
}

Or something similar.

We may want to add helpers or examples to use Slint with known runtime.

ogoffart avatar Jun 27 '23 09:06 ogoffart

This is the async loop that runs on the STM32F429-DK for your reference. There is no select in no_std. The lilos asnc rtos was employed here as task scheduler. Is there any possible improvement out there. Thank you.

https://github.com/ierturk/rust-on-stm32/blob/b8af1ad27e8adf37a9666fd8c9d315ac08ff4c32/src/drivers/bsp.rs#L496

ierturk avatar Dec 01 '23 11:12 ierturk

Perhaps you can use futures::select! ?

tronical avatar Dec 01 '23 12:12 tronical

I'll chek it.

ierturk avatar Dec 01 '23 13:12 ierturk

It seems that the select macro works for std but not no_std

https://github.com/rust-lang/futures-rs/blob/ae3297f25d4bc26e000990cc1dfd16505ae9998a/futures-util/src/async_await/select_mod.rs#L318

ierturk avatar Dec 01 '23 13:12 ierturk

Indeed, but select_biased seems enabled for no_std.

tronical avatar Dec 01 '23 13:12 tronical

The proposed solution was implemented by using the macro select_biased!, however in the current implementation the method slint::platform::duration_until_next_timer_update always returns None. I don't know what, but it didn't work.

My solution is based on the lilos sugestion. 5.2. Giving other tasks an opportunity to run if ready https://github.com/cbiffle/lilos/blob/main/doc/intro.adoc#52-giving-other-tasks-an-opportunity-to-run-if-ready

It works by combining following methods lilos::exec::run_tasks_with_preemption https://github.com/ierturk/rust-on-stm32/blob/79e84297026e4c71a8bcc54eb0e3aeee25e97b93/src/main.rs#L42

and lilos::exec::yield_cpu().await https://github.com/ierturk/rust-on-stm32/blob/79e84297026e4c71a8bcc54eb0e3aeee25e97b93/src/drivers/bsp.rs#L567

ierturk avatar Dec 03 '23 17:12 ierturk

Indeed, but select_biased seems enabled for no_std.

There is an open PR here https://github.com/rust-lang/futures-rs/pull/1912

ierturk avatar Dec 04 '23 17:12 ierturk

I'll add my 2 cents here:

I am primarily a C# developer with an interest in Rust. I would consider myself an intermediate Rust programmer, but novice Tokio user. I recently discovered Slint and since I was also looking into gRPC at the same time, I thought I'd try to combine them in a small Slint GUI desktop (Windows) app that makes a gRPC call to a server to fetch some data that is displayed in the UI. Shouldn't be too hard, right?

Alas, my poor brain imploded when I contemplated how to integrate Slint's synchronous Rust API with a Tokio async-based gRPC client like tonic. Needing to implement a message channel between the UI code and the async gRPC code seems a little too yo-dawg for my liking. Is this the only way?

doxxx avatar Feb 13 '24 21:02 doxxx

Well, I got it working without a message channel. It's not terrible..

I'm using the Hello World tutorial from tonic as the basis for the gRPC part.

use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;
use tokio::task::spawn_blocking;

pub mod hello_world {
    tonic::include_proto!("helloworld");
}

slint::include_modules!();

fn main() {
    // Create a multi-threaded Tokio Runtime
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();

    // The Tokio Runtime main task just launches a blocking thread for the UI and waits for it to finish.
    // This could do other things as well, like starting background initialization tasks, etc.
    runtime.block_on(async {
        spawn_blocking(run_ui)
            .await
            .unwrap()
            .unwrap()
    });
}

fn run_ui() -> Result<(), slint::PlatformError> {
    // Initialize UI
    let ui = AppWindow::new()?;

    // Get a UI weakref for use in the callback
    let ui_handle = ui.as_weak();

    // Handle the button click
    ui.on_send_hello(move || {
        // Get the name that the user entered in the text field
        let ui = ui_handle.unwrap();
        let name = ui.get_name().to_string();

        // Another UI weakref for updating it with the response from the gRPC server
        let ui_handle = ui.as_weak();

        // Spawn a Tokio task to talk to the gRPC server
        [runtime](tokio::runtime::Handle::current()).spawn(async move {
            // send the request and await the result
            let result = send_hello(&name).await.unwrap();
            // update the UI with the server response on the UI thread
            ui_handle.upgrade_in_event_loop(|ui| {
                ui.set_hello_response(result.into());
            }).unwrap();
        });
    });

    ui.run()
}

async fn send_hello(name: &str) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;
    let request = tonic::Request::new(HelloRequest { name: name.into() });
    let response = client.say_hello(request).await?;
    let message = response.into_inner().message;
    println!("RESPONSE: {:?}", message);
    Ok(message)
}
import { Button, VerticalBox, HorizontalBox, LineEdit } from "std-widgets.slint";

export component AppWindow inherits Window {
    in-out property <string> name: "";
    in-out property <string> hello-response: "";

    callback send-hello();

    VerticalBox {
        HorizontalBox {
            Text {
                vertical-alignment: TextVerticalAlignment.center;
                text: "Name:";
            }
            le-name := LineEdit {
                text: root.name;
                edited(text) => {
                    root.name = text;
                }
                accepted => {
                    root.send-hello();
                }
            }
        }
        Button {
            text: "Send Hello";
            clicked => {
                root.send-hello();
            }
        }
        Text {
            text: "Server says \"\{root.hello-response}\"";
        }
    }

    init => {
        le-name.focus();
    }
}

EDIT: Simplified to not require passing Tokio runtime handle through UI

doxxx avatar Feb 14 '24 14:02 doxxx