slint-ui / slint

Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.
https://slint.dev
Other
16.94k stars 568 forks source link

Node callbacks not working #2477

Closed tronical closed 10 months ago

tronical commented 1 year ago

Discussed in https://github.com/slint-ui/slint/discussions/2065

Originally posted by **arturolinares** January 14, 2023 Hi! I am having trouble using the slint-ui with node in a small application. I have a window with a button that when it is clicked makes a request to Google and updates the button text to the response body length. The UI appears to work well, except that the request doesn't seem to finish until the window is closed. Maybe I need to perform the request outside the UI thread... but how can I do that? ![Peek 13-01-2023 19-45](https://user-images.githubusercontent.com/203078/212445021-87a5f512-ef1b-4339-8db2-58083f8030bd.gif) This is the script: ```js const slint = require("slint-ui"); const ui = require("./ui/main.slint"); const fetch = require('node-fetch'); function doFetch() { fetch('https://google.com') .then(r => r.text()) .then(text => { console.log("Update button text"); app.MainButton.text = text.length; }); } let app = new ui.MainWindow({ clicked: () => doFetch() }); app.run(); ``` The slint file: ```slint import { Button } from "std-widgets.slint"; export MainWindow := Window { callback clicked <=> MainButton.clicked; MainButton := Button { text: "Click Me!"; } } ``` Thanks in advance :smile:
tronical commented 1 year ago

One way of solving this on Linux and Windows is to run the winit event loop in separate threads. The winit API allows for that on these platforms.

For macOS the situation is harder, as Cocoa requires running in the main thread. However it appears that other folks have tried solving this problem before - in the more general context of combining the Cocoa event loop with nodejs. https://github.com/TooTallNate/NodObjC/issues/2 is one particularly interesting starting point.

Edit: As Olivier pointed out, even on Linux and Windows just delegating Slint to a thread adds complexity. So that's not necessarily a viable approach either, and instead we may need to figure out a libuv integration there, too.

ogoffart commented 1 year ago

If using a different thread or a different process, we will need lots of blocking call to synchronize everything, because slint's approach to callback is blocking.

Also, in JS, if we have something like

UI.property_foo = "foo";
UI.property_bar = "bar";

We should never have rendering between the two calls.

One way to do it would be with an event queue like this.

// the JS calls get_property
EVENT_QUEUE.append(Event::GetProperty{..});
slint::invoke_in_event_loop(process_events);
let reply = RESPONSE_QUEUE.wait_for_event();

// this function runs on the slint's thread event loop 
fn process_events() {
    if EVENT_QUEUE.is_empty() { return };
    loop {
        // this is blocking
        let event = EVENT_QUEUE.wait_for_event();
        match event {
            // the js thread needs to make sure to send an EndTransaction event after every iteration of its own event loop
            Event::EndTransaction => { break; }
            Event::SetProperty{ .. } => { todo!() }
            Event::InvokeCallBack{ .. } => { todo!() }
            Event::GetProperty{ name, component } => { 
                let component = deserialize_component(component);
                let value = component.get_property(name);
               RESPONSE_QUEUE.append(serialize_value(value)); 
            }
            //...
        }
    }
}

Getting model data and so on would also be done through events

tronical commented 1 year ago

If using a different thread or a different process, we will need lots of blocking call to synchronize everything,

Perhaps there's a misunderstanding. What I'm suggesting is to merely move the blocking waiting for activity on the uv event FD to a thread. Once that's triggered, we still process the libuv events in the main thread, alongside winit - but just for a "tick".

tronical commented 11 months ago

I believe what Electron does is this:

1) the main process merges the chromium browser process event loop with nodejs' event loop according to https://www.electronjs.org/de/blog/electron-internals-node-integration

2) electron inherits the chromium model where chromium creates a renderer process that - in a nutshell - does the rendering only.

The idea was to do the same. I tried once and failed - I couldn't get node to process or queue new tasks. Sadly my records of that are lost. I've still got the code around to try it, so I'll repeat that. Maybe it was related to me using an old node version.

ogoffart commented 11 months ago

When we call the run from js, we can get the libuv loop pointer with get_uv_event_loop . Then maybe we can do something like this (pseudo-code)

napi_run_event_loop() {
   let uv = napi.get_uv_event_loop();
   let backend_fd = unsafe uv_backend_fd(uv);
   std::thread::spawn(|| {
        loop { // todo: when to exit this thread?
            poll(backend_fd);
            slint::invoke_from_event_loop(|| {
                uv_run(uv, UV_RUN_NOWAIT)
             })
        }
   });
   slint::run_event_loop();
}

So poll the backend_fd in a thread. and wake the main thread to call the events from there.

The problem is that it says in the docs that

uv_run() is not reentrant. It must not be called from a callback.

and we are most likely called from a callback.

tronical commented 11 months ago

I crudely tried to do exactly that in https://github.com/slint-ui/slint/commits/simon/napi-event-loop but I can't get it to work :(

tronical commented 11 months ago

For future reference, commit https://github.com/slint-ui/slint/commit/28f5510ee43244db32d592dd4ae6140dd3cc447e tries to do implement this and test it by starting the event loop, then create a http server, issue a request to it and terminate the loop when the data was received. Sadly the "server ready" callback is never invoked. My understanding of nodejs is that this is becaus just running uv_run is not enough, the node micro tasks also need to be dealt with, for which I cannot find any API.