mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 18:58:36 +00:00
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:
parent
b6b3337430
commit
bf77b064ab
73 changed files with 370 additions and 2131 deletions
7
api/node/.gitignore
vendored
7
api/node/.gitignore
vendored
|
@ -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
13
api/node/.npmignore
Normal 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
4
api/node/.yarnrc.yml
Normal 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
|
|
@ -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"
|
|
@ -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)
|
||||
|
||||
[](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);
|
||||
```
|
||||
```
|
58
api/node/__test__/api.spec.ts
Normal file
58
api/node/__test__/api.spec.ts
Normal 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");
|
||||
})
|
239
api/node/__test__/compiler.spec.ts
Normal file
239
api/node/__test__/compiler.spec.ts
Normal 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');
|
||||
})
|
173
api/node/__test__/globals.spec.ts
Normal file
173
api/node/__test__/globals.spec.ts
Normal 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" });
|
||||
})
|
480
api/node/__test__/js_value_conversion.spec.ts
Normal file
480
api/node/__test__/js_value_conversion.spec.ts
Normal 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" });
|
||||
})
|
8
api/node/__test__/resources/error.slint
Normal file
8
api/node/__test__/resources/error.slint
Normal 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";
|
||||
}
|
BIN
api/node/__test__/resources/rgb.png
Normal file
BIN
api/node/__test__/resources/rgb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 272 B |
10
api/node/__test__/resources/test-constructor.slint
Normal file
10
api/node/__test__/resources/test-constructor.slint
Normal 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;
|
||||
}
|
9
api/node/__test__/resources/test.slint
Normal file
9
api/node/__test__/resources/test.slint
Normal 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";
|
||||
}
|
101
api/node/__test__/types.spec.ts
Normal file
101
api/node/__test__/types.spec.ts
Normal 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"
|
||||
}
|
||||
);
|
||||
})
|
||||
|
38
api/node/__test__/window.spec.ts
Normal file
38
api/node/__test__/window.spec.ts
Normal 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);
|
||||
})
|
|
@ -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();
|
||||
}
|
|
@ -4,9 +4,9 @@
|
|||
[](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
389
api/node/index.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)?))
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
20
api/node/src/interpreter.rs
Normal file
20
api/node/src/interpreter.rs
Normal 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::*;
|
109
api/node/src/interpreter/component_compiler.rs
Normal file
109
api/node/src/interpreter/component_compiler.rs
Normal 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())
|
||||
}
|
||||
}
|
72
api/node/src/interpreter/component_definition.rs
Normal file
72
api/node/src/interpreter/component_definition.rs
Normal 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()
|
||||
}
|
||||
}
|
372
api/node/src/interpreter/component_instance.rs
Normal file
372
api/node/src/interpreter/component_instance.rs
Normal 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();
|
||||
}
|
||||
}
|
62
api/node/src/interpreter/diagnostic.rs
Normal file
62
api/node/src/interpreter/diagnostic.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
274
api/node/src/interpreter/value.rs
Normal file
274
api/node/src/interpreter/value.rs
Normal 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()))
|
||||
}
|
120
api/node/src/interpreter/window.rs
Normal file
120
api/node/src/interpreter/window.rs
Normal 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
19
api/node/src/lib.rs
Normal 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
27
api/node/src/timer.rs
Normal 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
17
api/node/src/types.rs
Normal 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
227
api/node/src/types/brush.rs
Normal 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()
|
||||
}
|
||||
}
|
96
api/node/src/types/image_data.rs
Normal file
96
api/node/src/types/image_data.rs
Normal 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
185
api/node/src/types/model.rs
Normal 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(())
|
||||
}
|
||||
}
|
54
api/node/src/types/point.rs
Normal file
54
api/node/src/types/point.rs
Normal 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 })
|
||||
}
|
||||
}
|
62
api/node/src/types/size.rs
Normal file
62
api/node/src/types/size.rs
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue