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.92k stars 563 forks source link

Web JS API #1875

Open ogoffart opened 1 year ago

ogoffart commented 1 year ago

We currently have the wasm interpreter that we use in the documentation and the online editor. We should add API so that we can also set/get properties and callback from JS on the browser

tronical commented 1 year ago

I think acceptance criteria for this should include the following:

tronical commented 1 year ago

The reason why I think we should offer a web component based API is because pyscript demonstrates beautifully how well this can be used to embed languages in the DOM.

Check out https://github.com/pyscript/pyscript/blob/main/docs/tutorials/getting-started.md#trying-before-installing for an example how simple and seamless this can be.

See for the HTMLElement subclass that implements the pyscript tag: https://github.com/pyscript/pyscript/blob/main/pyscriptjs/src/components/pyscript.ts#L10

See how this is registered: https://github.com/pyscript/pyscript/blob/a6280268383d92fe180f32b500a7829aeac7f877/pyscriptjs/src/components/elements.ts#L16

That's using window.customElements: https://developer.mozilla.org/en-US/docs/Web/API/Window/customElements

tronical commented 1 year ago

We discussed different options on how to approach this. We identified three different entry points for how to use Slint in a JavaScript environment:

  1. Using Slint from NodeJS
  2. Using Slint via a <slint> tag
  3. Scripting a Slint design via a "Scripts" tab in the online editor

All three should have the same JavaScript API to modify properties, and set and invoke callbacks.

Major differences between these three:

  1. NodeJS uses neon instead of wasm-bindgen.
  2. NodeJS uses the windowing system, while the other two use the HTML Canvas element for rendering.

JS API

The JS API should offer access to all properties as "native" JavaScript properties via getters/setters. To solve the situation where a property <bool> hide; in Slint might clash with a function hide() we may want to provide, we also provide string based access to properties and callbacks:

function set_property(name: string, value: any) {}
function get_property(name: string): any {}
function set_callback(name: string, cb: (...args: any[]) => any) {
function invoke_callback(name: string, ...args: any[]): any

For the first two entry points, we want to offer an API for dynamically compiling a .slint file and instantiating exported components:

let slint = ...; //
let component = await slint.build_component_from_string("export App := Window { ... }");
let component = await slint.build_component_from_url("https://.../foo.slint");
let instance = component.create();

(Those names taken from the Rust/C+ interpreter API, maybe we should rethink them?)

I'll post comments with ideas for the three different approaches.

tronical commented 1 year ago

NodeJS

The existing API to compile a .slint file and instantiate a component looks like this:

let slint = require("slint-ui");       
require("slint-ui"); 
let ui = require("../ui/main.slint");
let main = new ui.Main();
main.run();

We may want to reconsider this approach. In any case, we want to offer the aforementioned API for loading files dynamically.

tronical commented 1 year ago

<slint> Tag

Let the example code speak for itself :-)

<script src="slint.js"></script>
<slint id="some_id">
    export App := Window { 
        callback launch-rocket();
        property <color> rocket-color;
        ...
    }
</slint>

<script>

    let app = document.querySelector("slint#some_id");
    // when is app ready? We need a promise to wait for
    // maybe custom element API offers a solution
    await app.init();

    app.on_launch_rocket = () => {

    };
    app.rocket_color = "red";
    // No run() needed
    // No show() needed??
});
</script>
export class SlintTag inherits HTMLElement {
    constructor() {
        let source = somehow_unescape(this.innerHTML);
        this.innerHTML = "<canvas>";

        let ui = slint.load_from_source(source);
        let app = new ui.__root();
        app.show(this.querySelector("canvas"));

        // .. add properties and callback to this ..
    }

    function set_property() {}
    function get_propert() {} 
    /// static api here
}
<script src="slint.js"></script>
<script>
    let ui = await slint.load_from_source("MyWindow := ...");
    // let ui = await slint.load_from_url("...");
    let main = new ui.Main();
    main.show(document.getElementById("canvas"))

    //slint.run_event_loop();  <-- no need (we do it internally)

    // Throw exception when already shown:
    try { main.show(document.getElementById("othercanvas")); }
    main.hide(); // <-- now it can be shown again

    main.foo = 42;

    let main = new ui();
    main.show(...);
    main.inner.rocket_color = "red";

</script>
<canvas id="canvas">
tronical commented 1 year ago

Snippets in the Online Editor

We want to have a tab in the online editor that allows scripting the design. There are three different ways of evaluating such a script:

  1. Evaluation through a web worker. This separates the script from the DOM element and the WASM run-time. A proxy object is required that wraps the component instance behind the Web Worker message passing API.
  2. Evaluation together with the DOM element and WASM run-time inside an <iframe>.
  3. Evaluation directly using eval().

The first two approaches require wrapping the source in data urls / blobs.

The iframe approach may be more invasive in the editor as each script change has to replace the iframe. The web worker approach perfectly isolates, but adds the complexity of the asynchronous worker message passing API (unclear how to solve this well).