Replace neon node port (#3744)

* Update api/node/README.md

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Florian Blasius 2023-10-24 15:07:59 +02:00 committed by GitHub
parent b6b3337430
commit bf77b064ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 370 additions and 2131 deletions

7
api/node/.gitignore vendored
View file

@ -1,3 +1,4 @@
docs
package-lock.json
dist
rust-module.js
rust-module.d.ts
index.js
docs/

13
api/node/.npmignore Normal file
View file

@ -0,0 +1,13 @@
target
Cargo.lock
.cargo
.github
npm
.eslintrc
.prettierignore
rustfmt.toml
yarn.lock
*.node
.yarn
__test__
renovate.json

4
api/node/.yarnrc.yml Normal file
View file

@ -0,0 +1,4 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
nodeLinker: node-modules

View file

@ -3,40 +3,30 @@
[package]
name = "slint-node"
description = "Internal Slint Runtime Library for NodeJS API."
authors.workspace = true
documentation.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
build = "build.rs"
# This is not meant to be used as a library from crate.io
publish = false
categories = ["gui", "development-tools"]
[lib]
path = "lib.rs"
crate-type = ["cdylib"]
name = "slint_node_native"
[dependencies]
napi = { version = "2.12.0", default-features = false, features = ["napi8"] }
napi-derive = "2.12.2"
i-slint-compiler = { workspace = true, features = ["default"] }
i-slint-core = { workspace = true, features = ["default"] }
slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] }
vtable = { version = "0.1.6", path="../../../helper_crates/vtable" }
spin_on = "0.1"
css-color-parser2 = { workspace = true }
generativity = "1"
itertools = { workspace = true }
neon = "0.8.0"
once_cell = "1.5"
rand = "0.8"
scoped-tls-hkt = "0.1"
spin_on = "0.1" #FIXME: remove and delegate to async JS instead
# Enable image-rs' default features to make all image formats available for nodejs builds
image = { version = "0.24.0" }
[build-dependencies]
neon-build = "0.8.0"
napi-build = "2.0.1"

View file

@ -1,19 +1,24 @@
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial -->
# Slint-node (Beta)
[![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui)
[Slint](https://slint.dev/) is a UI toolkit that supports different programming languages.
Slint-node is the integration with node.
Slint-node is the integration with Node.js.
The complete Node documentation can be viewed online at https://slint.dev/docs/node/.
To get started you can use the [Walk-through tutorial](https://slint.dev/docs/tutorial/node).
To get started you use the [walk-through tutorial](https://slint.dev/docs/tutorial/node).
We also have a [Getting Started Template](https://github.com/slint-ui/slint-nodejs-template) repository with
the code of a minimal application using Slint that can be used as a starting point to your program.
**Warning: Beta**
Slint-node is still in the early stages of development: APIs will change and important features are still being developed.
## Slint Language Manual
The [Slint Language Documentation](../slint) covers the Slint UI description language
in detail.
## Installing Slint
Slint is available via NPM, so you can install by running the following command:
@ -26,7 +31,7 @@ npm install slint-ui
You need to install the following components:
* **[Node.js](https://nodejs.org/download/release/v16.19.1/)** (v16. Newer versions currently not supported: [#961](https://github.com/slint-ui/slint/issues/961))
* **[Node.js](https://nodejs.org/download/release/)** (v16. or newer)
* **[npm](https://www.npmjs.com/)**
* **[Rust compiler](https://www.rust-lang.org/tools/install)** (1.70 or newer)
@ -34,41 +39,62 @@ You will also need a few more dependencies, see <https://github.com/slint-ui/sli
## Using Slint
First, import the API from the `slint-ui` module. In the following examples we're using [ECMAScript module syntax](https://nodejs.org/api/esm.html#modules-ecmascript-modules), but if you prefer you can also import the API using [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs-modules) syntax.
To initialize the API, you first need to import the `slint-ui` module in our code:
```js
let slint = require("slint-ui");
import * as slint from "slint-ui";
```
This step also installs a hook in NodeJS that allows you to import `.slint` files directly:
Next, load a slint file with the `loadFile` function:
```js
let ui = require("../ui/main.slint");
let ui = slint.loadFile("ui/main.slint");
```
Combining these two steps leads us to the obligatory "Hello World" example:
```js
require("slint-ui");
let ui = require("../ui/main.slint");
import * as slint from "slint-ui";
let ui = slint.loadFile(".ui/main.slint");
let main = new ui.Main();
main.run();
```
See [/examples/todo/node](/examples/todo/node) for a full example.
For a full example, see [/examples/todo/node](https://github.com/slint-ui/slint/tree/master/examples/todo/node).
## API Overview
### Instantiating a component
### Instantiating a Component
The following example shows how to instantiating a Slint component from JavaScript.
**`ui/main.slint`**
```slint
export component MainWindow inherits Window {
callback clicked <=> i-touch-area.clicked;
in property <int> counter;
width: 400px;
height: 200px;
i-touch-area := TouchArea {}
}
```
The exported component is exposed as a type constructor. The type constructor takes as parameter
an object which allow to initialize the value of public properties or callbacks.
**`main.js`**
```js
require("slint-ui");
import * as slint from "slint-ui";
// In this example, the main.slint file exports a module which
// has a counter property and a clicked callback
let ui = require("ui/main.slint");
let ui = slint.loadFile("ui/main.slint");
let component = new ui.MainWindow({
counter: 42,
clicked: function() { console.log("hello"); }
@ -77,7 +103,7 @@ let component = new ui.MainWindow({
### Accessing a property
Properties are exposed as properties on the component instance
Properties declared as `out` or `in-out` in `.slint` files are visible as JavaScript on the component instance.
```js
component.counter = 42;
@ -86,9 +112,32 @@ console.log(component.counter);
### Callbacks
The callbacks are also exposed as property that have a setHandler function, and that can can be called.
Callback in Slint can be defined usign the `callback` keyword and can be connected to a callback of an other component
usign the `<=>` syntax.
**`ui/my-component.slint`**
```slint
export component MyComponent inherits Window {
callback clicked <=> i-touch-area.clicked;
width: 400px;
height: 200px;
i-touch-area := TouchArea {}
}
```
The callbacks in JavaScript are exposed as property that has a setHandler function, and that can be called as a function.
**`main.js`**
```js
import * as slint from "slint-ui";
let ui = slint.loadFile("ui/my-component.slint");
let component = new ui.MyComponent();
// connect to a callback
component.clicked.setHandler(function() { console.log("hello"); })
// emit a callback
@ -97,20 +146,24 @@ component.clicked();
### Type Mappings
The types used for properties in .slint design markup each translate to specific types in JavaScript. The follow table summarizes the entire mapping:
| `.slint` Type | JavaScript Type | Notes |
| --- | --- | --- |
| `int` | `Number` | |
| `float` | `Number` | |
| `string` | `String` | |
| `color` | `String` | Colors are represented as strings in the form `"#rrggbbaa"`. When setting a color property, any CSS compliant color is accepted as a string. |
| `color` | `Color` | |
| `brush` | `Brush` | |
| `image` | `ImageData` | |
| `length` | `Number` | |
| `physical_length` | `Number` | |
| `duration` | `Number` | The number of milliseconds |
| `angle` | `Number` | The value in degrees |
| structure | `Object` | Structures are mapped to JavaScrip objects with structure fields mapped to properties. |
| array | `Array` or Model Object | |
| `angle` | `Number` | The angle in degrees |
| structure | `Object` | Structures are mapped to JavaScript objects where each structure field is a property. |
| array | `Array` or any implementation of Model | |
### Models
### Arrays and Models
For property of array type, they can either be set using an array.
In that case, getting the property also return an array.
@ -129,33 +182,62 @@ Another option is to set a model object. A model object has the following funct
* `rowData(index)`: return the row at the given index
* `setRowData(index, data)`: called when the model need to be changed. `this.notify.rowDataChanged` must be called if successful.
When such an object is set to a model property, it gets a new `notify` object with the following function
* `rowDataChanged(index)`: notify the view that the row was changed.
* `rowAdded(index, count)`: notify the view that rows were added.
* `rowRemoved(index, count)`: notify the view that a row were removed.
* `reset()`: notify the view that everything may have changed.
As an example, here is the implementation of the `ArrayModel` (which is available as `slint.ArrayModel`)
```js
import * as slint from "slint-ui";
let array = [1, 2, 3];
let model = {
rowCount() { return a.length; },
rowData(row) { return a[row]; },
setRowData(row, data) { a[row] = data; this.notify.rowDataChanged(row); },
push() {
let size = a.length;
Array.prototype.push.apply(a, arguments);
export class ArrayModel<T> extends slint.Model<T> {
private a: Array<T>
constructor(arr: Array<T>) {
super();
this.a = arr;
}
rowCount() {
return this.a.length;
}
rowData(row: number) {
return this.a[row];
}
setRowData(row: number, data: T) {
this.a[row] = data;
this.notify.rowDataChanged(row);
}
push(...values: T[]) {
let size = this.a.length;
Array.prototype.push.apply(this.a, values);
this.notify.rowAdded(size, arguments.length);
},
remove(index, size) {
let r = a.splice(index, size);
this.notify.rowRemoved(size, arguments.length);
},
};
}
remove(index: number, size: number) {
let r = this.a.splice(index, size);
this.notify.rowRemoved(index, size);
}
get length(): number {
return this.a.length;
}
values(): IterableIterator<T> {
return this.a.values();
}
entries(): IterableIterator<[number, T]> {
return this.a.entries()
}
}
let model = new ArrayModel(array);
component.model = model;
model.push(4); // this works
// does NOT work, getting the model does not return the right object
// component.model.push(5);
```
```

View file

@ -0,0 +1,58 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava'
const path = require('node:path');
import { loadFile, CompileError } from '../index'
test('loadFile', (t) => {
let demo = loadFile(path.join(__dirname, "resources/test.slint"));
let test = new demo.Test();
t.is(test.check, "Test");
let errorPath = path.join(__dirname, "resources/error.slint");
const error = t.throws(() => {
loadFile(errorPath)
},
{instanceOf: CompileError}
);
t.is(error?.message, "Could not compile " + errorPath);
t.deepEqual(error?.diagnostics, [
{
column: 18,
level: 0,
lineNumber: 7,
message: 'Missing type. The syntax to declare a property is `property <type> name;`. Only two way bindings can omit the type',
sourceFile: errorPath
},
{
column: 22,
level: 0,
lineNumber: 7,
message: 'Syntax error: expected \';\'',
sourceFile: errorPath
},
{
column: 22,
level: 0,
lineNumber: 7,
message: 'Parse error',
sourceFile: errorPath
},
]);
})
test('constructor parameters', (t) => {
let demo = loadFile(path.join(__dirname, "resources/test-constructor.slint"));
let hello = "";
let test = new demo.Test({ say_hello: function() { hello = "hello"; }, check: "test"});
// test.say_hello.setHandler(function () { blub = "hello"; });
test.say_hello();
t.is(test.check, "test");
t.is(hello, "hello");
})

View file

@ -0,0 +1,239 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava'
import { private_api } from '../index'
test('get/set include paths', (t) => {
let compiler = new private_api.ComponentCompiler;
t.is(compiler.includePaths.length, 0);
compiler.includePaths = ["path/one/", "path/two/", "path/three/"];
t.deepEqual(compiler.includePaths, ["path/one/", "path/two/", "path/three/"]);
})
test('get/set style', (t) => {
let compiler = new private_api.ComponentCompiler;
t.is(compiler.style, null);
compiler.style = "fluent";
t.is(compiler.style, "fluent");
})
test('get/set build from source', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`export component App {}`, "");
t.not(definition, null);
t.is(definition!.name, "App");
})
test('constructor error ComponentDefinition and ComponentInstance', (t) => {
const componentDefinitionError = t.throws(() => { new private_api.ComponentDefinition });
t.is(componentDefinitionError?.message, "ComponentDefinition can only be created by using ComponentCompiler.");
const componentInstanceError = t.throws(() => { new private_api.ComponentInstance });
t.is(componentInstanceError?.message, "ComponentInstance can only be created by using ComponentCompiler.");
})
test('properties ComponentDefinition', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`export struct Struct {}
export component App {
in-out property <bool> bool-property;
in-out property <brush> brush-property;
in-out property <color> color-property;
in-out property <float> float-property;
in-out property <image> image-property;
in-out property <int> int-property;
in-out property <[string]> model-property;
in-out property <string> string-property;
in-out property <Struct> struct-property;
}`, "");
t.not(definition, null);
let properties = definition!.properties;
t.is(properties.length, 9);
properties.sort((a, b) => {
const nameA = a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
})
t.is(properties[0].name, "bool-property");
t.is(properties[0].valueType, private_api.ValueType.Bool);
t.is(properties[1].name, "brush-property");
t.is(properties[1].valueType, private_api.ValueType.Brush);
t.is(properties[2].name, "color-property");
t.is(properties[2].valueType, private_api.ValueType.Brush);
t.is(properties[3].name, "float-property");
t.is(properties[3].valueType, private_api.ValueType.Number);
t.is(properties[4].name, "image-property");
t.is(properties[4].valueType, private_api.ValueType.Image);
t.is(properties[5].name, "int-property");
t.is(properties[5].valueType, private_api.ValueType.Number);
t.is(properties[6].name, "model-property");
t.is(properties[6].valueType, private_api.ValueType.Model);
t.is(properties[7].name, "string-property");
t.is(properties[7].valueType, private_api.ValueType.String);
t.is(properties[8].name, "struct-property");
t.is(properties[8].valueType, private_api.ValueType.Struct);
})
test('callbacks ComponentDefinition', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
callback first-callback();
callback second-callback();
}`, "");
t.not(definition, null);
let callbacks = definition!.callbacks;
t.is(callbacks.length, 2);
callbacks.sort();
t.is(callbacks[0], "first-callback");
t.is(callbacks[1], "second-callback");
})
test('globalProperties ComponentDefinition', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`export struct Struct {}
export global TestGlobal {
in-out property <bool> bool-property;
in-out property <brush> brush-property;
in-out property <color> color-property;
in-out property <float> float-property;
in-out property <image> image-property;
in-out property <int> int-property;
in-out property <[string]> model-property;
in-out property <string> string-property;
in-out property <Struct> struct-property;
}
export component App {
}`, "");
t.not(definition, null);
t.is(definition!.globalProperties("NonExistent"), null);
let properties = definition!.globalProperties("TestGlobal");
t.not(properties, null);
t.is(properties!.length, 9);
properties!.sort((a, b) => {
const nameA = a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
})
t.is(properties![0].name, "bool-property");
t.is(properties![0].valueType, private_api.ValueType.Bool);
t.is(properties![1].name, "brush-property");
t.is(properties![1].valueType, private_api.ValueType.Brush);
t.is(properties![2].name, "color-property");
t.is(properties![2].valueType, private_api.ValueType.Brush);
t.is(properties![3].name, "float-property");
t.is(properties![3].valueType, private_api.ValueType.Number);
t.is(properties![4].name, "image-property");
t.is(properties![4].valueType, private_api.ValueType.Image);
t.is(properties![5].name, "int-property");
t.is(properties![5].valueType, private_api.ValueType.Number);
t.is(properties![6].name, "model-property");
t.is(properties![6].valueType, private_api.ValueType.Model);
t.is(properties![7].name, "string-property");
t.is(properties![7].valueType, private_api.ValueType.String);
t.is(properties![8].name, "struct-property");
t.is(properties![8].valueType, private_api.ValueType.Struct);
})
test('globalCallbacks ComponentDefinition', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export global TestGlobal {
callback first-callback();
callback second-callback();
}
export component App {
}`, "");
t.not(definition, null);
t.is(definition!.globalCallbacks("NonExistent"), null);
let callbacks = definition!.globalCallbacks("TestGlobal");
t.not(callbacks, null);
t.is(callbacks!.length, 2);
callbacks!.sort();
t.is(callbacks![0], "first-callback");
t.is(callbacks![1], "second-callback");
})
test('compiler diagnostics', (t) => {
let compiler = new private_api.ComponentCompiler;
t.is(compiler.buildFromSource(`export component App {
garbage
}`, "testsource.slint"), null);
const diags = compiler.diagnostics;
t.is(diags.length, 1);
t.deepEqual(diags[0], {
level: 0,
message: 'Parse error',
lineNumber: 2,
column: 12,
sourceFile: 'testsource.slint'
});
})
test('non-existent properties and callbacks', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
const prop_err = t.throws(() => {
instance!.setProperty("non-existent", 42);
});
t.is(prop_err!.code, 'GenericFailure');
t.is(prop_err!.message, 'Property non-existent not found in the component');
const callback_err = t.throws(() => {
instance!.setCallback("non-existent-callback", () => { });
});
t.is(callback_err!.code, 'GenericFailure');
t.is(callback_err!.message, 'Callback non-existent-callback not found in the component');
})

View file

@ -0,0 +1,173 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava';
const path = require('node:path');
import { private_api } from '../index'
test('get/set global properties', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export global Global { in-out property <string> name: "Initial"; }
export component App {}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
t.is(instance!.getGlobalProperty("Global", "name"), "Initial");
instance!.setGlobalProperty("Global", "name", "Hello");
t.is(instance!.getGlobalProperty("Global", "name"), "Hello");
t.throws(() => {
instance!.getGlobalProperty("MyGlobal", "name")
},
{
code: "GenericFailure",
message: "Global MyGlobal not found"
}
);
t.throws(() => {
instance!.setGlobalProperty("MyGlobal", "name", "hello")
},
{
code: "GenericFailure",
message: "Global MyGlobal not found"
}
);
t.throws(() => {
instance!.getGlobalProperty("Global", "age")
},
{
code: "GenericFailure",
message: "no such property"
}
);
t.throws(() => {
instance!.setGlobalProperty("Global", "age", 42)
},
{
code: "GenericFailure",
message: "Property age of global Global not found in the component"
}
);
t.throws(() => {
instance!.setGlobalProperty("Global", "name", 42)
},
{
code: "InvalidArg",
message: "expect String, got: Number"
}
);
t.throws(() => {
instance!.setGlobalProperty("Global", "name", { "blah": "foo" })
},
{
code: "InvalidArg",
message: "expect String, got: Object"
}
);
})
test('invoke global callback', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export struct Person {
name: string
}
export global Global {
callback great(string, string, string, string, string);
callback great-person(Person);
callback person() -> Person;
callback get-string() -> string;
person => {
{
name: "florian"
}
}
get-string => {
"string"
}
}
export component App {}
`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
t.throws(() => {
instance!.setGlobalCallback("MyGlobal", "great", () => {})
},
{
code: "GenericFailure",
message: "Global MyGlobal not found"
}
);
t.throws(() => {
instance!.invokeGlobal("MyGlobal", "great", [])
},
{
code: "GenericFailure",
message: "Global MyGlobal not found"
}
);
let speakTest;
instance!.setGlobalCallback("Global", "great", (a: string, b: string, c: string, d: string, e: string) => {
speakTest = "hello " + a + ", " + b + ", " + c + ", " + d + " and " + e;
});
t.throws(() => {
instance!.setGlobalCallback("Global", "bye", () => {})
},
{
code: "GenericFailure",
message: "Callback bye of global Global not found in the component"
}
);
t.throws(() => {
instance!.invokeGlobal("Global", "bye", [])
},
{
code: "GenericFailure",
message: "Callback bye of global Global not found in the component"
}
);
instance!.invokeGlobal("Global", "great", ["simon", "olivier", "auri", "tobias", "florian"]);
t.deepEqual(speakTest, "hello simon, olivier, auri, tobias and florian");
instance!.setGlobalCallback("Global", "great-person", (p: any) => {
speakTest = "hello " + p.name;
});
instance!.invokeGlobal("Global", "great-person", [{ "name": "simon" }]);
t.deepEqual(speakTest, "hello simon");
t.throws(() => {
instance!.invokeGlobal("Global", "great-person", [{ "hello": "simon" }]);
},
{
code: "InvalidArg",
message: "expect String, got: Undefined"
}
);
t.deepEqual(instance!.invokeGlobal("Global", "get-string", []), "string");
t.deepEqual(instance!.invokeGlobal("Global", "person", []), { "name": "florian" });
})

View file

@ -0,0 +1,480 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava';
const path = require('node:path');
var Jimp = require("jimp");
import { private_api, Brush, Color, ImageData, ArrayModel } from '../index'
test('get/set string properties', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`export component App { in-out property <string> name: "Initial"; }`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
t.is(instance!.getProperty("name"), "Initial");
instance!.setProperty("name", "Hello");
t.is(instance!.getProperty("name"), "Hello");
t.throws(() => {
instance!.setProperty("name", 42)
},
{
code: "InvalidArg",
message: "expect String, got: Number"
}
);
t.throws(() => {
instance!.setProperty("name", { "blah": "foo" })
},
{
code: "InvalidArg",
message: "expect String, got: Object"
}
);
})
test('get/set number properties', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
in-out property <float> age: 42;
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
t.is(instance!.getProperty("age"), 42);
instance!.setProperty("age", 100);
t.is(instance!.getProperty("age"), 100);
t.throws(() => {
instance!.setProperty("age", "Hello")
},
{
code: "InvalidArg",
message: "expect Number, got: String"
}
);
t.throws(() => {
instance!.setProperty("age", { "blah": "foo" })
},
{
code: "InvalidArg",
message: "expect Number, got: Object"
}
);
})
test('get/set bool properties', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`export component App { in-out property <bool> ready: true; }`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
t.is(instance!.getProperty("ready"), true);
instance!.setProperty("ready", false);
t.is(instance!.getProperty("ready"), false);
t.throws(() => {
instance!.setProperty("ready", "Hello")
},
{
code: "InvalidArg",
message: "expect Boolean, got: String"
}
);
t.throws(() => {
instance!.setProperty("ready", { "blah": "foo" })
},
{
code: "InvalidArg",
message: "expect Boolean, got: Object"
}
);
})
test('set struct properties', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export struct Player {
name: string,
age: int,
energy_level: float
}
export component App {
in-out property <Player> player: {
name: "Florian",
age: 20,
energy_level: 40%
};
}
`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
t.deepEqual(instance!.getProperty("player"), {
"name": "Florian",
"age": 20,
"energy_level": 0.4
});
instance!.setProperty("player", {
"name": "Simon",
"age": 22,
"energy_level": 0.8
});
t.deepEqual(instance!.getProperty("player"), {
"name": "Simon",
"age": 22,
"energy_level": 0.8
});
// Missing properties throw an exception (TODO: the message is not very helpful, should say which one)
const incomplete_struct_err = t.throws(() => {
instance!.setProperty("player", {
"name": "Incomplete Player"
})
}, {
instanceOf: Error
}
);
t.is(incomplete_struct_err!.code, 'InvalidArg');
t.is(incomplete_struct_err!.message, 'expect Number, got: Undefined');
// Extra properties are thrown away
instance!.setProperty("player", {
"name": "Excessive Player",
"age": 100,
"energy_level": 0.8,
"weight": 200,
});
t.deepEqual(instance!.getProperty("player"), {
"name": "Excessive Player",
"age": 100,
"energy_level": 0.8
});
})
test('get/set image properties', async (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
in-out property <image> image: @image-url("resources/rgb.png");
in property <image> external-image;
out property <bool> external-image-ok: self.external-image.width == 64 && self.external-image.height == 64;
}
`, __filename);
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let slintImage = instance!.getProperty("image");
if (t.true((slintImage instanceof ImageData))) {
t.deepEqual((slintImage as ImageData).width, 64);
t.deepEqual((slintImage as ImageData).height, 64);
let image = await Jimp.read(path.join(__dirname, "resources/rgb.png"));
// Sanity check: setProperty fails when passed definitely a non-image
t.throws(() => {
instance!.setProperty("external-image", 42);
}, {
message: "Cannot convert object to image, because the provided object does not have an u32 `width` property"
});
t.throws(() => {
instance!.setProperty("external-image", { garbage: true });
}, {
message: "Cannot convert object to image, because the provided object does not have an u32 `width` property"
});
t.throws(() => {
instance!.setProperty("external-image", { width: [1, 2, 3] });
}, {
message: "Cannot convert object to image, because the provided object does not have an u32 `height` property"
});
t.throws(() => {
instance!.setProperty("external-image", { width: 1, height: 1, data: new Uint8ClampedArray() });
}, {
message: "data property does not have the correct size; expected 1 (width) * 1 (height) * 4 = 0; got 4"
});
t.is(image.bitmap.width, 64);
t.is(image.bitmap.height, 64);
// Duck typing: The `image.bitmap` object that Jump returns, has the shape of the official ImageData, so
// it should be possible to use it with Slint:
instance!.setProperty("external-image", image.bitmap);
t.is(instance!.getProperty("external-image-ok"), true);
t.is(image.bitmap.data.length, (slintImage as ImageData).data.length);
t.deepEqual(image.bitmap.data, (slintImage as ImageData).data);
}
})
test('get/set brush properties', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
in-out property <brush> black: #000000;
in-out property <brush> trans: transparent;
in-out property <brush> ref: transparent;
}
`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let black = instance!.getProperty("black");
t.is((black as Brush).toString(), "#000000ff");
if (t.true((black instanceof Brush))) {
let blackColor = (black as Brush).color;
t.deepEqual(blackColor.red, 0);
t.deepEqual(blackColor.green, 0);
t.deepEqual(blackColor.blue, 0);
}
instance?.setProperty("black", "#ffffff");
let white = instance!.getProperty("black");
if (t.true((white instanceof Brush))) {
let whiteColor = (white as Brush).color;
t.deepEqual(whiteColor.red, 255);
t.deepEqual(whiteColor.green, 255);
t.deepEqual(whiteColor.blue, 255);
}
let transparent = instance!.getProperty("trans");
if (t.true((black instanceof Brush))) {
t.assert((transparent as Brush).isTransparent);
}
let ref = Brush.fromColor(Color.fromRgb(100, 110, 120));
instance!.setProperty("ref", ref);
let instance_ref = instance!.getProperty("ref");
if (t.true((instance_ref instanceof Brush))) {
let ref_color = (instance_ref as Brush).color;
t.deepEqual(ref_color.red, 100);
t.deepEqual(ref_color.green, 110);
t.deepEqual(ref_color.blue, 120);
}
})
test('ArrayModel', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export struct Player {
name: string,
age: int
}
export component App {
in-out property <[int]> int-model;
in-out property <[string]> string-model;
in-out property <[Player]> struct-model;
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
instance!.setProperty("int-model", new ArrayModel([10, 9, 8]));
let intArrayModel = instance!.getProperty("int-model") as ArrayModel<number>;
t.deepEqual(intArrayModel.values(), new ArrayModel([10, 9, 8]).values());
instance!.setProperty("string-model", new ArrayModel(["Simon", "Olivier", "Auri", "Tobias", "Florian"]));
let stringArrayModel = instance!.getProperty("string-model") as ArrayModel<number>;
t.deepEqual(stringArrayModel.values(), new ArrayModel(["Simon", "Olivier", "Auri", "Tobias", "Florian"]).values());
instance!.setProperty("struct-model", new ArrayModel([ { "name": "simon", "age": 22 }, { "name": "florian", "age": 22 }]));
let structArrayModel = instance!.getProperty("struct-model") as ArrayModel<object>;
t.deepEqual(structArrayModel.values(), new ArrayModel([ { "name": "simon", "age": 22 }, { "name": "florian", "age": 22 }]).values());
})
test('ArrayModel rowCount', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
out property <int> model-length: model.length;
in-out property <[int]> model;
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let model = new ArrayModel([10, 9, 8]);
instance!.setProperty("model", model);
t.is(3, model.rowCount());
t.is(3, instance?.getProperty("model-length") as number);
})
test('ArrayModel rowData/setRowData', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
callback data(int) -> int;
in-out property <[int]> model;
data(row) => {
model[row]
}
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let model = new ArrayModel([10, 9, 8]);
instance!.setProperty("model", model);
t.is(9, model.rowData(1));
t.deepEqual(instance!.invoke("data", [1]), 9);
model.setRowData(1, 4);
t.is(4, model.rowData(1));
t.deepEqual(instance!.invoke("data", [1]), 4);
})
test('Model notify', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
width: 300px;
height: 300px;
out property<length> layout-height: layout.height;
in-out property<[length]> fixed-height-model;
VerticalLayout {
alignment: start;
layout := VerticalLayout {
for fixed-height in fixed-height-model: Rectangle {
background: blue;
height: fixed-height;
}
}
}
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let model = new ArrayModel([100, 0]);
instance!.setProperty("fixed-height-model", model);
t.is(100, instance!.getProperty("layout-height") as number);
model.setRowData(1, 50);
t.is(150, instance!.getProperty("layout-height") as number);
model.push(75);
t.is(225, instance!.getProperty("layout-height") as number);
model.remove(1, 2);
t.is(100, instance!.getProperty("layout-height") as number);
})
test('model from array', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App {
in-out property <[int]> int-array;
in-out property <[string]> string-array;
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
instance!.setProperty("int-array", [10, 9, 8]);
t.deepEqual(instance!.getProperty("int-array"), [10, 9, 8]);
instance!.setProperty("string-array", ["Simon", "Olivier", "Auri", "Tobias", "Florian"]);
t.deepEqual(instance!.getProperty("string-array"), ["Simon", "Olivier", "Auri", "Tobias", "Florian"]);
})
test('invoke callback', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export struct Person {
name: string
}
export component App {
callback great(string, string, string, string, string);
callback great-person(Person);
callback person() -> Person;
callback get-string() -> string;
person => {
{
name: "florian"
}
}
get-string => {
"string"
}
}
`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let speakTest;
instance!.setCallback("great", (a: string, b: string, c: string, d: string, e: string) => {
speakTest = "hello " + a + ", " + b + ", " + c + ", " + d + " and " + e;
});
instance!.invoke("great", ["simon", "olivier", "auri", "tobias", "florian"]);
t.deepEqual(speakTest, "hello simon, olivier, auri, tobias and florian");
instance!.setCallback("great-person", (p: any) => {
speakTest = "hello " + p.name;
});
instance!.invoke("great-person", [{ "name": "simon" }]);
t.deepEqual(speakTest, "hello simon");
t.throws(() => {
instance!.invoke("great-person", [{ "hello": "simon" }]);
},
{
code: "InvalidArg",
message: "expect String, got: Undefined"
}
);
t.deepEqual(instance!.invoke("get-string", []), "string");
t.deepEqual(instance!.invoke("person", []), { "name": "florian" });
})

View file

@ -0,0 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
export component Error {
out property bool> check: "Test";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

View file

@ -0,0 +1,10 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
export component Test {
callback say_hello();
in-out property <string> check;
}

View file

@ -0,0 +1,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
export component Test {
out property <string> check: "Test";
}

View file

@ -0,0 +1,101 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava';
import { Brush, Color, ArrayModel, Timer } from '../index'
test('Color from fromRgb', (t) => {
let color = Color.fromRgb(100, 110, 120);
t.deepEqual(color.red, 100);
t.deepEqual(color.green, 110);
t.deepEqual(color.blue, 120);
})
test('Color from fromArgb', (t) => {
let color = Color.fromArgb(120, 100, 110, 120);
t.deepEqual(color.red, 100);
t.deepEqual(color.green, 110);
t.deepEqual(color.blue, 120);
t.deepEqual(color.asArgbEncoded, 2019847800);
})
test('Color from fromArgbEncoded', (t) => {
let color = Color.fromArgbEncoded(2019847800);
t.deepEqual(color.red, 100);
t.deepEqual(color.green, 110);
t.deepEqual(color.blue, 120);
})
test('Color brighter', (t) => {
let color = Color.fromRgb(100, 110, 120).brighter(0.1);
t.deepEqual(color.red, 110);
t.deepEqual(color.green, 121);
t.deepEqual(color.blue, 132);
})
test('Color darker', (t) => {
let color = Color.fromRgb(100, 110, 120).darker(0.1);
t.deepEqual(color.red, 91);
t.deepEqual(color.green, 100);
t.deepEqual(color.blue, 109);
})
test('Brush from Color', (t) => {
let brush = Brush.fromColor(Color.fromRgb(100, 110, 120));
t.deepEqual(brush.color.red, 100);
t.deepEqual(brush.color.green, 110);
t.deepEqual(brush.color.blue, 120);
})
test('ArrayModel push', (t) => {
let arrayModel = new ArrayModel([0]);
t.is(arrayModel.rowCount(), 1);
t.is(arrayModel.rowData(0), 0);
arrayModel.push(2);
t.is(arrayModel.rowCount(), 2);
t.is(arrayModel.rowData(1), 2);
})
test('ArrayModel setRowData', (t) => {
let arrayModel = new ArrayModel([0]);
t.is(arrayModel.rowCount(), 1);
t.is(arrayModel.rowData(0), 0);
arrayModel.setRowData(0, 2);
t.is(arrayModel.rowCount(), 1);
t.is(arrayModel.rowData(0), 2);
})
test('ArrayModel remove', (t) => {
let arrayModel = new ArrayModel([0, 2, 1]);
t.is(arrayModel.rowCount(), 3);
t.is(arrayModel.rowData(0), 0);
t.is(arrayModel.rowData(1), 2);
arrayModel.remove(0, 2);
t.is(arrayModel.rowCount(), 1);
t.is(arrayModel.rowData(0), 1);
})
test('Timer negative duration', (t) => {
t.throws(() => {
Timer.singleShot(-1, function () {})
},
{
code: "GenericFailure",
message: "Duration cannot be negative"
}
);
})

View file

@ -0,0 +1,38 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava'
import { private_api, Window } from '../index'
test('Window constructor', (t) => {
t.throws(() => {
new Window()
},
{
code: "GenericFailure",
message: "Window can only be created by using a Component."
}
);
})
test('Window show / hide', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`
export component App inherits Window {
width: 300px;
height: 300px;
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
let window = instance!.window();
t.is(window.is_visible, false);
window.show();
t.is(window.is_visible, true);
window.hide();
t.is(window.is_visible, false);
})

View file

@ -2,5 +2,5 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
fn main() {
neon_build::setup();
napi_build::setup();
}

View file

@ -4,9 +4,9 @@
[![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui)
[Slint](https://slint.dev/) is a UI toolkit that supports different programming languages.
Slint-node is the integration with node.
Slint-node is the integration with Node.js.
To get started you can use the [Walk-through tutorial](https://slint.dev/docs/tutorial/node).
To get started you use the [walk-through tutorial](https://slint.dev/docs/tutorial/node).
We also have a [Getting Started Template](https://github.com/slint-ui/slint-nodejs-template) repository with
the code of a minimal application using Slint that can be used as a starting point to your program.
@ -15,7 +15,7 @@ Slint-node is still in the early stages of development: APIs will change and imp
## Slint Language Manual
The [Slint language manual](../slint) covers the Slint UI description language
The [Slint Language Documentation](../slint) covers the Slint UI description language
in detail.
## Installing Slint
@ -30,7 +30,7 @@ npm install slint-ui
You need to install the following components:
* **[Node.js](https://nodejs.org/download/release/v16.19.1/)** (v16. Newer versions currently not supported: [#961](https://github.com/slint-ui/slint/issues/961))
* **[Node.js](https://nodejs.org/download/release/)** (v16. or newer)
* **[npm](https://www.npmjs.com/)**
* **[Rust compiler](https://www.rust-lang.org/tools/install)** (1.70 or newer)
@ -38,41 +38,62 @@ You will also need a few more dependencies, see <https://github.com/slint-ui/sli
## Using Slint
First, import the API from the `slint-ui` module. In the following examples we're using [ECMAScript module syntax](https://nodejs.org/api/esm.html#modules-ecmascript-modules), but if you prefer you can also import the API using [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs-modules) syntax.
To initialize the API, you first need to import the `slint-ui` module in our code:
```js
let slint = require("slint-ui");
import * as slint from "slint-ui";
```
This step also installs a hook in NodeJS that allows you to import `.slint` files directly:
Next, load a slint file with the `loadFile` function:
```js
let ui = require("../ui/main.slint");
let ui = slint.loadFile("ui/main.slint");
```
Combining these two steps leads us to the obligatory "Hello World" example:
```js
require("slint-ui");
let ui = require("../ui/main.slint");
import * as slint from "slint-ui";
let ui = slint.loadFile(".ui/main.slint");
let main = new ui.Main();
main.run();
```
See [/examples/todo/node](https://github.com/slint-ui/slint/tree/master/examples/todo/node) for a full example.
For a full example, see [/examples/todo/node](https://github.com/slint-ui/slint/tree/master/examples/todo/node).
## API Overview
### Instantiating a component
### Instantiating a Component
The following example shows how to instantiating a Slint component from JavaScript.
**`ui/main.slint`**
```
export component MainWindow inherits Window {
callback clicked <=> i-touch-area.clicked;
in property <int> counter;
width: 400px;
height: 200px;
i-touch-area := TouchArea {}
}
```
The exported component is exposed as a type constructor. The type constructor takes as parameter
an object which allow to initialize the value of public properties or callbacks.
**`main.js`**
```js
require("slint-ui");
import * as slint from "slint-ui";
// In this example, the main.slint file exports a module which
// has a counter property and a clicked callback
let ui = require("ui/main.slint");
let ui = slint.loadFile("ui/main.slint");
let component = new ui.MainWindow({
counter: 42,
clicked: function() { console.log("hello"); }
@ -81,7 +102,7 @@ let component = new ui.MainWindow({
### Accessing a property
Properties are exposed as properties on the component instance
Properties declared as `out` or `in-out` in `.slint` files are visible as JavaScript on the component instance.
```js
component.counter = 42;
@ -90,9 +111,32 @@ console.log(component.counter);
### Callbacks
The callbacks are also exposed as property that have a setHandler function, and that can can be called.
Callback in Slint can be defined usign the `callback` keyword and can be connected to a callback of an other component
usign the `<=>` syntax.
**`ui/my-component.slint`**
```
export component MyComponent inherits Window {
callback clicked <=> i-touch-area.clicked;
width: 400px;
height: 200px;
i-touch-area := TouchArea {}
}
```
The callbacks in JavaScript are exposed as property that has a setHandler function, and that can be called as a function.
**`main.js`**
```js
import * as slint from "slint-ui";
let ui = slint.loadFile("ui/my-component.slint");
let component = new ui.MyComponent();
// connect to a callback
component.clicked.setHandler(function() { console.log("hello"); })
// emit a callback
@ -101,65 +145,36 @@ component.clicked();
### Type Mappings
The types used for properties in .slint design markup each translate to specific types in JavaScript. The follow table summarizes the entire mapping:
| `.slint` Type | JavaScript Type | Notes |
| --- | --- | --- |
| `int` | `Number` | |
| `float` | `Number` | |
| `string` | `String` | |
| `color` | `String` | Colors are represented as strings in the form `"#rrggbbaa"`. When setting a color property, any CSS compliant color is accepted as a string. |
| `color` | {@link Color} | |
| `brush` | {@link Brush} | |
| `image` | {@link ImageData} | |
| `length` | `Number` | |
| `physical_length` | `Number` | |
| `duration` | `Number` | The number of milliseconds |
| `angle` | `Number` | The value in degrees |
| structure | `Object` | Structures are mapped to JavaScrip objects with structure fields mapped to properties. |
| array | `Array` or Model Object | |
| `angle` | `Number` | The angle in degrees |
| structure | `Object` | Structures are mapped to JavaScript objects where each structure field is a property. |
| array | `Array` or any implementation of {@link Model} | |
### Models
### Arrays and Models
For property of array type, they can either be set using an array.
In that case, getting the property also return an array.
If the array was set within the .slint file, the array can be obtained
[Array properties](../slint/src/reference/types#arrays-and-models) can be set from JavaScript by passing
either `Array` objects or implementations of the {@link Model} interface.
When passing a JavaScript `Array` object, the contents of the array are copied. Any changes to the JavaScript afterwards will not be visible on the Slint side. Similarly, reading a Slint array property from JavaScript that was
previously initialised from a JavaScript `Array`, will return a newly allocated JavaScript `Array`.
```js
component.model = [1, 2, 3];
// component.model.push(4); // does not work, because it operate on a copy
// but re-assigning works
// component.model.push(4); // does not work, because assignment creates a copy.
// Use re-assignment instead.
component.model = component.model.concat(4);
```
Another option is to set a model object. A model object has the following function:
* `rowCount()`: returns the number of element in the model.
* `rowData(index)`: return the row at the given index
* `setRowData(index, data)`: called when the model need to be changed. `this.notify.rowDataChanged` must be called if successful.
When such an object is set to a model property, it gets a new `notify` object with the following function
* `rowDataChanged(index)`: notify the view that the row was changed.
* `rowAdded(index, count)`: notify the view that rows were added.
* `rowRemoved(index, count)`: notify the view that a row were removed.
* `reset()`: notify the view that everything may have changed.
As an example, here is the implementation of the `ArrayModel` (which is available as `slint.ArrayModel`)
```js
let array = [1, 2, 3];
let model = {
rowCount() { return a.length; },
rowData(row) { return a[row]; },
setRowData(row, data) { a[row] = data; this.notify.rowDataChanged(row); },
push() {
let size = a.length;
Array.prototype.push.apply(a, arguments);
this.notify.rowAdded(size, arguments.length);
},
remove(index, size) {
let r = a.splice(index, size);
this.notify.rowRemoved(size, arguments.length);
},
};
component.model = model;
model.push(4); // this works
// does NOT work, getting the model does not return the right object
// component.model.push(5);
```
Another option is to set an object that implements the {@link Model} interface. Rreading a Slint array property from JavaScript that was previously initialised from a {@link Model} object, will return a reference to the model.

389
api/node/index.ts Normal file
View file

@ -0,0 +1,389 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import * as path from "path";
import * as napi from "./rust-module";
export { Diagnostic, DiagnosticLevel, Window, Brush, Color, ImageData, Point, Size, SlintModelNotify } from "./rust-module";
/**
* ModelPeer is the interface that the run-time implements. An instance is
* set on dynamic {@link Model} instances and can be used to notify the run-time
* of changes in the structure or data of the model.
*/
export interface ModelPeer {
/**
* Call this function from our own model to notify that fields of data
* in the specified row have changed.
* @argument row
*/
rowDataChanged(row: number): void;
/**
* Call this function from your own model to notify that one or multiple
* rows were added to the model, starting at the specified row.
* @param row
* @param count
*/
rowAdded(row: number, count: number): void;
/**
* Call this function from your own model to notify that one or multiple
* rows were removed from the model, starting at the specified row.
* @param row
* @param count
*/
rowRemoved(row: number, count: number): void;
/**
* Call this function from your own model to notify that the model has been
* changed and everything must be reloaded
*/
reset(): void;
}
/**
* Model<T> is the interface for feeding dynamic data into
* `.slint` views.
*
* A model is organized like a table with rows of data. The
* fields of the data type T behave like columns.
*
* ### Example
* As an example let's see the implementation of {@link ArrayModel}
*
* ```js
* export class ArrayModel<T> implements Model<T> {
* private a: Array<T>
* notify: ModelPeer;
*
* constructor(arr: Array<T>) {
* this.a = arr;
* this.notify = new NullPeer();
* }
*
* rowCount() {
* return this.a.length;
* }
*
* rowData(row: number) {
* return this.a[row];
* }
*
* setRowData(row: number, data: T) {
* this.a[row] = data;
* this.notify.rowDataChanged(row);
* }
*
* push(...values: T[]) {
* let size = this.a.length;
* Array.prototype.push.apply(this.a, values);
* this.notify.rowAdded(size, arguments.length);
* }
*
* remove(index: number, size: number) {
* let r = this.a.splice(index, size);
* this.notify.rowRemoved(index, size);
* }
*
* get length(): number {
* return this.a.length;
* }
*
* values(): IterableIterator<T> {
* return this.a.values();
* }
*
* entries(): IterableIterator<[number, T]> {
* return this.a.entries()
* }
*}
* ```
*/
export interface Model<T> {
/**
* Implementations of this function must return the current number of rows.
*/
rowCount(): number;
/**
* Implementations of this function must return the data at the specified row.
* @param row
*/
rowData(row: number): T | undefined;
/**
* Implementations of this function must store the provided data parameter
* in the model at the specified row.
* @param row
* @param data
*/
setRowData(row: number, data: T): void;
/**
* This public member is set by the run-time and implementation must use this
* to notify the run-time of changes in the model.
*/
notify: ModelPeer;
}
/**
* @hidden
*/
class NullPeer implements ModelPeer {
rowDataChanged(row: number): void { }
rowAdded(row: number, count: number): void { }
rowRemoved(row: number, count: number): void { }
reset(): void { }
}
/**
* ArrayModel wraps a JavaScript array for use in `.slint` views. The underlying
* array can be modified with the [[ArrayModel.push]] and [[ArrayModel.remove]] methods.
*/
export class ArrayModel<T> implements Model<T> {
/**
* @hidden
*/
private a: Array<T>
notify: ModelPeer;
/**
* Creates a new ArrayModel.
*
* @param arr
*/
constructor(arr: Array<T>) {
this.a = arr;
this.notify = new NullPeer();
}
rowCount() {
return this.a.length;
}
rowData(row: number) {
return this.a[row];
}
setRowData(row: number, data: T) {
this.a[row] = data;
this.notify.rowDataChanged(row);
}
/**
* Pushes new values to the array that's backing the model and notifies
* the run-time about the added rows.
* @param values
*/
push(...values: T[]) {
let size = this.a.length;
Array.prototype.push.apply(this.a, values);
this.notify.rowAdded(size, arguments.length);
}
// FIXME: should this be named splice and have the splice api?
/**
* Removes the specified number of element from the array that's backing
* the model, starting at the specified index. This is equivalent to calling
* Array.slice() on the array and notifying the run-time about the removed
* rows.
* @param index
* @param size
*/
remove(index: number, size: number) {
let r = this.a.splice(index, size);
this.notify.rowRemoved(index, size);
}
get length(): number {
return this.a.length;
}
values(): IterableIterator<T> {
return this.a.values();
}
entries(): IterableIterator<[number, T]> {
return this.a.entries()
}
}
/**
* This interface describes the public API of a Slint component that is common to all instances. Use this to
* show() the window on the screen, access the window and subsequent window properties, or start the
* Slint event loop with run().
*/
export interface ComponentHandle {
/**
* Shows the window and runs the event loop.
*/
run();
/**
* Shows the component's window on the screen.
*/
show();
/**
* Hides the component's window, so that it is not visible anymore.
*/
hide();
/**
* Returns the {@link Window} associated with this component instance.
* The window API can be used to control different aspects of the integration into the windowing system, such as the position on the screen.
*/
get window(): napi.Window;
}
/**
* @hidden
*/
class Component implements ComponentHandle {
private instance: napi.ComponentInstance;
/**
* @hidden
*/
constructor(instance: napi.ComponentInstance) {
this.instance = instance;
}
run() {
this.instance.run();
}
show() {
this.instance.window().show();
}
hide() {
this.instance.window().hide();
}
get window(): napi.Window {
return this.instance.window();
}
/**
* @hidden
*/
get component_instance(): napi.ComponentInstance {
return this.instance;
}
}
/**
* @hidden
*/
interface Callback {
(): any;
setHandler(cb: any): void;
}
/**
* Represents an errors that can be emitted by the compiler.
*/
export class CompileError extends Error {
public diagnostics: napi.Diagnostic[];
/**
* Creates a new CompileError.
*
* @param message
* @param diagnostics
*/
constructor(message: string, diagnostics: napi.Diagnostic[]) {
super(message);
this.diagnostics = diagnostics;
}
}
/**
* Loads the given slint file and returns a constructor to create an instance of the exported component.
*/
export function loadFile(filePath: string) : Object {
// this is a workaround that fixes an issue there resources in slint files cannot be loaded if the
// file path is given as relative path
let absoluteFilePath = path.resolve(filePath);
let compiler = new napi.ComponentCompiler;
let definition = compiler.buildFromPath(absoluteFilePath);
let diagnostics = compiler.diagnostics;
if (diagnostics.length > 0) {
let warnings = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Warning);
warnings.forEach((w) => console.log("Warning: " + w));
let errors = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Error);
if (errors.length > 0) {
throw new CompileError("Could not compile " + filePath, errors);
}
}
let slint_module = Object.create({});
Object.defineProperty(slint_module, definition!.name.replace(/-/g, '_'), {
value: function(properties: any) {
let instance = definition!.create();
if (instance == null) {
throw Error("Could not create a component handle for" + filePath);
}
for(var key in properties) {
let value = properties[key];
if (value instanceof Function) {
instance.setCallback(key, value);
} else {
instance.setProperty(key, properties[key]);
}
}
let componentHandle = new Component(instance!);
instance!.definition().properties.forEach((prop) => {
Object.defineProperty(componentHandle, prop.name.replace(/-/g, '_') , {
get() { return instance!.getProperty(prop.name); },
set(value) { instance!.setProperty(prop.name, value); },
enumerable: true
})
});
instance!.definition().callbacks.forEach((cb) => {
Object.defineProperty(componentHandle, cb.replace(/-/g, '_') , {
get() {
let callback = function () { return instance!.invoke(cb, Array.from(arguments)); } as Callback;
callback.setHandler = function (callback) { instance!.setCallback(cb, callback) };
return callback;
},
enumerable: true,
})
});
return componentHandle;
},
});
return slint_module;
}
// This api will be removed after teh event loop handling is merged check PR #3718.
// After that this in no longer necessary.
export namespace Timer {
export function singleShot(duration: number, handler: () => void) {
napi.singleshotTimer(duration, handler)
}
}
/**
* @hidden
*/
export namespace private_api {
export import mock_elapsed_time = napi.mockElapsedTime;
export import ComponentCompiler = napi.ComponentCompiler;
export import ComponentDefinition = napi.ComponentDefinition;
export import ComponentInstance = napi.ComponentInstance;
export import ValueType = napi.ValueType;
export function send_mouse_click(component: Component, x: number, y: number) {
component.component_instance.sendMouseClick(x, y);
}
export function send_keyboard_string_sequence(component: Component, s: string) {
component.component_instance.sendKeyboardStringSequence(s);
}
}

View file

@ -1,314 +0,0 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
// Load the native library with `process.dlopen` instead of with `require`.
// This is only done for autotest that do not require nom or neon_cli to
// copy the lib to its right place
/**
* @hidden
*/
function load_native_lib() {
const os = require('os');
(process as any).dlopen(module, process.env.SLINT_NODE_NATIVE_LIB,
os.constants.dlopen.RTLD_NOW);
return module.exports;
}
/**
* @hidden
*/
let native = !process.env.SLINT_NODE_NATIVE_LIB ? require('../native/index.node') : load_native_lib();
/**
* @hidden
*/
class Component {
protected comp: any;
constructor(comp: any) {
this.comp = comp;
}
run() {
this.comp.run();
}
show() {
this.window.show();
}
hide() {
this.window.hide()
}
get window(): SlintWindow {
return new WindowAPI(this.comp.window());
}
get component(): any {
return this.comp;
}
}
interface Point {
x: number;
y: number;
}
interface Size {
width: number;
height: number;
}
interface SlintWindow {
show(): void;
hide(): void;
is_visible: boolean;
logical_position: Point;
physical_position: Point;
logical_size: Size;
physical_size: Size;
}
/**
* @hidden
*/
class WindowAPI implements SlintWindow {
protected impl: any;
constructor(impl: any) {
this.impl = impl;
}
show(): void {
this.impl.show();
}
hide(): void {
this.impl.hide();
}
get is_visible(): boolean {
return this.impl.get_is_visible();
}
get logical_position(): Point {
return this.impl.get_logical_position();
}
set logical_position(pos: Point) {
this.impl.set_logical_position(pos);
}
get physical_position(): Point {
return this.impl.get_physical_position();
}
set physical_position(pos: Point) {
this.impl.set_physical_position(pos);
}
get logical_size(): Size {
return this.impl.get_logical_size();
}
set logical_size(size: Size) {
this.impl.set_logical_size(size);
}
get physical_size(): Size {
return this.impl.get_physical_size();
}
set physical_size(size: Size) {
this.impl.set_physical_size(size);
}
}
/**
* @hidden
*/
interface Callback {
(): any;
setHandler(cb: any): void;
}
require.extensions['.60'] = require.extensions['.slint'] =
function (module, filename) {
var c = native.load(filename);
module.exports[c.name().replace(/-/g, '_')] = function (init_properties: any) {
let comp = c.create(init_properties);
let ret = new Component(comp);
c.properties().forEach((x: string) => {
Object.defineProperty(ret, x.replace(/-/g, '_'), {
get() { return comp.get_property(x); },
set(newValue) { comp.set_property(x, newValue); },
enumerable: true,
})
});
c.callbacks().forEach((x: string) => {
Object.defineProperty(ret, x.replace(/-/g, '_'), {
get() {
let callback = function () { return comp.invoke_callback(x, [...arguments]); } as Callback;
callback.setHandler = function (callback) { comp.connect_callback(x, callback) };
return callback;
},
enumerable: true,
})
});
return ret;
}
}
/**
* ModelPeer is the interface that the run-time implements. An instance is
* set on dynamic Model<T> instances and can be used to notify the run-time
* of changes in the structure or data of the model.
*/
interface ModelPeer {
/**
* Call this function from our own model to notify that fields of data
* in the specified row have changed.
* @argument row
*/
rowDataChanged(row: number): void;
/**
* Call this function from your own model to notify that one or multiple
* rows were added to the model, starting at the specified row.
* @param row
* @param count
*/
rowAdded(row: number, count: number): void;
/**
* Call this function from your own model to notify that one or multiple
* rows were removed from the model, starting at the specified row.
* @param row
* @param count
*/
rowRemoved(row: number, count: number): void;
/**
* Call this function from your own model to notify that the model has been
* changed and everything must be reloaded
*/
reset(): void;
}
/**
* Model<T> is the interface for feeding dynamic data into
* `.slint` views.
*
* A model is organized like a table with rows of data. The
* fields of the data type T behave like columns.
*/
interface Model<T> {
/**
* Implementations of this function must return the current number of rows.
*/
rowCount(): number;
/**
* Implementations of this function must return the data at the specified row.
* @param row
*/
rowData(row: number): T;
/**
* Implementations of this function must store the provided data parameter
* in the model at the specified row.
* @param row
* @param data
*/
setRowData(row: number, data: T): void;
/**
* This public member is set by the run-time and implementation must use this
* to notify the run-time of changes in the model.
*/
notify: ModelPeer;
}
/**
* @hidden
*/
class NullPeer implements ModelPeer {
rowDataChanged(row: number): void { }
rowAdded(row: number, count: number): void { }
rowRemoved(row: number, count: number): void { }
reset(): void { }
}
/**
* ArrayModel wraps a JavaScript array for use in `.slint` views. The underlying
* array can be modified with the [[ArrayModel.push]] and [[ArrayModel.remove]] methods.
*/
class ArrayModel<T> implements Model<T> {
/**
* @hidden
*/
private a: Array<T>
notify: ModelPeer;
/**
* Creates a new ArrayModel.
*
* @param arr
*/
constructor(arr: Array<T>) {
this.a = arr;
this.notify = new NullPeer();
}
rowCount() {
return this.a.length;
}
rowData(row: number) {
return this.a[row];
}
setRowData(row: number, data: T) {
this.a[row] = data;
this.notify.rowDataChanged(row);
}
/**
* Pushes new values to the array that's backing the model and notifies
* the run-time about the added rows.
* @param values
*/
push(...values: T[]) {
let size = this.a.length;
Array.prototype.push.apply(this.a, values);
this.notify.rowAdded(size, arguments.length);
}
// FIXME: should this be named splice and have the splice api?
/**
* Removes the specified number of element from the array that's backing
* the model, starting at the specified index. This is equivalent to calling
* Array.slice() on the array and notifying the run-time about the removed
* rows.
* @param index
* @param size
*/
remove(index: number, size: number) {
let r = this.a.splice(index, size);
this.notify.rowRemoved(index, size);
}
get length(): number {
return this.a.length;
}
values(): IterableIterator<T> {
return this.a.values();
}
entries(): IterableIterator<[number, T]> {
return this.a.entries()
}
}
function send_mouse_click(component: Component, x: number, y: number) {
component.component.send_mouse_click(x, y)
}
function send_keyboard_string_sequence(component: Component, s: String) {
component.component.send_keyboard_string_sequence(s)
}
module.exports = {
private_api: {
mock_elapsed_time: native.mock_elapsed_time,
send_mouse_click: send_mouse_click,
send_keyboard_string_sequence: send_keyboard_string_sequence,
},
ArrayModel: ArrayModel,
Timer: {
singleShot: native.singleshot_timer,
},
};

View file

@ -1,42 +0,0 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import { URL, pathToFileURL } from 'url';
const extensionsRegex = /\.(60|slint)$/;
const baseURL = pathToFileURL(`${process.cwd()}/`).href;
export function resolve(specifier, context, defaultResolve) {
const { parentURL = baseURL } = context;
if (extensionsRegex.test(specifier)) {
return { url: new URL(specifier, parentURL).href };
}
return defaultResolve(specifier, context, defaultResolve);
}
export function getFormat(url, context, defaultGetFormat) {
if (extensionsRegex.test(url)) {
return {
format: 'module'
};
}
return defaultGetFormat(url, context, defaultGetFormat);
}
export function transformSource(source, context, defaultTransformSource) {
const { url, format } = context;
if (extensionsRegex.test(url)) {
console.log(`This is where one can compile ${url}`)
return {
source: "console.log('Hey'); export function foo(x) { return x + 55 }"
};
}
// Let Node.js handle all other sources.
return defaultTransformSource(source, context, defaultTransformSource);
}

View file

@ -1,155 +0,0 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use i_slint_compiler::langtype::Type;
use i_slint_core::model::Model;
use neon::prelude::*;
use std::cell::Cell;
use std::rc::{Rc, Weak};
/// Model coming from JS
pub struct JsModel {
notify: i_slint_core::model::ModelNotify,
/// The index of the value in the PersistentContext
value_index: u32,
data_type: Type,
}
impl JsModel {
pub fn new<'cx>(
obj: Handle<'cx, JsObject>,
data_type: Type,
cx: &mut impl Context<'cx>,
persistent_context: &crate::persistent_context::PersistentContext<'cx>,
) -> NeonResult<Rc<Self>> {
let val = obj.as_value(cx);
let model = Rc::new(JsModel {
notify: Default::default(),
value_index: persistent_context.allocate(cx, val),
data_type,
});
let mut notify = SlintModelNotify::new::<_, JsValue, _>(cx, std::iter::empty())?;
cx.borrow_mut(&mut notify, |mut notify| notify.0 = Rc::downgrade(&model));
let notify = notify.as_value(cx);
obj.set(cx, "notify", notify)?;
Ok(model)
}
pub fn get_object<'cx>(
&self,
cx: &mut impl Context<'cx>,
persistent_context: &crate::persistent_context::PersistentContext<'cx>,
) -> JsResult<'cx, JsObject> {
persistent_context.get(cx, self.value_index)?.downcast_or_throw(cx)
}
}
impl Model for JsModel {
type Data = slint_interpreter::Value;
fn row_count(&self) -> usize {
let r = Cell::new(0usize);
crate::run_with_global_context(&|cx, persistent_context| {
let obj = self.get_object(cx, persistent_context).unwrap();
let _ = obj
.get(cx, "rowCount")
.ok()
.and_then(|func| func.downcast::<JsFunction>().ok())
.and_then(|func| func.call(cx, obj, std::iter::empty::<Handle<JsValue>>()).ok())
.and_then(|res| res.downcast::<JsNumber>().ok())
.map(|num| r.set(num.value() as _));
});
r.get()
}
fn row_data(&self, row: usize) -> Option<Self::Data> {
if row >= self.row_count() {
None
} else {
let r = Cell::new(slint_interpreter::Value::default());
crate::run_with_global_context(&|cx, persistent_context| {
let row = JsNumber::new(cx, row as f64);
let obj = self.get_object(cx, persistent_context).unwrap();
let _ = obj
.get(cx, "rowData")
.ok()
.and_then(|func| func.downcast::<JsFunction>().ok())
.and_then(|func| func.call(cx, obj, std::iter::once(row)).ok())
.and_then(|res| {
crate::to_eval_value(res, self.data_type.clone(), cx, persistent_context)
.ok()
})
.map(|res| r.set(res));
});
Some(r.into_inner())
}
}
fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker {
&self.notify
}
fn set_row_data(&self, row: usize, data: Self::Data) {
crate::run_with_global_context(&|cx, persistent_context| {
let row = JsNumber::new(cx, row as f64).as_value(cx);
let data = crate::to_js_value(data.clone(), cx, persistent_context).unwrap();
let obj = self.get_object(cx, persistent_context).unwrap();
let _ = obj
.get(cx, "setRowData")
.ok()
.and_then(|func| func.downcast::<JsFunction>().ok())
.and_then(|func| func.call(cx, obj, [row, data].iter().cloned()).ok());
});
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
}
struct WrappedJsModel(Weak<JsModel>);
declare_types! {
class SlintModelNotify for WrappedJsModel {
init(_) {
Ok(WrappedJsModel(Weak::default()))
}
method rowDataChanged(mut cx) {
let this = cx.this();
let row = cx.argument::<JsNumber>(0)?.value() as usize;
if let Some(model) = cx.borrow(&this, |x| x.0.upgrade()) {
model.notify.row_changed(row)
}
Ok(JsUndefined::new().as_value(&mut cx))
}
method rowAdded(mut cx) {
let this = cx.this();
let row = cx.argument::<JsNumber>(0)?.value() as usize;
let count = cx.argument::<JsNumber>(1)?.value() as usize;
if let Some(model) = cx.borrow(&this, |x| x.0.upgrade()) {
model.notify.row_added(row, count)
}
Ok(JsUndefined::new().as_value(&mut cx))
}
method rowRemoved(mut cx) {
let this = cx.this();
let row = cx.argument::<JsNumber>(0)?.value() as usize;
let count = cx.argument::<JsNumber>(1)?.value() as usize;
if let Some(model) = cx.borrow(&this, |x| x.0.upgrade()) {
model.notify.row_removed(row, count)
}
Ok(JsUndefined::new().as_value(&mut cx))
}
method reset(mut cx) {
let this = cx.this();
if let Some(model) = cx.borrow(&this, |x| x.0.upgrade()) {
model.notify.reset()
}
Ok(JsUndefined::new().as_value(&mut cx))
}
}
}

View file

@ -1,703 +0,0 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use core::cell::RefCell;
use i_slint_compiler::langtype::Type;
use i_slint_core::model::{Model, ModelRc};
use i_slint_core::window::WindowInner;
use i_slint_core::{ImageInner, SharedVector};
use itertools::Itertools;
use neon::prelude::*;
use rand::RngCore;
use slint_interpreter::ComponentHandle;
mod js_model;
mod persistent_context;
struct WrappedComponentType(Option<slint_interpreter::ComponentDefinition>);
struct WrappedItemTreeRc(Option<slint_interpreter::ComponentInstance>);
struct WrappedWindow(Option<std::rc::Rc<dyn i_slint_core::window::WindowAdapter>>);
/// We need to do some gymnastic with closures to pass the ExecuteContext with the right lifetime
type GlobalContextCallback<'c> =
dyn for<'b> Fn(&mut ExecuteContext<'b>, &persistent_context::PersistentContext<'b>) + 'c;
scoped_tls_hkt::scoped_thread_local!(static GLOBAL_CONTEXT:
for <'a> &'a dyn for<'c> Fn(&'c GlobalContextCallback<'c>));
/// This function exists as a workaround so one can access the ExecuteContext from callback handler
fn run_scoped<'cx, T>(
cx: &mut impl Context<'cx>,
object_with_persistent_context: Handle<'cx, JsObject>,
functor: impl FnOnce() -> Result<T, String>,
) -> NeonResult<T> {
let persistent_context =
persistent_context::PersistentContext::from_object(cx, object_with_persistent_context)?;
cx.execute_scoped(|cx| {
let cx = RefCell::new(cx);
let cx_fn = move |callback: &GlobalContextCallback| {
callback(&mut *cx.borrow_mut(), &persistent_context)
};
GLOBAL_CONTEXT.set(&&cx_fn, functor)
})
.or_else(|e| cx.throw_error(e))
}
fn run_with_global_context(f: &GlobalContextCallback) {
GLOBAL_CONTEXT.with(|cx_fn| cx_fn(f))
}
/// Load a .slint files.
///
/// The first argument of this function is a string to the .slint file
///
/// The return value is a SlintComponentType
fn load(mut cx: FunctionContext) -> JsResult<JsValue> {
let path = cx.argument::<JsString>(0)?.value();
let path = std::path::Path::new(path.as_str());
let include_paths = match std::env::var_os("SLINT_INCLUDE_PATH") {
Some(paths) => {
std::env::split_paths(&paths).filter(|path| !path.as_os_str().is_empty()).collect()
}
None => vec![],
};
let library_paths = match std::env::var_os("SLINT_LIBRARY_PATH") {
Some(paths) => std::env::split_paths(&paths)
.filter_map(|entry| {
entry
.to_str()
.unwrap_or_default()
.split('=')
.collect_tuple()
.map(|(k, v)| (k.into(), v.into()))
})
.collect(),
None => std::collections::HashMap::new(),
};
let mut compiler = slint_interpreter::ComponentCompiler::default();
compiler.set_include_paths(include_paths);
compiler.set_library_paths(library_paths);
let c = spin_on::spin_on(compiler.build_from_path(path));
slint_interpreter::print_diagnostics(compiler.diagnostics());
let c = if let Some(c) = c { c } else { return cx.throw_error("Compilation error") };
let mut obj = SlintComponentType::new::<_, JsValue, _>(&mut cx, std::iter::empty())?;
cx.borrow_mut(&mut obj, |mut obj| obj.0 = Some(c));
Ok(obj.as_value(&mut cx))
}
fn make_callback_handler<'cx>(
cx: &mut impl Context<'cx>,
persistent_context: &persistent_context::PersistentContext<'cx>,
fun: Handle<'cx, JsFunction>,
return_type: Option<Box<Type>>,
) -> Box<dyn Fn(&[slint_interpreter::Value]) -> slint_interpreter::Value> {
let fun_value = fun.as_value(cx);
let fun_idx = persistent_context.allocate(cx, fun_value);
Box::new(move |args| {
let args = args.to_vec();
let ret = core::cell::Cell::new(slint_interpreter::Value::Void);
let borrow_ret = &ret;
let return_type = &return_type;
run_with_global_context(&move |cx, persistent_context| {
let args = args
.iter()
.map(|a| to_js_value(a.clone(), cx, persistent_context).unwrap())
.collect::<Vec<_>>();
let ret = persistent_context
.get(cx, fun_idx)
.unwrap()
.downcast::<JsFunction>()
.unwrap()
.call::<_, _, JsValue, _>(cx, JsUndefined::new(), args)
.unwrap();
if let Some(return_type) = return_type {
borrow_ret.set(
to_eval_value(ret, (**return_type).clone(), cx, persistent_context).unwrap(),
);
}
});
ret.into_inner()
})
}
fn create<'cx>(
cx: &mut CallContext<'cx, impl neon::object::This>,
component_type: slint_interpreter::ComponentDefinition,
) -> JsResult<'cx, JsValue> {
let component = component_type.create().unwrap();
let persistent_context = persistent_context::PersistentContext::new(cx);
if let Some(args) = cx.argument_opt(0).and_then(|arg| arg.downcast::<JsObject>().ok()) {
let properties = component_type
.properties_and_callbacks()
.map(|(k, v)| (k.replace('_', "-"), v))
.collect::<std::collections::HashMap<_, _>>();
for x in args.get_own_property_names(cx)?.to_vec(cx)? {
let prop_name = x.to_string(cx)?.value().replace('_', "-");
let value = args.get(cx, x)?;
let ty = properties
.get(&prop_name)
.ok_or(())
.or_else(|()| {
cx.throw_error(format!("Property {} not found in the component", prop_name))
})?
.clone();
if let Type::Callback { return_type, .. } = ty {
let fun = value.downcast_or_throw::<JsFunction, _>(cx)?;
component
.set_callback(
prop_name.as_str(),
make_callback_handler(cx, &persistent_context, fun, return_type),
)
.or_else(|_| cx.throw_error("Cannot set callback"))?;
} else {
let value = to_eval_value(value, ty, cx, &persistent_context)?;
component
.set_property(prop_name.as_str(), value)
.or_else(|_| cx.throw_error("Cannot assign property"))?;
}
}
}
let mut obj = SlintComponent::new::<_, JsValue, _>(cx, std::iter::empty())?;
persistent_context.save_to_object(cx, obj.downcast().unwrap());
cx.borrow_mut(&mut obj, |mut obj| obj.0 = Some(component));
Ok(obj.as_value(cx))
}
fn to_eval_value<'cx>(
val: Handle<'cx, JsValue>,
ty: i_slint_compiler::langtype::Type,
cx: &mut impl Context<'cx>,
persistent_context: &persistent_context::PersistentContext<'cx>,
) -> NeonResult<slint_interpreter::Value> {
use slint_interpreter::Value;
match ty {
Type::Float32
| Type::Int32
| Type::Duration
| Type::Angle
| Type::PhysicalLength
| Type::LogicalLength
| Type::Rem
| Type::Percent
| Type::UnitProduct(_) => {
Ok(Value::Number(val.downcast_or_throw::<JsNumber, _>(cx)?.value()))
}
Type::String => Ok(Value::String(val.to_string(cx)?.value().into())),
Type::Color | Type::Brush => {
let c = val
.to_string(cx)?
.value()
.parse::<css_color_parser2::Color>()
.or_else(|e| cx.throw_error(&e.to_string()))?;
Ok((i_slint_core::Color::from_argb_u8((c.a * 255.) as u8, c.r, c.g, c.b)).into())
}
Type::Array(a) => match val.downcast::<JsArray>() {
Ok(arr) => {
let vec = arr.to_vec(cx)?;
Ok(Value::Model(ModelRc::new(i_slint_core::model::SharedVectorModel::from(
vec.into_iter()
.map(|i| to_eval_value(i, (*a).clone(), cx, persistent_context))
.collect::<Result<SharedVector<_>, _>>()?,
))))
}
Err(_) => {
let obj = val.downcast_or_throw::<JsObject, _>(cx)?;
obj.get(cx, "rowCount")?.downcast_or_throw::<JsFunction, _>(cx)?;
obj.get(cx, "rowData")?.downcast_or_throw::<JsFunction, _>(cx)?;
let m = js_model::JsModel::new(obj, *a, cx, persistent_context)?;
Ok(Value::Model(m.into()))
}
},
Type::Image => {
let path = val.to_string(cx)?.value();
Ok(Value::Image(
i_slint_core::graphics::Image::load_from_path(std::path::Path::new(&path))
.or_else(|_| cx.throw_error(format!("cannot load image {:?}", path)))?,
))
}
Type::Bool => Ok(Value::Bool(val.downcast_or_throw::<JsBoolean, _>(cx)?.value())),
Type::Struct { fields, .. } => {
let obj = val.downcast_or_throw::<JsObject, _>(cx)?;
Ok(Value::Struct(
fields
.iter()
.map(|(pro_name, pro_ty)| {
Ok((
pro_name.clone(),
to_eval_value(
obj.get(cx, pro_name.replace('-', "_").as_str())?,
pro_ty.clone(),
cx,
persistent_context,
)?,
))
})
.collect::<Result<_, _>>()?,
))
}
Type::Enumeration(_) => todo!(),
Type::Invalid
| Type::Void
| Type::InferredProperty
| Type::InferredCallback
| Type::Function { .. }
| Type::Model
| Type::Callback { .. }
| Type::ComponentFactory { .. }
| Type::Easing
| Type::PathData
| Type::LayoutCache
| Type::ElementReference => cx.throw_error("Cannot convert to a Slint property value"),
}
}
fn to_js_value<'cx>(
val: slint_interpreter::Value,
cx: &mut impl Context<'cx>,
persistent_context: &persistent_context::PersistentContext<'cx>,
) -> NeonResult<Handle<'cx, JsValue>> {
use slint_interpreter::Value;
Ok(match val {
Value::Void => JsUndefined::new().as_value(cx),
Value::Number(n) => JsNumber::new(cx, n).as_value(cx),
Value::String(s) => JsString::new(cx, s.as_str()).as_value(cx),
Value::Bool(b) => JsBoolean::new(cx, b).as_value(cx),
Value::Image(r) => match (&r).into() {
&ImageInner::None => JsUndefined::new().as_value(cx),
&ImageInner::EmbeddedImage { .. }
| &ImageInner::StaticTextures { .. }
| &ImageInner::Svg(..)
| &ImageInner::BackendStorage(..)
| &ImageInner::BorrowedOpenGLTexture(..) => JsNull::new().as_value(cx), // TODO: maybe pass around node buffers?
},
Value::Model(model) => {
if let Some(js_model) = model.as_any().downcast_ref::<js_model::JsModel>() {
js_model.get_object(cx, persistent_context)?.as_value(cx)
} else {
// TODO: this should probably create a proxy object instead of extracting the entire model. On the other hand
// we should encounter this only if the model was created in .slint, which is when it'll be an array
// of values.
let js_array = JsArray::new(cx, model.row_count() as _);
for i in 0..model.row_count() {
let v = to_js_value(model.row_data(i).unwrap(), cx, persistent_context)?;
js_array.set(cx, i as u32, v)?;
}
js_array.as_value(cx)
}
}
Value::Struct(o) => {
let js_object = JsObject::new(cx);
for (k, e) in o.iter() {
let v = to_js_value(e.clone(), cx, persistent_context)?;
js_object.set(cx, k.replace('-', "_").as_str(), v)?;
}
js_object.as_value(cx)
}
Value::Brush(i_slint_core::Brush::SolidColor(c)) => JsString::new(
cx,
&format!("#{:02x}{:02x}{:02x}{:02x}", c.red(), c.green(), c.blue(), c.alpha()),
)
.as_value(cx),
_ => todo!("converting {:?} to js has not been implemented", val),
})
}
declare_types! {
class SlintComponentType for WrappedComponentType {
init(_) {
Ok(WrappedComponentType(None))
}
method create(mut cx) {
let this = cx.this();
let ct = cx.borrow(&this, |x| x.0.clone());
let ct = ct.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
create(&mut cx, ct)
}
method name(mut cx) {
let this = cx.this();
let ct = cx.borrow(&this, |x| x.0.clone());
let ct = ct.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
Ok(cx.string(ct.name()).as_value(&mut cx))
}
method properties(mut cx) {
let this = cx.this();
let ct = cx.borrow(&this, |x| x.0.clone());
let ct = ct.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let properties = ct.properties_and_callbacks().filter(|(_, prop_type)| prop_type.is_property_type());
let array = JsArray::new(&mut cx, 0);
for (len, (p, _)) in properties.enumerate() {
let prop_name = JsString::new(&mut cx, p);
array.set(&mut cx, len as u32, prop_name)?;
}
Ok(array.as_value(&mut cx))
}
method callbacks(mut cx) {
let this = cx.this();
let ct = cx.borrow(&this, |x| x.0.clone());
let ct = ct.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let callbacks = ct.properties_and_callbacks().filter(|(_, prop_type)| matches!(prop_type, Type::Callback{..}));
let array = JsArray::new(&mut cx, 0);
for (len , (p, _)) in callbacks.enumerate() {
let prop_name = JsString::new(&mut cx, p);
array.set(&mut cx, len as u32, prop_name)?;
}
Ok(array.as_value(&mut cx))
}
}
class SlintComponent for WrappedItemTreeRc {
init(_) {
Ok(WrappedItemTreeRc(None))
}
method run(mut cx) {
let this = cx.this();
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
run_scoped(&mut cx,this.downcast().unwrap(), || {
component.run().unwrap();
Ok(())
})?;
Ok(JsUndefined::new().as_value(&mut cx))
}
method window(mut cx) {
let this = cx.this();
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let window_adapter = WindowInner::from_pub(component.window()).window_adapter();
let mut obj = SlintWindow::new::<_, JsValue, _>(&mut cx, std::iter::empty())?;
cx.borrow_mut(&mut obj, |mut obj| obj.0 = Some(window_adapter));
Ok(obj.as_value(&mut cx))
}
method get_property(mut cx) {
let prop_name = cx.argument::<JsString>(0)?.value();
let this = cx.this();
let persistent_context =
persistent_context::PersistentContext::from_object(&mut cx, this.downcast().unwrap())?;
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let value = run_scoped(&mut cx,this.downcast().unwrap(), || {
component.get_property(prop_name.as_str())
.map_err(|_| "Cannot read property".to_string())
})?;
to_js_value(value, &mut cx, &persistent_context)
}
method set_property(mut cx) {
let prop_name = cx.argument::<JsString>(0)?.value();
let this = cx.this();
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let ty = component.definition().properties_and_callbacks()
.find_map(|(name, proptype)| if name == prop_name { Some(proptype) } else { None })
.ok_or(())
.or_else(|()| {
cx.throw_error(format!("Property {} not found in the component", prop_name))
})?;
let persistent_context =
persistent_context::PersistentContext::from_object(&mut cx, this.downcast().unwrap())?;
let value = to_eval_value(cx.argument::<JsValue>(1)?, ty, &mut cx, &persistent_context)?;
run_scoped(&mut cx, this.downcast().unwrap(), || {
component.set_property(prop_name.as_str(), value)
.map_err(|_| "Cannot assign property".to_string())
})?;
Ok(JsUndefined::new().as_value(&mut cx))
}
method invoke_callback(mut cx) {
let callback_name = cx.argument::<JsString>(0)?.value();
let arguments = cx.argument::<JsArray>(1)?.to_vec(&mut cx)?;
let this = cx.this();
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let ty = component.definition().properties_and_callbacks()
.find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None })
.ok_or(())
.or_else(|()| {
cx.throw_error(format!("Callback {} not found in the component", callback_name))
})?;
let persistent_context =
persistent_context::PersistentContext::from_object(&mut cx, this.downcast().unwrap())?;
let args = if let Type::Callback {args, ..} = ty {
let count = args.len();
let args = arguments.into_iter()
.zip(args.into_iter())
.map(|(a, ty)| to_eval_value(a, ty, &mut cx, &persistent_context))
.collect::<Result<Vec<_>, _>>()?;
if args.len() != count {
cx.throw_error(format!("{} expect {} arguments, but {} where provided", callback_name, count, args.len()))?;
}
args
} else {
cx.throw_error(format!("{} is not a callback", callback_name))?;
unreachable!()
};
let res = run_scoped(&mut cx,this.downcast().unwrap(), || {
component.invoke(callback_name.as_str(), args.as_slice())
.map_err(|_| "Cannot emit callback".to_string())
})?;
to_js_value(res, &mut cx, &persistent_context)
}
method connect_callback(mut cx) {
let callback_name = cx.argument::<JsString>(0)?.value();
let handler = cx.argument::<JsFunction>(1)?;
let this = cx.this();
let persistent_context =
persistent_context::PersistentContext::from_object(&mut cx, this.downcast().unwrap())?;
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let ty = component.definition().properties_and_callbacks()
.find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None })
.ok_or(())
.or_else(|()| {
cx.throw_error(format!("Callback {} not found in the component", callback_name))
})?;
if let Type::Callback {return_type, ..} = ty {
component.set_callback(
callback_name.as_str(),
make_callback_handler(&mut cx, &persistent_context, handler, return_type)
).or_else(|_| cx.throw_error("Cannot set callback"))?;
Ok(JsUndefined::new().as_value(&mut cx))
} else {
cx.throw_error(format!("{} is not a callback", callback_name))?;
unreachable!()
}
}
method send_mouse_click(mut cx) {
let x = cx.argument::<JsNumber>(0)?.value() as f32;
let y = cx.argument::<JsNumber>(1)?.value() as f32;
let this = cx.this();
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
run_scoped(&mut cx,this.downcast().unwrap(), || {
slint_interpreter::testing::send_mouse_click(&component, x, y);
Ok(())
})?;
Ok(JsUndefined::new().as_value(&mut cx))
}
method send_keyboard_string_sequence(mut cx) {
let sequence = cx.argument::<JsString>(0)?.value();
let this = cx.this();
let component = cx.borrow(&this, |x| x.0.as_ref().map(|c| c.clone_strong()));
let component = component.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
run_scoped(&mut cx,this.downcast().unwrap(), || {
slint_interpreter::testing::send_keyboard_string_sequence(&component, sequence.into());
Ok(())
})?;
Ok(JsUndefined::new().as_value(&mut cx))
}
}
class SlintWindow for WrappedWindow {
init(_) {
Ok(WrappedWindow(None))
}
method show(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
window_adapter.window().show().unwrap();
Ok(JsUndefined::new().as_value(&mut cx))
}
method hide(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
window_adapter.window().hide().unwrap();
Ok(JsUndefined::new().as_value(&mut cx))
}
method get_is_visible(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
Ok(JsBoolean::new(&mut cx, window_adapter.window().is_visible()).as_value(&mut cx))
}
method get_logical_position(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let pos = window_adapter.position().unwrap_or_default().to_logical(window_adapter.window().scale_factor());
let point_object = JsObject::new(&mut cx);
let x_value = JsNumber::new(&mut cx, pos.x).as_value(&mut cx);
point_object.set(&mut cx, "x", x_value)?;
let y_value = JsNumber::new(&mut cx, pos.y).as_value(&mut cx);
point_object.set(&mut cx, "y", y_value)?;
Ok(point_object.as_value(&mut cx))
}
method get_physical_position(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let pos = window_adapter.position().unwrap_or_default();
let point_object = JsObject::new(&mut cx);
let x_value = JsNumber::new(&mut cx, pos.x).as_value(&mut cx);
point_object.set(&mut cx, "x", x_value)?;
let y_value = JsNumber::new(&mut cx, pos.y).as_value(&mut cx);
point_object.set(&mut cx, "y", y_value)?;
Ok(point_object.as_value(&mut cx))
}
method set_logical_position(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let point_object = cx.argument::<JsObject>(0)?;
let x = point_object.get(&mut cx, "x")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
let y = point_object.get(&mut cx, "y")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
window_adapter.set_position(i_slint_core::api::LogicalPosition::new(x as f32, y as f32).into());
Ok(JsUndefined::new().as_value(&mut cx))
}
method set_physical_position(mut cx) {
let this = cx.this();
let window = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let point_object = cx.argument::<JsObject>(0)?;
let x = point_object.get(&mut cx, "x")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
let y = point_object.get(&mut cx, "y")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
window_adapter.set_position(i_slint_core::api::PhysicalPosition::new(x as i32, y as i32).into());
Ok(JsUndefined::new().as_value(&mut cx))
}
method get_logical_size(mut cx) {
let this = cx.this();
let window_adapter = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window_adapter.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let size = window_adapter.window().size().to_logical(window_adapter.window().scale_factor());
let size_object = JsObject::new(&mut cx);
let width_value = JsNumber::new(&mut cx, size.width).as_value(&mut cx);
size_object.set(&mut cx, "width", width_value)?;
let height_value = JsNumber::new(&mut cx, size.height).as_value(&mut cx);
size_object.set(&mut cx, "height", height_value)?;
Ok(size_object.as_value(&mut cx))
}
method get_physical_size(mut cx) {
let this = cx.this();
let window_adapter = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window_adapter.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let size = window_adapter.window().size();
let size_object = JsObject::new(&mut cx);
let width_value = JsNumber::new(&mut cx, size.width).as_value(&mut cx);
size_object.set(&mut cx, "width", width_value)?;
let height_value = JsNumber::new(&mut cx, size.height).as_value(&mut cx);
size_object.set(&mut cx, "height", height_value)?;
Ok(size_object.as_value(&mut cx))
}
method set_logical_size(mut cx) {
let this = cx.this();
let window_adapter = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window_adapter.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let window = window_adapter.window();
let size_object = cx.argument::<JsObject>(0)?;
let width = size_object.get(&mut cx, "width")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
let height = size_object.get(&mut cx, "height")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
window.set_size(i_slint_core::api::LogicalSize::new(width as f32, height as f32));
Ok(JsUndefined::new().as_value(&mut cx))
}
method set_physical_size(mut cx) {
let this = cx.this();
let window_adapter = cx.borrow(&this, |x| x.0.as_ref().cloned());
let window_adapter = window_adapter.ok_or(()).or_else(|()| cx.throw_error("Invalid type"))?;
let window = window_adapter.window();
let size_object = cx.argument::<JsObject>(0)?;
let width = size_object.get(&mut cx, "width")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
let height = size_object.get(&mut cx, "height")?.downcast_or_throw::<JsNumber, _>(&mut cx)?.value();
window.set_size(i_slint_core::api::PhysicalSize::new(width as u32, height as u32));
Ok(JsUndefined::new().as_value(&mut cx))
}
}
}
fn singleshot_timer_property(id: u32) -> String {
format!("$__slint_singleshot_timer_{}", id)
}
fn singleshot_timer(mut cx: FunctionContext) -> JsResult<JsValue> {
let duration_in_msecs = cx.argument::<JsNumber>(0)?.value() as u64;
let handler = cx.argument::<JsFunction>(1)?;
let global_object: Handle<JsObject> = cx.global().downcast().unwrap();
let unique_timer_property = {
let mut rng = rand::thread_rng();
loop {
let id = rng.next_u32();
let key = singleshot_timer_property(id);
if global_object.get(&mut cx, &*key)?.is_a::<JsUndefined>() {
break key;
}
}
};
let handler_value = handler.as_value(&mut cx);
global_object.set(&mut cx, &*unique_timer_property, handler_value).unwrap();
let callback = move || {
run_with_global_context(&move |cx, _| {
let global_object: Handle<JsObject> = cx.global().downcast().unwrap();
let callback = global_object
.get(cx, &*unique_timer_property)
.unwrap()
.downcast::<JsFunction>()
.unwrap();
global_object.set(cx, &*unique_timer_property, JsUndefined::new()).unwrap();
callback.call::<_, _, JsValue, _>(cx, JsUndefined::new(), vec![]).unwrap();
});
};
i_slint_core::timers::Timer::single_shot(
std::time::Duration::from_millis(duration_in_msecs),
callback,
);
Ok(JsUndefined::new().upcast())
}
register_module!(mut m, {
m.export_function("load", load)?;
m.export_function("mock_elapsed_time", mock_elapsed_time)?;
m.export_function("singleshot_timer", singleshot_timer)?;
Ok(())
});
/// let some time elapse for testing purposes
fn mock_elapsed_time(mut cx: FunctionContext) -> JsResult<JsValue> {
let ms = cx.argument::<JsNumber>(0)?.value();
i_slint_core::tests::slint_mock_elapsed_time(ms as _);
Ok(JsUndefined::new().as_value(&mut cx))
}

View file

@ -1,37 +0,0 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
/*!
Since neon does not allow to have a persistent handle, use this hack.
*/
use neon::prelude::*;
pub struct PersistentContext<'a>(Handle<'a, JsArray>);
const KEY: &str = "$__persistent_context";
/// Since neon does not allow to have a persistent handle, this allocates property in an array.
/// This array is gonna be kept as a property somewhere.
impl<'a> PersistentContext<'a> {
pub fn new(cx: &mut impl Context<'a>) -> Self {
PersistentContext(JsArray::new(cx, 0))
}
pub fn allocate(&self, cx: &mut impl Context<'a>, value: Handle<'a, JsValue>) -> u32 {
let idx = self.0.len();
self.0.set(cx, idx, value).unwrap();
idx
}
pub fn get(&self, cx: &mut impl Context<'a>, idx: u32) -> JsResult<'a, JsValue> {
self.0.get(cx, idx)
}
pub fn save_to_object(&self, cx: &mut impl Context<'a>, o: Handle<'a, JsObject>) {
o.set(cx, KEY, self.0).unwrap();
}
pub fn from_object(cx: &mut impl Context<'a>, o: Handle<'a, JsObject>) -> NeonResult<Self> {
Ok(PersistentContext(o.get(cx, KEY)?.downcast_or_throw(cx)?))
}
}

View file

@ -1,25 +1,48 @@
{
"name": "slint-ui",
"version": "1.3.0",
"homepage": "https://github.com/slint-ui/slint",
"license": "SEE LICENSE IN LICENSE.md",
"repository": {
"type": "git",
"url": "https://github.com/slint-ui/slint"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@types/node": "^14.11.11",
"neon-cli": "^0.4",
"typescript": "^4.0.3"
},
"scripts": {
"install": "neon build --release && tsc",
"build": "tsc",
"docs": "typedoc --hideGenerator --readme cover.md lib/index.ts"
},
"devDependencies": {
"typedoc": "^0.19.2"
"name": "slint-ui",
"version": "1.3.0",
"main": "index.js",
"types": "index.d.ts",
"homepage": "https://github.com/slint-ui/slint",
"license": "SEE LICENSE IN LICENSE.md",
"repository": {
"type": "git",
"url": "https://github.com/slint-ui/slint"
},
"devDependencies": {
"@napi-rs/cli": "^2.15.2",
"@swc-node/register": "^1.5.5",
"@swc/core": "^1.3.32",
"@types/node": "^20.8.6",
"ava": "^5.3.0",
"esbuild": "^0.14.54",
"jimp": "^0.22.8",
"typedoc": "^0.25.2"
},
"engines": {
"node": ">= 10"
},
"scripts": {
"artifacts": "napi artifacts",
"compile": "esbuild index.ts --bundle --external:*.node --format=cjs --platform=node --outfile=index.js",
"build": "napi build --platform --release --js rust-module.js --dts rust-module.d.ts && npm run compile",
"build:debug": "napi build --platform --js rust-module.js --dts rust-module.d.ts && npm run compile && npm run syntax_check",
"install": "npm run build",
"docs": "npm run build && typedoc --hideGenerator --treatWarningsAsErrors --readme cover.md index.ts",
"test": "ava",
"syntax_check": "tsc -noEmit index.ts"
},
"ava": {
"require": [
"@swc-node/register"
],
"extensions": [
"ts"
],
"timeout": "2m",
"workerThreads": false,
"environmentVariables": {
"TS_NODE_PROJECT": "./tsconfig.json"
}
}
}

View file

@ -0,0 +1,20 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
mod diagnostic;
pub use diagnostic::*;
mod component_compiler;
pub use component_compiler::*;
mod component_definition;
pub use component_definition::*;
mod component_instance;
pub use component_instance::*;
mod value;
pub use value::*;
mod window;
pub use window::*;

View file

@ -0,0 +1,109 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use std::collections::HashMap;
use std::path::PathBuf;
use super::JsComponentDefinition;
use super::JsDiagnostic;
use itertools::Itertools;
use slint_interpreter::ComponentCompiler;
/// ComponentCompiler is the entry point to the Slint interpreter that can be used
/// to load .slint files or compile them on-the-fly from a string.
#[napi(js_name = "ComponentCompiler")]
pub struct JsComponentCompiler {
internal: ComponentCompiler,
}
#[napi]
impl JsComponentCompiler {
/// Returns a new ComponentCompiler.
#[napi(constructor)]
pub fn new() -> Self {
let mut compiler = ComponentCompiler::default();
let include_paths = match std::env::var_os("SLINT_INCLUDE_PATH") {
Some(paths) => {
std::env::split_paths(&paths).filter(|path| !path.as_os_str().is_empty()).collect()
}
None => vec![],
};
let library_paths = match std::env::var_os("SLINT_LIBRARY_PATH") {
Some(paths) => std::env::split_paths(&paths)
.filter_map(|entry| {
entry
.to_str()
.unwrap_or_default()
.split('=')
.collect_tuple()
.map(|(k, v)| (k.into(), v.into()))
})
.collect(),
None => std::collections::HashMap::new(),
};
compiler.set_include_paths(include_paths);
compiler.set_library_paths(library_paths);
Self { internal: compiler }
}
#[napi(setter)]
pub fn set_include_paths(&mut self, include_paths: Vec<String>) {
self.internal.set_include_paths(include_paths.iter().map(|p| PathBuf::from(p)).collect());
}
#[napi(getter)]
pub fn include_paths(&self) -> Vec<String> {
self.internal
.include_paths()
.iter()
.map(|p| p.to_str().unwrap_or_default().to_string())
.collect()
}
#[napi(setter)]
pub fn set_library_paths(&mut self, paths: HashMap<String, String>) {
let mut library_paths = HashMap::new();
for (key, path) in paths {
library_paths.insert(key, PathBuf::from(path));
}
self.internal.set_library_paths(library_paths);
}
#[napi(setter)]
pub fn set_style(&mut self, style: String) {
self.internal.set_style(style);
}
#[napi(getter)]
pub fn style(&self) -> Option<String> {
self.internal.style().cloned()
}
// todo: set_file_loader
#[napi(getter)]
pub fn diagnostics(&self) -> Vec<JsDiagnostic> {
self.internal.diagnostics().iter().map(|d| JsDiagnostic::from(d.clone())).collect()
}
/// Compile a .slint file into a ComponentDefinition
///
/// Returns the compiled `ComponentDefinition` if there were no errors.
#[napi]
pub fn build_from_path(&mut self, path: String) -> Option<JsComponentDefinition> {
spin_on::spin_on(self.internal.build_from_path(PathBuf::from(path))).map(|d| d.into())
}
/// Compile some .slint code into a ComponentDefinition
#[napi]
pub fn build_from_source(
&mut self,
source_code: String,
path: String,
) -> Option<JsComponentDefinition> {
spin_on::spin_on(self.internal.build_from_source(source_code, PathBuf::from(path)))
.map(|d| d.into())
}
}

View file

@ -0,0 +1,72 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use slint_interpreter::ComponentDefinition;
use super::{JsComponentInstance, JsProperty};
#[napi(js_name = "ComponentDefinition")]
pub struct JsComponentDefinition {
internal: ComponentDefinition,
}
impl From<ComponentDefinition> for JsComponentDefinition {
fn from(definition: ComponentDefinition) -> Self {
Self { internal: definition }
}
}
#[napi]
impl JsComponentDefinition {
#[napi(constructor)]
pub fn new() -> napi::Result<Self> {
Err(napi::Error::from_reason(
"ComponentDefinition can only be created by using ComponentCompiler.".to_string(),
))
}
#[napi(getter)]
pub fn properties(&self) -> Vec<JsProperty> {
self.internal
.properties()
.map(|(name, value_type)| JsProperty { name, value_type: value_type.into() })
.collect()
}
#[napi(getter)]
pub fn callbacks(&self) -> Vec<String> {
self.internal.callbacks().collect()
}
#[napi(getter)]
pub fn globals(&self) -> Vec<String> {
self.internal.globals().collect()
}
#[napi]
pub fn global_properties(&self, global_name: String) -> Option<Vec<JsProperty>> {
self.internal.global_properties(global_name.as_str()).map(|iter| {
iter.map(|(name, value_type)| JsProperty { name, value_type: value_type.into() })
.collect()
})
}
#[napi]
pub fn global_callbacks(&self, global_name: String) -> Option<Vec<String>> {
self.internal.global_callbacks(global_name.as_str()).map(|iter| iter.collect())
}
#[napi]
pub fn create(&self) -> Option<JsComponentInstance> {
if let Ok(instance) = self.internal.create() {
return Some(instance.into());
}
None
}
#[napi(getter)]
pub fn name(&self) -> String {
self.internal.name().into()
}
}

View file

@ -0,0 +1,372 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use i_slint_compiler::langtype::Type;
use i_slint_core::window::WindowInner;
use napi::{Env, Error, JsFunction, JsUnknown, NapiRaw, NapiValue, Ref, Result};
use slint_interpreter::{ComponentHandle, ComponentInstance, Value};
use crate::JsWindow;
use super::JsComponentDefinition;
#[napi(js_name = "ComponentInstance")]
pub struct JsComponentInstance {
inner: ComponentInstance,
}
impl From<ComponentInstance> for JsComponentInstance {
fn from(instance: ComponentInstance) -> Self {
Self { inner: instance }
}
}
#[napi]
impl JsComponentInstance {
#[napi(constructor)]
pub fn new() -> napi::Result<Self> {
Err(napi::Error::from_reason(
"ComponentInstance can only be created by using ComponentCompiler.".to_string(),
))
}
#[napi]
pub fn definition(&self) -> JsComponentDefinition {
self.inner.definition().into()
}
#[napi]
pub fn run(&self) {
self.inner.run().unwrap()
}
#[napi]
pub fn get_property(&self, env: Env, name: String) -> Result<JsUnknown> {
let value = self
.inner
.get_property(name.as_ref())
.map_err(|e| Error::from_reason(e.to_string()))?;
super::value::to_js_unknown(&env, &value)
}
#[napi]
pub fn set_property(&self, env: Env, prop_name: String, js_value: JsUnknown) -> Result<()> {
let ty = self
.inner
.definition()
.properties_and_callbacks()
.find_map(|(name, proptype)| if name == prop_name { Some(proptype) } else { None })
.ok_or(())
.map_err(|_| {
napi::Error::from_reason(format!("Property {prop_name} not found in the component"))
})?;
self.inner
.set_property(&prop_name, super::value::to_value(&env, js_value, ty)?)
.map_err(|e| Error::from_reason(format!("{e}")))?;
Ok(())
}
#[napi]
pub fn get_global_property(
&self,
env: Env,
global_name: String,
name: String,
) -> Result<JsUnknown> {
if !self.definition().globals().contains(&global_name) {
return Err(napi::Error::from_reason(format!("Global {global_name} not found")));
}
let value = self
.inner
.get_global_property(global_name.as_ref(), name.as_ref())
.map_err(|e| Error::from_reason(e.to_string()))?;
super::value::to_js_unknown(&env, &value)
}
#[napi]
pub fn set_global_property(
&self,
env: Env,
global_name: String,
prop_name: String,
js_value: JsUnknown,
) -> Result<()> {
let ty = self
.inner
.definition()
.global_properties_and_callbacks(global_name.as_str())
.ok_or(napi::Error::from_reason(format!("Global {global_name} not found")))?
.find_map(|(name, proptype)| if name == prop_name { Some(proptype) } else { None })
.ok_or(())
.map_err(|_| {
napi::Error::from_reason(format!(
"Property {prop_name} of global {global_name} not found in the component"
))
})?;
self.inner
.set_global_property(
global_name.as_str(),
&prop_name,
super::value::to_value(&env, js_value, ty)?,
)
.map_err(|e| Error::from_reason(format!("{e}")))?;
Ok(())
}
#[napi]
pub fn set_callback(
&self,
env: Env,
callback_name: String,
callback: JsFunction,
) -> Result<()> {
let function_ref = RefCountedReference::new(&env, callback)?;
let ty = self
.inner
.definition()
.properties_and_callbacks()
.find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None })
.ok_or(())
.map_err(|_| {
napi::Error::from_reason(format!(
"Callback {callback_name} not found in the component"
))
})?;
if let Type::Callback { return_type, .. } = ty {
self.inner
.set_callback(callback_name.as_str(), {
let return_type = return_type.clone();
move |args| {
let callback: JsFunction = function_ref.get().unwrap();
let result = callback
.call(
None,
args.iter()
.map(|v| super::value::to_js_unknown(&env, v).unwrap())
.collect::<Vec<JsUnknown>>()
.as_ref(),
)
.unwrap();
if let Some(return_type) = &return_type {
super::to_value(&env, result, *(*return_type).clone()).unwrap()
} else {
Value::Void
}
}
})
.map_err(|_| napi::Error::from_reason("Cannot set callback."))?;
return Ok(());
}
Err(napi::Error::from_reason(format!("{} is not a callback", callback_name).as_str()))
}
#[napi]
pub fn set_global_callback(
&self,
env: Env,
global_name: String,
callback_name: String,
callback: JsFunction,
) -> Result<()> {
let function_ref = RefCountedReference::new(&env, callback)?;
let ty = self
.inner
.definition()
.global_properties_and_callbacks(global_name.as_str())
.ok_or(napi::Error::from_reason(format!("Global {global_name} not found")))?
.find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None })
.ok_or(())
.map_err(|_| {
napi::Error::from_reason(format!(
"Callback {callback_name} of global {global_name} not found in the component"
))
})?;
if let Type::Callback { return_type, .. } = ty {
self.inner
.set_global_callback(global_name.as_str(), callback_name.as_str(), {
let return_type = return_type.clone();
move |args| {
let callback: JsFunction = function_ref.get().unwrap();
let result = callback
.call(
None,
args.iter()
.map(|v| super::value::to_js_unknown(&env, v).unwrap())
.collect::<Vec<JsUnknown>>()
.as_ref(),
)
.unwrap();
if let Some(return_type) = &return_type {
super::to_value(&env, result, *(*return_type).clone()).unwrap()
} else {
Value::Void
}
}
})
.map_err(|_| napi::Error::from_reason("Cannot set callback."))?;
return Ok(());
}
Err(napi::Error::from_reason(format!("{} is not a callback", callback_name).as_str()))
}
#[napi]
pub fn invoke(
&self,
env: Env,
callback_name: String,
arguments: Vec<JsUnknown>,
) -> Result<JsUnknown> {
let ty = self
.inner
.definition()
.properties_and_callbacks()
.find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None })
.ok_or(())
.map_err(|_| {
napi::Error::from_reason(
format!("Callback {} not found in the component", callback_name).as_str(),
)
})?;
let args = if let Type::Callback { args, .. } = ty {
let count = args.len();
let args = arguments
.into_iter()
.zip(args.into_iter())
.map(|(a, ty)| super::value::to_value(&env, a, ty))
.collect::<Result<Vec<_>, _>>()?;
if args.len() != count {
return Err(napi::Error::from_reason(
format!(
"{} expect {} arguments, but {} where provided",
callback_name,
count,
args.len()
)
.as_str(),
));
}
args
} else {
return Err(napi::Error::from_reason(
format!("{} is not a callback", callback_name).as_str(),
));
};
let result = self
.inner
.invoke(callback_name.as_str(), args.as_slice())
.map_err(|_| napi::Error::from_reason("Cannot invoke callback."))?;
super::to_js_unknown(&env, &result)
}
#[napi]
pub fn invoke_global(
&self,
env: Env,
global_name: String,
callback_name: String,
arguments: Vec<JsUnknown>,
) -> Result<JsUnknown> {
let ty = self
.inner
.definition()
.global_properties_and_callbacks(global_name.as_str())
.ok_or(napi::Error::from_reason(format!("Global {global_name} not found")))?
.find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None })
.ok_or(())
.map_err(|_| {
napi::Error::from_reason(
format!(
"Callback {} of global {global_name} not found in the component",
callback_name
)
.as_str(),
)
})?;
let args = if let Type::Callback { args, .. } = ty {
let count = args.len();
let args = arguments
.into_iter()
.zip(args.into_iter())
.map(|(a, ty)| super::value::to_value(&env, a, ty))
.collect::<Result<Vec<_>, _>>()?;
if args.len() != count {
return Err(napi::Error::from_reason(
format!(
"{} expect {} arguments, but {} where provided",
callback_name,
count,
args.len()
)
.as_str(),
));
}
args
} else {
return Err(napi::Error::from_reason(
format!("{} is not a callback on global {}", callback_name, global_name).as_str(),
));
};
let result = self
.inner
.invoke_global(global_name.as_str(), callback_name.as_str(), args.as_slice())
.map_err(|_| napi::Error::from_reason("Cannot invoke callback."))?;
super::to_js_unknown(&env, &result)
}
#[napi]
pub fn send_mouse_click(&self, x: f64, y: f64) {
slint_interpreter::testing::send_mouse_click(&self.inner, x as f32, y as f32);
}
#[napi]
pub fn send_keyboard_string_sequence(&self, sequence: String) {
slint_interpreter::testing::send_keyboard_string_sequence(&self.inner, sequence.into());
}
#[napi]
pub fn window(&self) -> Result<JsWindow> {
Ok(JsWindow { inner: WindowInner::from_pub(self.inner.window()).window_adapter() })
}
}
// Wrapper around Ref<>, which requires manual ref-counting.
pub struct RefCountedReference {
env: Env,
reference: Ref<()>,
}
impl RefCountedReference {
pub fn new<T: NapiRaw>(env: &Env, value: T) -> Result<Self> {
Ok(Self { env: env.clone(), reference: env.create_reference(value)? })
}
pub fn get<T: NapiValue>(&self) -> Result<T> {
self.env.get_reference_value(&self.reference)
}
}
impl Drop for RefCountedReference {
fn drop(&mut self) {
self.reference.unref(self.env).unwrap();
}
}

View file

@ -0,0 +1,62 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use slint_interpreter::{Diagnostic, DiagnosticLevel};
/// This enum describes the level or severity of a diagnostic message produced by the compiler.
#[napi(js_name = "DiagnosticLevel")]
pub enum JsDiagnosticLevel {
/// The diagnostic found is an error that prevents successful compilation.
Error,
/// The diagnostic found is a warning.
Warning,
}
impl From<DiagnosticLevel> for JsDiagnosticLevel {
fn from(diagnostic_level: DiagnosticLevel) -> Self {
match diagnostic_level {
DiagnosticLevel::Warning => JsDiagnosticLevel::Warning,
_ => JsDiagnosticLevel::Error,
}
}
}
/// This structure represent a diagnostic emitted while compiling .slint code.
///
/// It is basically a message, a level (warning or error), attached to a
/// position in the code.
#[napi(object, js_name = "Diagnostic")]
pub struct JsDiagnostic {
/// The level for this diagnostic.
pub level: JsDiagnosticLevel,
/// Message for this diagnostic.
pub message: String,
/// The line number in the .slint source file.
pub line_number: u32,
// The column in the .slint source file
pub column: u32,
/// The path of the source file where this diagnostic occurred.
pub source_file: Option<String>,
}
impl From<Diagnostic> for JsDiagnostic {
fn from(internal_diagnostic: Diagnostic) -> Self {
let (line_number, column) = internal_diagnostic.line_column();
Self {
level: internal_diagnostic.level().into(),
message: internal_diagnostic.message().into(),
line_number: line_number as u32,
column: column as u32,
source_file: internal_diagnostic
.source_file()
.and_then(|path| path.to_str())
.map(|str| str.into()),
}
}
}

View file

@ -0,0 +1,274 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use crate::{JsBrush, JsImageData, JsModel};
use i_slint_compiler::langtype::Type;
use i_slint_core::graphics::{Image, Rgba8Pixel, SharedPixelBuffer};
use i_slint_core::model::{Model, ModelRc, SharedVectorModel};
use i_slint_core::{Brush, Color, SharedVector};
use napi::{bindgen_prelude::*, Env, JsBoolean, JsNumber, JsObject, JsString, JsUnknown, Result};
use napi_derive::napi;
use slint_interpreter::Value;
#[napi(js_name = "ValueType")]
pub enum JsValueType {
Void,
Number,
String,
Bool,
Model,
Struct,
Brush,
Image,
}
impl From<slint_interpreter::ValueType> for JsValueType {
fn from(value_type: slint_interpreter::ValueType) -> Self {
match value_type {
slint_interpreter::ValueType::Number => JsValueType::Number,
slint_interpreter::ValueType::String => JsValueType::String,
slint_interpreter::ValueType::Bool => JsValueType::Bool,
slint_interpreter::ValueType::Model => JsValueType::Model,
slint_interpreter::ValueType::Struct => JsValueType::Struct,
slint_interpreter::ValueType::Brush => JsValueType::Brush,
slint_interpreter::ValueType::Image => JsValueType::Image,
_ => JsValueType::Void,
}
}
}
#[napi(js_name = "Property")]
pub struct JsProperty {
pub name: String,
pub value_type: JsValueType,
}
pub fn to_js_unknown(env: &Env, value: &Value) -> Result<JsUnknown> {
match value {
Value::Void => env.get_null().map(|v| v.into_unknown()),
Value::Number(number) => env.create_double(*number).map(|v| v.into_unknown()),
Value::String(string) => env.create_string(string).map(|v| v.into_unknown()),
Value::Bool(value) => env.get_boolean(*value).map(|v| v.into_unknown()),
Value::Image(image) => {
Ok(JsImageData::from(image.clone()).into_instance(*env)?.as_object(*env).into_unknown())
}
Value::Struct(struct_value) => {
let mut o = env.create_object()?;
for (field_name, field_value) in struct_value.iter() {
o.set_property(
env.create_string(&field_name.replace('-', "_"))?,
to_js_unknown(env, field_value)?,
)?;
}
Ok(o.into_unknown())
}
Value::Brush(brush) => {
Ok(JsBrush::from(brush.clone()).into_instance(*env)?.as_object(*env).into_unknown())
}
Value::Model(model) => {
if let Some(js_model) = model.as_any().downcast_ref::<JsModel>() {
let model: Object = js_model.model().get()?;
Ok(model.into_unknown())
} else {
let mut vec = vec![];
for i in 0..model.row_count() {
vec.push(to_js_unknown(env, &model.row_data(i).unwrap())?);
}
Ok(Array::from_vec(env, vec)?.coerce_to_object()?.into_unknown())
}
}
_ => env.get_undefined().map(|v| v.into_unknown()),
}
}
pub fn to_value(env: &Env, unknown: JsUnknown, typ: Type) -> Result<Value> {
match typ {
Type::Float32
| Type::Int32
| Type::Duration
| Type::Angle
| Type::PhysicalLength
| Type::LogicalLength
| Type::Rem
| Type::Percent
| Type::UnitProduct(_) => {
let js_number: Result<JsNumber> = unknown.try_into();
Ok(Value::Number(js_number?.get_double()?))
}
Type::String => {
let js_string: JsString = unknown.try_into()?;
Ok(Value::String(js_string.into_utf8()?.as_str()?.into()))
}
Type::Bool => {
let js_bool: JsBoolean = unknown.try_into()?;
Ok(Value::Bool(js_bool.get_value()?))
}
Type::Color => {
let color_ref = env.create_reference(unknown.coerce_to_object()?)?;
if let Some(js_color) = env
.get_reference_value::<JsObject>(&color_ref)
.ok()
.and_then(|obj| obj.get("color").ok().flatten())
.and_then(|brush_prop| env.get_value_external::<Brush>(&brush_prop).ok())
{
return Ok(Value::Brush(js_color.clone()));
}
if let Some(js_brush) = env
.get_reference_value::<JsObject>(&color_ref)
.ok()
.and_then(|js_object| js_object.coerce_to_string().ok())
.and_then(|string| string_to_brush(string).ok())
{
return Ok(js_brush);
} else {
return Err(napi::Error::from_reason(
"Cannot convert object to brush, because the given object is neither a brush nor a string".to_string()
));
}
}
Type::Brush => {
let brush_ref = env.create_reference(unknown.coerce_to_object()?)?;
if let Some(js_brush) = env
.get_reference_value::<JsObject>(&brush_ref)
.ok()
.and_then(|obj| obj.get("brush").ok().flatten())
.and_then(|brush_prop| env.get_value_external::<Brush>(&brush_prop).ok())
{
return Ok(Value::Brush(js_brush.clone()));
}
if let Some(js_brush) = env
.get_reference_value::<JsObject>(&brush_ref)
.ok()
.and_then(|js_object| js_object.coerce_to_string().ok())
.and_then(|string| string_to_brush(string).ok())
{
return Ok(js_brush);
} else {
return Err(napi::Error::from_reason(
"Cannot convert object to brush, because the given object is neither a brush nor a string".to_string()
));
}
}
Type::Image => {
let object = unknown.coerce_to_object()?;
if let Some(direct_image) = object.get("image").ok().flatten() {
Ok(Value::Image(env.get_value_external::<Image>(&direct_image)?.clone()))
} else {
let get_size_prop = |name| {
object
.get::<_, JsUnknown>(name)
.ok()
.flatten()
.and_then(|prop| prop.coerce_to_number().ok())
.and_then(|number| number.get_int64().ok())
.and_then(|i64_num| i64_num.try_into().ok())
.ok_or_else(
|| napi::Error::from_reason(
format!("Cannot convert object to image, because the provided object does not have an u32 `{name}` property")
))
};
fn try_convert_image<BufferType: AsRef<[u8]> + FromNapiValue>(
object: &JsObject,
width: u32,
height: u32,
) -> Result<SharedPixelBuffer<Rgba8Pixel>> {
let buffer =
object.get::<_, BufferType>("data").ok().flatten().ok_or_else(|| {
napi::Error::from_reason(
"data property does not have suitable array buffer type"
.to_string(),
)
})?;
const BPP: usize = core::mem::size_of::<Rgba8Pixel>();
let actual_size = buffer.as_ref().len();
let expected_size: usize = (width as usize) * (height as usize) * BPP;
if actual_size != expected_size {
return Err(napi::Error::from_reason(format!(
"data property does not have the correct size; expected {} (width) * {} (height) * {} = {}; got {}",
width, height, BPP, actual_size, expected_size
)));
}
Ok(SharedPixelBuffer::clone_from_slice(buffer.as_ref(), width, height))
}
let width: u32 = get_size_prop("width")?;
let height: u32 = get_size_prop("height")?;
let pixel_buffer =
try_convert_image::<Uint8ClampedArray>(&object, width, height)
.or_else(|_| try_convert_image::<Buffer>(&object, width, height))?;
Ok(Value::Image(Image::from_rgba8(pixel_buffer)))
}
}
Type::Struct { fields, name: _, node: _, rust_attributes: _ } => {
let js_object = unknown.coerce_to_object()?;
Ok(Value::Struct(
fields
.iter()
.map(|(pro_name, pro_ty)| {
Ok((
pro_name.clone(),
to_value(
env,
js_object.get_property(
env.create_string(&pro_name.replace('-', "_"))?,
)?,
pro_ty.clone(),
)?,
))
})
.collect::<Result<_, _>>()?,
))
}
Type::Array(a) => {
if unknown.is_array()? {
let array = Array::from_unknown(unknown)?;
let mut vec = vec![];
for i in 0..array.len() {
vec.push(to_value(env, array.get(i)?.unwrap(), *a.to_owned())?);
}
Ok(Value::Model(ModelRc::new(SharedVectorModel::from(SharedVector::from_slice(
&vec,
)))))
} else {
let model = unknown.coerce_to_object()?;
let _: JsFunction = model.get("rowCount")?.unwrap();
let _: JsFunction = model.get("rowData")?.unwrap();
Ok(Value::Model(ModelRc::new(JsModel::new(*env, model, *a.to_owned())?)))
}
}
Type::Enumeration(_) => todo!(),
Type::Invalid
| Type::Model
| Type::Void
| Type::InferredProperty
| Type::InferredCallback
| Type::Function { .. }
| Type::Callback { .. }
| Type::ComponentFactory { .. }
| Type::Easing
| Type::PathData
| Type::LayoutCache
| Type::ElementReference => Err(napi::Error::from_reason("reason")),
}
}
fn string_to_brush(js_string: JsString) -> Result<Value> {
let string = js_string.into_utf8()?.as_str()?.to_string();
let c = string
.parse::<css_color_parser2::Color>()
.map_err(|_| napi::Error::from_reason(format!("Could not convert {string} to Brush.")))?;
Ok(Value::Brush(Brush::from(Color::from_argb_u8((c.a * 255.) as u8, c.r, c.g, c.b)).into()))
}

View file

@ -0,0 +1,120 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use crate::types::{JsPoint, JsSize};
use i_slint_core::window::WindowAdapterRc;
use slint_interpreter::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
/// This type represents a window towards the windowing system, that's used to render the
/// scene of a component. It provides API to control windowing system specific aspects such
/// as the position on the screen.
#[napi(js_name = "Window")]
pub struct JsWindow {
pub(crate) inner: WindowAdapterRc,
}
impl From<WindowAdapterRc> for JsWindow {
fn from(instance: WindowAdapterRc) -> Self {
Self { inner: instance }
}
}
#[napi]
impl JsWindow {
/// @hidden
#[napi(constructor)]
pub fn new() -> napi::Result<Self> {
Err(napi::Error::from_reason(
"Window can only be created by using a Component.".to_string(),
))
}
/// Shows the window on the screen. An additional strong reference on the
/// associated component is maintained while the window is visible.
#[napi]
pub fn show(&self) -> napi::Result<()> {
self.inner
.window()
.show()
.map_err(|_| napi::Error::from_reason("Cannot show window.".to_string()))
}
/// Hides the window, so that it is not visible anymore.
#[napi]
pub fn hide(&self) -> napi::Result<()> {
self.inner
.window()
.hide()
.map_err(|_| napi::Error::from_reason("Cannot hide window.".to_string()))
}
/// Returns the visibility state of the window. This function can return false even if you previously called show()
/// on it, for example if the user minimized the window.
#[napi(getter, js_name = "is_visible")]
pub fn is_visible(&self) -> bool {
self.inner.window().is_visible()
}
/// Returns the logical position of the window on the screen.
#[napi(getter)]
pub fn get_logical_position(&self) -> JsPoint {
let pos = self.inner.window().position().to_logical(self.inner.window().scale_factor());
JsPoint { x: pos.x as f64, y: pos.y as f64 }
}
/// Sets the logical position of the window on the screen.
#[napi(setter)]
pub fn set_logical_position(&self, position: JsPoint) {
self.inner
.window()
.set_position(LogicalPosition { x: position.x as f32, y: position.y as f32 });
}
/// Returns the physical position of the window on the screen.
#[napi(getter)]
pub fn get_physical_position(&self) -> JsPoint {
let pos = self.inner.window().position();
JsPoint { x: pos.x as f64, y: pos.y as f64 }
}
/// Sets the physical position of the window on the screen.
#[napi(setter)]
pub fn set_physical_position(&self, position: JsPoint) {
self.inner.window().set_position(PhysicalPosition {
x: position.x.floor() as i32,
y: position.y.floor() as i32,
});
}
/// Returns the logical size of the window on the screen,
#[napi(getter)]
pub fn get_logical_size(&self) -> JsSize {
let size = self.inner.window().size().to_logical(self.inner.window().scale_factor());
JsSize { width: size.width as f64, height: size.height as f64 }
}
/// Sets the logical size of the window on the screen,
#[napi(setter)]
pub fn set_logical_size(&self, size: JsSize) {
self.inner.window().set_size(LogicalSize::from_physical(
PhysicalSize { width: size.width.floor() as u32, height: size.height.floor() as u32 },
self.inner.window().scale_factor(),
));
}
/// Returns the physical size of the window on the screen,
#[napi(getter)]
pub fn get_physical_size(&self) -> JsSize {
let size = self.inner.window().size();
JsSize { width: size.width as f64, height: size.height as f64 }
}
/// Sets the logical size of the window on the screen,
#[napi(setter)]
pub fn set_physical_size(&self, size: JsSize) {
self.inner.window().set_size(PhysicalSize {
width: size.width.floor() as u32,
height: size.height.floor() as u32,
});
}
}

19
api/node/src/lib.rs Normal file
View file

@ -0,0 +1,19 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
mod interpreter;
pub use interpreter::*;
mod types;
pub use types::*;
mod timer;
pub use timer::*;
#[macro_use]
extern crate napi_derive;
#[napi]
pub fn mock_elapsed_time(ms: f64) {
i_slint_core::tests::slint_mock_elapsed_time(ms as _);
}

27
api/node/src/timer.rs Normal file
View file

@ -0,0 +1,27 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use napi::{Env, JsFunction, Result};
use crate::RefCountedReference;
/// Starts the timer with the duration, in order for the callback to called when the timer fires. It is fired only once and then deleted.
#[napi]
pub fn singleshot_timer(env: Env, duration_in_msecs: f64, handler: JsFunction) -> Result<()> {
if duration_in_msecs < 0. {
return Err(napi::Error::from_reason("Duration cannot be negative"));
}
let duration_in_msecs = duration_in_msecs as u64;
let handler_ref = RefCountedReference::new(&env, handler)?;
i_slint_core::timers::Timer::single_shot(
std::time::Duration::from_millis(duration_in_msecs),
move || {
let callback: JsFunction = handler_ref.get().unwrap();
callback.call_without_args(None).unwrap();
},
);
Ok(())
}

17
api/node/src/types.rs Normal file
View file

@ -0,0 +1,17 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
mod brush;
pub use brush::*;
mod image_data;
pub use image_data::*;
mod model;
pub use model::*;
mod point;
pub use point::*;
mod size;
pub use size::*;

227
api/node/src/types/brush.rs Normal file
View file

@ -0,0 +1,227 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use i_slint_core::{Brush, Color};
use napi::bindgen_prelude::External;
/// Color represents a color in the Slint run-time, represented using 8-bit channels for red, green, blue and the alpha (opacity).
#[napi(js_name = Color)]
pub struct JsColor {
inner: Color,
}
impl From<Color> for JsColor {
fn from(color: Color) -> Self {
Self { inner: color }
}
}
#[napi]
impl JsColor {
/// Creates a new transparent color.
#[napi(constructor)]
pub fn new() -> Self {
Self { inner: Color::default() }
}
/// Construct a color from an integer encoded as `0xAARRGGBB`
#[napi(factory)]
pub fn from_argb_encoded(encoded: u32) -> Self {
Self { inner: Color::from_argb_encoded(encoded) }
}
/// Construct a color from the red, green and blue color channel parameters. The alpha
/// channel will have the value 255.
#[napi(factory)]
pub fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
Self { inner: Color::from_rgb_u8(red, green, blue) }
}
/// Construct a color from the alpha, red, green and blue color channel parameters.
#[napi(factory)]
pub fn from_argb(alpha: u8, red: u8, green: u8, blue: u8) -> Self {
Self { inner: Color::from_argb_u8(alpha, red, green, blue) }
}
/// Returns `(alpha, red, green, blue)` encoded as number.
#[napi(getter)]
pub fn as_argb_encoded(&self) -> u32 {
self.inner.as_argb_encoded()
}
/// Returns the red channel of the color as number in the range 0..255.
#[napi(getter)]
pub fn red(&self) -> u8 {
self.inner.red()
}
/// Returns the green channel of the color as number in the range 0..255.
#[napi(getter)]
pub fn green(&self) -> u8 {
self.inner.green()
}
/// Returns the blue channel of the color as number in the range 0..255.
#[napi(getter)]
pub fn blue(&self) -> u8 {
self.inner.blue()
}
/// Returns the alpha channel of the color as number in the range 0..255.
#[napi(getter)]
pub fn alpha(&self) -> u8 {
self.inner.alpha()
}
// Returns a new version of this color that has the brightness increased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and multiplying the brightness (value) with (1 + factor).
/// The result is converted back to RGB and the alpha channel is unchanged.
/// So for example `brighter(0.2)` will increase the brightness by 20%, and
/// calling `brighter(-0.5)` will return a color that's 50% darker.
#[napi]
pub fn brighter(&self, factor: f64) -> JsColor {
JsColor::from(self.inner.brighter(factor as f32))
}
/// Returns a new version of this color that has the brightness decreased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and dividing the brightness (value) by (1 + factor). The
/// result is converted back to RGB and the alpha channel is unchanged.
/// So for example `darker(0.3)` will decrease the brightness by 30%.
#[napi]
pub fn darker(&self, factor: f64) -> JsColor {
JsColor::from(self.inner.darker(factor as f32))
}
/// Returns a new version of this color with the opacity decreased by `factor`.
///
/// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
#[napi]
pub fn transparentize(&self, amount: f64) -> JsColor {
JsColor::from(self.inner.transparentize(amount as f32))
}
/// Returns a new color that is a mix of `self` and `other`, with a proportion
/// factor given by `factor` (which will be clamped to be between `0.0` and `1.0`).
#[napi]
pub fn mix(&self, other: &JsColor, factor: f64) -> JsColor {
JsColor::from(self.inner.mix(&other.inner, factor as f32))
}
/// Returns a new version of this color with the opacity set to `alpha`.
#[napi]
pub fn with_alpha(&self, alpha: f64) -> JsColor {
JsColor::from(self.inner.with_alpha(alpha as f32))
}
/// Returns the color as string in hex representation e.g. `#000000` for black.
#[napi]
pub fn to_string(&self) -> String {
format!("#{:02x}{:02x}{:02x}{:02x}", self.red(), self.green(), self.blue(), self.alpha())
}
}
/// A brush is a data structure that is used to describe how
/// a shape, such as a rectangle, path or even text, shall be filled.
/// A brush can also be applied to the outline of a shape, that means
/// the fill of the outline itself.
#[napi(js_name = Brush)]
pub struct JsBrush {
inner: Brush,
}
impl From<Brush> for JsBrush {
fn from(brush: Brush) -> Self {
Self { inner: brush }
}
}
impl From<JsColor> for JsBrush {
fn from(color: JsColor) -> Self {
Self::from(Brush::from(color.inner))
}
}
#[napi]
impl JsBrush {
/// Creates a new transparent brush.
#[napi(constructor)]
pub fn new() -> Self {
Self { inner: Brush::default() }
}
/// Creates a brush form a `Color`.
#[napi(factory)]
pub fn from_color(color: &JsColor) -> Self {
Self { inner: Brush::SolidColor(color.inner) }
}
/// If the brush is SolidColor, the contained color is returned.
/// If the brush is a LinearGradient, the color of the first stop is returned.
#[napi(getter)]
pub fn color(&self) -> JsColor {
self.inner.color().into()
}
/// Returns true if this brush contains a fully transparent color (alpha value is zero)
#[napi(getter)]
pub fn is_transparent(&self) -> bool {
self.inner.is_transparent()
}
/// Returns true if this brush is fully opaque.
#[napi(getter)]
pub fn is_opaque(&self) -> bool {
self.inner.is_opaque()
}
/// Returns a new version of this brush that has the brightness increased
/// by the specified factor. This is done by calling [`Color::brighter`] on
/// all the colors of this brush.
#[napi]
pub fn brighter(&self, factor: f64) -> JsBrush {
JsBrush::from(self.inner.brighter(factor as f32))
}
/// Returns a new version of this brush that has the brightness decreased
/// by the specified factor. This is done by calling [`Color::darker`] on
/// all the color of this brush.
#[napi]
pub fn darker(&self, factor: f64) -> JsBrush {
JsBrush::from(self.inner.darker(factor as f32))
}
/// Returns a new version of this brush with the opacity decreased by `factor`.
///
/// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
#[napi]
pub fn transparentize(&self, amount: f64) -> JsBrush {
JsBrush::from(self.inner.transparentize(amount as f32))
}
/// Returns a new version of this brush with the related color's opacities
/// set to `alpha`.
#[napi]
pub fn with_alpha(&self, alpha: f64) -> JsBrush {
JsBrush::from(self.inner.with_alpha(alpha as f32))
}
/// @hidden
#[napi(getter)]
pub fn brush(&self) -> External<Brush> {
External::new(self.inner.clone())
}
/// Returns the color as string in hex representation e.g. `#000000` for black.
/// It is only implemented for solid color brushes.
#[napi]
pub fn to_string(&self) -> String {
if let Brush::SolidColor(_) = self.inner {
return self.color().to_string();
}
println!("toString() is not yet implemented for gradient brushes.");
String::default()
}
}

View file

@ -0,0 +1,96 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use std::vec;
use i_slint_core::{
graphics::{Image, SharedImageBuffer, SharedPixelBuffer},
ImageInner,
};
use napi::bindgen_prelude::{Buffer, External};
// This is needed for typedoc check JsImageData::image
pub type ImageData = Image;
/// An image data type that can be displayed by the Image element
#[napi(js_name = ImageData)]
pub struct JsImageData {
inner: Image,
}
impl From<Image> for JsImageData {
fn from(image: Image) -> Self {
Self { inner: image }
}
}
#[napi]
impl JsImageData {
/// Constructs a new image with the given width and height.
/// Each pixel will set to red = 0, green = 0, blue = 0 and alpha = 0.
#[napi(constructor)]
pub fn new(width: u32, height: u32) -> Self {
Self { inner: Image::from_rgba8(SharedPixelBuffer::new(width, height)) }
}
/// Returns the width of the image in pixels.
#[napi(getter)]
pub fn width(&self) -> u32 {
self.inner.size().width
}
/// Returns the height of the image in pixels.
#[napi(getter)]
pub fn height(&self) -> u32 {
self.inner.size().height
}
/// Returns the image as buffer.
/// A Buffer is a subclass of Uint8Array.
#[napi(getter)]
pub fn data(&self) -> Buffer {
let image_inner: &ImageInner = (&self.inner).into();
if let Some(buffer) = image_inner.render_to_buffer(None) {
match buffer {
SharedImageBuffer::RGB8(buffer) => {
return Buffer::from(rgb_to_rgba(
buffer.as_bytes(),
(self.width() * self.height()) as usize,
))
}
SharedImageBuffer::RGBA8(buffer) => return Buffer::from(buffer.as_bytes()),
SharedImageBuffer::RGBA8Premultiplied(buffer) => {
return Buffer::from(rgb_to_rgba(
buffer.as_bytes(),
(self.width() * self.height()) as usize,
))
}
}
}
Buffer::from(vec![0; (self.width() * self.height() * 4) as usize])
}
/// @hidden
#[napi(getter)]
pub fn image(&self) -> External<ImageData> {
External::new(self.inner.clone())
}
}
fn rgb_to_rgba(bytes: &[u8], size: usize) -> Vec<u8> {
let mut rgba_bytes = vec![];
for i in 0..size {
if (i * 3) + 2 >= bytes.len() {
continue;
}
rgba_bytes.push(bytes[i * 3]);
rgba_bytes.push(bytes[(i * 3) + 1]);
rgba_bytes.push(bytes[(i * 3) + 2]);
rgba_bytes.push(255);
}
rgba_bytes
}

185
api/node/src/types/model.rs Normal file
View file

@ -0,0 +1,185 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use std::rc::{Rc, Weak};
use i_slint_compiler::langtype::Type;
use i_slint_core::model::Model;
use napi::{
bindgen_prelude::Object, Env, JsFunction, JsNumber, JsUnknown, NapiRaw, Result, ValueType,
};
use slint_interpreter::Value;
use crate::{to_js_unknown, to_value, RefCountedReference};
pub struct JsModel {
model: RefCountedReference,
env: Env,
notify: i_slint_core::model::ModelNotify,
data_type: Type,
}
impl JsModel {
pub fn new<T: NapiRaw>(env: Env, model: T, data_type: Type) -> napi::Result<Rc<Self>> {
let js_model = Rc::new(Self {
notify: Default::default(),
env,
model: RefCountedReference::new(&env, model)?,
data_type,
});
let notify = JsSlintModelNotify { model: Rc::downgrade(&js_model) };
js_model.model.get::<Object>()?.set("notify", notify)?;
Ok(js_model)
}
pub fn model(&self) -> &RefCountedReference {
&self.model
}
}
impl Model for JsModel {
type Data = slint_interpreter::Value;
fn row_count(&self) -> usize {
let model: Object = self.model.get().unwrap();
model
.get::<&str, JsFunction>("rowCount")
.ok()
.and_then(|callback| {
callback.and_then(|callback| callback.call::<JsUnknown>(Some(&model), &[]).ok())
})
.and_then(|res| res.coerce_to_number().ok())
.map(|num| num.get_uint32().ok().map_or(0, |count| count as usize))
.unwrap_or_default()
}
fn row_data(&self, row: usize) -> Option<Self::Data> {
let model: Object = self.model.get().unwrap();
model
.get::<&str, JsFunction>("rowData")
.ok()
.and_then(|callback| {
callback.and_then(|callback| {
callback
.call::<JsNumber>(
Some(&model),
&[self.env.create_double(row as f64).unwrap()],
)
.ok()
})
})
.and_then(|res| {
if res.get_type().unwrap() == ValueType::Undefined {
None
} else {
to_value(&self.env, res, self.data_type.clone()).ok()
}
})
}
fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker {
&self.notify
}
fn set_row_data(&self, row: usize, data: Self::Data) {
let model: Object = self.model.get().unwrap();
model.get::<&str, JsFunction>("setRowData").ok().and_then(|callback| {
callback.and_then(|callback| {
callback
.call::<JsUnknown>(
Some(&model),
&[
to_js_unknown(&self.env, &Value::Number(row as f64)).unwrap(),
to_js_unknown(&self.env, &data).unwrap(),
],
)
.ok()
})
});
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
}
#[napi(js_name = "SlintModelNotify")]
pub struct JsSlintModelNotify {
model: Weak<JsModel>,
}
impl JsSlintModelNotify {
fn model(&self) -> Result<Rc<JsModel>> {
self.model.upgrade().ok_or(napi::Error::from_reason("cannot upgrade model"))
}
}
#[napi]
impl JsSlintModelNotify {
#[napi(constructor)]
pub fn new() -> Self {
Self { model: Weak::default() }
}
#[napi]
pub fn row_data_changed(&self, row: f64) -> Result<()> {
let model = self.model()?;
if row < 0. && row >= model.row_count() as f64 {
return Err(napi::Error::from_reason(
"row with value {row} out of bounds.".to_string(),
));
}
model.notify.row_changed(row as usize);
Ok(())
}
#[napi]
pub fn row_added(&self, row: f64, count: f64) -> Result<()> {
let model = self.model()?;
if row < 0. && row >= model.row_count() as f64 {
return Err(napi::Error::from_reason(
"row with value {row} out of bounds.".to_string(),
));
}
if count < 0. {
return Err(napi::Error::from_reason("count cannot be negative.".to_string()));
}
model.notify.row_added(row as usize, count as usize);
Ok(())
}
#[napi]
pub fn row_removed(&self, row: f64, count: f64) -> Result<()> {
let model = self.model()?;
if row < 0. && row >= model.row_count() as f64 {
return Err(napi::Error::from_reason(
"row with value {row} out of bounds.".to_string(),
));
}
if count < 0. {
return Err(napi::Error::from_reason("count cannot be negative.".to_string()));
}
model.notify.row_removed(row as usize, count as usize);
Ok(())
}
#[napi]
pub fn reset(&self) -> Result<()> {
self.model()?.notify.reset();
Ok(())
}
}

View file

@ -0,0 +1,54 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use napi::{
bindgen_prelude::{FromNapiValue, Object},
JsUnknown,
};
/// Represents a two dimensional point.
#[napi(js_name = Point)]
pub struct JsPoint {
pub x: f64,
pub y: f64,
}
#[napi]
impl JsPoint {
/// Constructs new point from x and y.
#[napi(constructor)]
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
impl FromNapiValue for JsPoint {
unsafe fn from_napi_value(
env: napi::sys::napi_env,
napi_val: napi::sys::napi_value,
) -> napi::Result<Self> {
let obj = unsafe { Object::from_napi_value(env, napi_val)? };
let x: f64 = obj
.get::<_, JsUnknown>("x")
.ok()
.flatten()
.and_then(|p| p.coerce_to_number().ok())
.and_then(|f64_num| f64_num.try_into().ok())
.ok_or_else(
|| napi::Error::from_reason(
"Cannot convert object to Point, because the provided object does not have an f64 x property".to_string()
))?;
let y: f64 = obj
.get::<_, JsUnknown>("y")
.ok()
.flatten()
.and_then(|p| p.coerce_to_number().ok())
.and_then(|f64_num| f64_num.try_into().ok())
.ok_or_else(
|| napi::Error::from_reason(
"Cannot convert object to Point, because the provided object does not have an f64 y property".to_string()
))?;
Ok(JsPoint { x, y })
}
}

View file

@ -0,0 +1,62 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use napi::{
bindgen_prelude::{FromNapiValue, Object},
JsUnknown, Result,
};
/// Represents a two-dimensional size.
#[napi(js_name = Size)]
pub struct JsSize {
pub width: f64,
pub height: f64,
}
#[napi]
impl JsSize {
/// Constructs a size from the given width and height.
#[napi(constructor)]
pub fn new(width: f64, height: f64) -> Result<Self> {
if width < 0. {
return Err(napi::Error::from_reason("width cannot be negative".to_string()));
}
if height < 0. {
return Err(napi::Error::from_reason("height cannot be negative".to_string()));
}
Ok(Self { width, height })
}
}
impl FromNapiValue for JsSize {
unsafe fn from_napi_value(
env: napi::sys::napi_env,
napi_val: napi::sys::napi_value,
) -> napi::Result<Self> {
let obj = unsafe { Object::from_napi_value(env, napi_val)? };
let width: f64 = obj
.get::<_, JsUnknown>("width")
.ok()
.flatten()
.and_then(|p| p.coerce_to_number().ok())
.and_then(|f64_num| f64_num.try_into().ok())
.ok_or_else(
|| napi::Error::from_reason(
"Cannot convert object to Size, because the provided object does not have an f64 width property".to_string()
))?;
let height: f64 = obj
.get::<_, JsUnknown>("height")
.ok()
.flatten()
.and_then(|p| p.coerce_to_number().ok())
.and_then(|f64_num| f64_num.try_into().ok())
.ok_or_else(
|| napi::Error::from_reason(
"Cannot convert object to Size, because the provided object does not have an f64 height property".to_string()
))?;
Ok(JsSize { width, height })
}
}

View file

@ -1,71 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"typedocOptions": {
"mode": "file",
"out": "docs",
"readme": "README.md",
"disableSources": true,
"theme": "minimal",
"hideGenerator": true,
"name": "Slint Node"
}
}