Replace neon node port (#3744)

* Update api/node/README.md

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

---------

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

View file

@ -74,7 +74,7 @@ jobs:
# cargo update -p clap_lex --precise 0.5.0 # cargo update -p clap_lex --precise 0.5.0
# fi # fi
- name: Run tests - name: Run tests
run: DYLD_FRAMEWORK_PATH=$Qt5_DIR/lib cargo test --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude slint-napi --exclude test-driver-napi --exclude slint-node --exclude test-driver-nodejs --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude uefi-demo --exclude slint-cpp run: DYLD_FRAMEWORK_PATH=$Qt5_DIR/lib cargo test --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude slint-node --exclude test-driver-node --exclude slint-node --exclude test-driver-nodejs --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude uefi-demo --exclude slint-cpp
env: env:
SLINT_CREATE_SCREENSHOTS: 1 SLINT_CREATE_SCREENSHOTS: 1
shell: bash shell: bash
@ -87,55 +87,6 @@ jobs:
tests/screenshots/references tests/screenshots/references
node_test: node_test:
env:
DYLD_FRAMEWORK_PATH: /Users/runner/work/slint/Qt/5.15.2/clang_64/lib
QT_QPA_PLATFORM: offscreen
RUSTFLAGS: -D warnings
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_INCREMENTAL: false
RUST_BACKTRACE: 1
strategy:
matrix:
os: [ubuntu-22.04, macos-11]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/install-linux-dependencies
# Python 3.11 breaks the neon-sys build
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Qt
if: runner.os != 'Windows'
uses: jurplel/install-qt-action@v3
with:
version: '5.15.2'
setup-python: false
cache: true
- name: Setup headless display
uses: pyvista/setup-headless-display-action@v1
- name: Set default style
if: matrix.os != 'windows-2022'
run: |
echo "SLINT_STYLE=native" >> $GITHUB_ENV
- name: Set default style
if: matrix.os == 'windows-2022'
run: |
echo "SLINT_STYLE=fluent" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "SLINT_NO_QT=1" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- uses: ./.github/actions/install-nodejs
id: node-install
- uses: ./.github/actions/setup-rust
with:
key: x-v2-${{ steps.node-install.outputs.node-version }} # the cache key consists of a manually bumpable version and the node version, as the cached rustc artifacts contain linking information where to find node.lib, which is in a versioned directory.
- name: Build node plugin
run: cargo build --verbose --all-features -p slint-node
- name: Run node tests
run: cargo test --verbose --all-features -p test-driver-nodejs -p slint-node
napi_test:
env: env:
DYLD_FRAMEWORK_PATH: /Users/runner/work/slint/Qt/5.15.2/clang_64/lib DYLD_FRAMEWORK_PATH: /Users/runner/work/slint/Qt/5.15.2/clang_64/lib
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
@ -176,16 +127,16 @@ jobs:
with: with:
key: x-napi-v2-${{ steps.node-install.outputs.node-version }} # the cache key consists of a manually bumpable version and the node version, as the cached rustc artifacts contain linking information where to find node.lib, which is in a versioned directory. key: x-napi-v2-${{ steps.node-install.outputs.node-version }} # the cache key consists of a manually bumpable version and the node version, as the cached rustc artifacts contain linking information where to find node.lib, which is in a versioned directory.
- name: Run npm install - name: Run npm install
working-directory: ./api/napi working-directory: ./api/node
run: npm install run: npm install
- name: Typescript check - name: Typescript check
working-directory: ./api/napi working-directory: ./api/node
run: npm run syntax_check run: npm run syntax_check
- name: Run napi tests - name: Run node tests
working-directory: ./api/napi working-directory: ./api/node
run: npm test run: npm test
- name: Run test-driver-napi - name: Run test-driver-nodejs
run: cargo test --verbose --all-features -p test-driver-napi -p slint-napi run: cargo test --verbose --all-features -p test-driver-nodejs -p slint-node
cpp_test_driver: cpp_test_driver:
env: env:

View file

@ -72,7 +72,7 @@ Files: tests/screenshots/references/software/*/*.png
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev> Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
License: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial License: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
Files: api/napi/__test__/resources/*.png Files: api/node/__test__/resources/*.png
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev> Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
License: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial License: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial

View file

@ -4,8 +4,7 @@
[workspace] [workspace]
members = [ members = [
'api/cpp', 'api/cpp',
'api/node/native', 'api/node',
'api/napi',
'api/rs/build', 'api/rs/build',
'api/rs/macros', 'api/rs/macros',
'api/rs/slint', 'api/rs/slint',
@ -50,7 +49,6 @@ members = [
'tests/driver/driverlib', 'tests/driver/driverlib',
'tests/driver/interpreter', 'tests/driver/interpreter',
'tests/driver/nodejs', 'tests/driver/nodejs',
'tests/driver/napi',
'tests/driver/rust', 'tests/driver/rust',
'tests/screenshots', 'tests/screenshots',
'tools/compiler', 'tools/compiler',
@ -65,7 +63,6 @@ members = [
default-members = [ default-members = [
'api/rs/build', 'api/rs/build',
'api/rs/slint', 'api/rs/slint',
'api/napi',
'examples/gallery', 'examples/gallery',
'examples/memory', 'examples/memory',
'examples/printerdemo_old/rust', 'examples/printerdemo_old/rust',

4
api/napi/.gitignore vendored
View file

@ -1,4 +0,0 @@
rust-module.js
rust-module.d.ts
index.js
docs/

View file

@ -1,22 +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 -->
# Slint-napi (pre-Alpha)
This is a restart of `Slint-node` based on [napi-rs](https://github.com/napi-rs/napi-rs). The current state is `pre-Alpha` what means it is not yet ready for testing and use.
## Implemented features
* js/ts wrapper for the `Slint` interpreter infrastructure
* js/ts wrapper for `Slint` types
* `ImageData`
* `Color`
* `Brush`
## Missing features
* Possibility to run js/ts `async` code after window run call
* Generate a js/ts object-wrapper for the exported `Slint` component
* Public access to Slint `globals`
* CI: Generate prebuild platform `node` packages
* Documentation generation
* js/ts wrapper for `Slint` types
* `Model` (wip)

View file

@ -1,180 +0,0 @@
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
# Slint-node (Beta)
[![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui)
[Slint](https://slint.dev/) is a UI toolkit that supports different programming languages.
Slint-node is the integration with Node.js.
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:
```sh
npm install slint-ui
```
### Dependencies
You need to install the following components:
* **[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)
You will also need a few more dependencies, see <https://github.com/slint-ui/slint/blob/master/docs/building.md#prerequisites>
## 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
import * as slint from "slint-ui";
```
Next, load a slint file with the `loadFile` function:
```js
let ui = slint.loadFile("ui/main.slint");
```
Combining these two steps leads us to the obligatory "Hello World" example:
```js
import * as slint from "slint-ui";
let ui = slint.loadFile(".ui/main.slint");
let main = new ui.Main();
main.run();
```
For a full example, see [/examples/todo/node](https://github.com/slint-ui/slint/tree/master/examples/todo/node).
## API Overview
### 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
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 = slint.loadFile("ui/main.slint");
let component = new ui.MainWindow({
counter: 42,
clicked: function() { console.log("hello"); }
});
```
### Accessing a property
Properties declared as `out` or `in-out` in `.slint` files are visible as JavaScript on the component instance.
```js
component.counter = 42;
console.log(component.counter);
```
### Callbacks
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
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` | {@link Color} | |
| `brush` | {@link Brush} | |
| `image` | {@link ImageData} | |
| `length` | `Number` | |
| `physical_length` | `Number` | |
| `duration` | `Number` | The number of milliseconds |
| `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} | |
### Arrays and Models
[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 assignment creates a copy.
// Use re-assignment instead.
component.model = component.model.concat(4);
```
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.

View file

@ -1,49 +0,0 @@
{
"name": "slint-node",
"version": "1.3.0",
"main": "index.js",
"types": "index.d.ts",
"license": "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial",
"napi": {
"name": "slint-ui"
},
"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",
"prepublishOnly": "napi prepublish -t npm",
"test": "ava",
"syntax_check": "tsc -noEmit index.ts",
"universal": "napi universal",
"version": "1.3.0"
},
"ava": {
"require": [
"@swc-node/register"
],
"extensions": [
"ts"
],
"timeout": "2m",
"workerThreads": false,
"environmentVariables": {
"TS_NODE_PROJECT": "./tsconfig.json"
}
}
}

7
api/node/.gitignore vendored
View file

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

View file

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
[package] [package]
name = "slint-napi" name = "slint-node"
description = "Internal Slint Runtime Library for NodeJS API." description = "Internal Slint Runtime Library for NodeJS API."
authors.workspace = true authors.workspace = true
documentation.workspace = true documentation.workspace = true

View file

@ -1,19 +1,24 @@
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial --> <!-- 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) # Slint-node (Beta)
[![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui) [![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui)
[Slint](https://slint.dev/) is a UI toolkit that supports different programming languages. [Slint](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 use the [walk-through tutorial](https://slint.dev/docs/tutorial/node).
To get started you can 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 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. the code of a minimal application using Slint that can be used as a starting point to your program.
**Warning: Beta** **Warning: Beta**
Slint-node is still in the early stages of development: APIs will change and important features are still being developed. 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 ## Installing Slint
Slint is available via NPM, so you can install by running the following command: 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: 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/)** * **[npm](https://www.npmjs.com/)**
* **[Rust compiler](https://www.rust-lang.org/tools/install)** (1.70 or newer) * **[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 ## 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: To initialize the API, you first need to import the `slint-ui` module in our code:
```js ```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 ```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: Combining these two steps leads us to the obligatory "Hello World" example:
```js ```js
require("slint-ui"); import * as slint from "slint-ui";
let ui = require("../ui/main.slint"); let ui = slint.loadFile(".ui/main.slint");
let main = new ui.Main(); let main = new ui.Main();
main.run(); 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 ## 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 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. an object which allow to initialize the value of public properties or callbacks.
**`main.js`**
```js ```js
require("slint-ui"); import * as slint from "slint-ui";
// In this example, the main.slint file exports a module which // In this example, the main.slint file exports a module which
// has a counter property and a clicked callback // 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({ let component = new ui.MainWindow({
counter: 42, counter: 42,
clicked: function() { console.log("hello"); } clicked: function() { console.log("hello"); }
@ -77,7 +103,7 @@ let component = new ui.MainWindow({
### Accessing a property ### 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 ```js
component.counter = 42; component.counter = 42;
@ -86,9 +112,32 @@ console.log(component.counter);
### Callbacks ### 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 ```js
import * as slint from "slint-ui";
let ui = slint.loadFile("ui/my-component.slint");
let component = new ui.MyComponent();
// connect to a callback // connect to a callback
component.clicked.setHandler(function() { console.log("hello"); }) component.clicked.setHandler(function() { console.log("hello"); })
// emit a callback // emit a callback
@ -97,20 +146,24 @@ component.clicked();
### Type Mappings ### 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 | | `.slint` Type | JavaScript Type | Notes |
| --- | --- | --- | | --- | --- | --- |
| `int` | `Number` | | | `int` | `Number` | |
| `float` | `Number` | | | `float` | `Number` | |
| `string` | `String` | | | `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` | | | `length` | `Number` | |
| `physical_length` | `Number` | | | `physical_length` | `Number` | |
| `duration` | `Number` | The number of milliseconds | | `duration` | `Number` | The number of milliseconds |
| `angle` | `Number` | The value in degrees | | `angle` | `Number` | The angle in degrees |
| structure | `Object` | Structures are mapped to JavaScrip objects with structure fields mapped to properties. | | structure | `Object` | Structures are mapped to JavaScript objects where each structure field is a property. |
| array | `Array` or Model Object | | | array | `Array` or any implementation of Model | |
### Models ### Arrays and Models
For property of array type, they can either be set using an array. For property of array type, they can either be set using an array.
In that case, getting the property also return an array. In that case, getting the property also return an array.
@ -129,31 +182,60 @@ Another option is to set a model object. A model object has the following funct
* `rowData(index)`: return the row at the given index * `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. * `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`) As an example, here is the implementation of the `ArrayModel` (which is available as `slint.ArrayModel`)
```js ```js
import * as slint from "slint-ui";
let array = [1, 2, 3]; let array = [1, 2, 3];
let model = {
rowCount() { return a.length; }, export class ArrayModel<T> extends slint.Model<T> {
rowData(row) { return a[row]; }, private a: Array<T>
setRowData(row, data) { a[row] = data; this.notify.rowDataChanged(row); },
push() { constructor(arr: Array<T>) {
let size = a.length; super();
Array.prototype.push.apply(a, arguments); 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); this.notify.rowAdded(size, arguments.length);
}, }
remove(index, size) {
let r = a.splice(index, size); remove(index: number, size: number) {
this.notify.rowRemoved(size, arguments.length); 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; component.model = model;
model.push(4); // this works model.push(4); // this works
// does NOT work, getting the model does not return the right object // does NOT work, getting the model does not return the right object

View file

Before

Width:  |  Height:  |  Size: 272 B

After

Width:  |  Height:  |  Size: 272 B

Before After
Before After

View file

@ -3,7 +3,7 @@
import test from 'ava'; import test from 'ava';
import { Brush, Color, ArrayModel } from '../index' import { Brush, Color, ArrayModel, Timer } from '../index'
test('Color from fromRgb', (t) => { test('Color from fromRgb', (t) => {
let color = Color.fromRgb(100, 110, 120); let color = Color.fromRgb(100, 110, 120);
@ -87,3 +87,15 @@ test('ArrayModel remove', (t) => {
t.is(arrayModel.rowCount(), 1); t.is(arrayModel.rowCount(), 1);
t.is(arrayModel.rowData(0), 1); t.is(arrayModel.rowData(0), 1);
}) })
test('Timer negative duration', (t) => {
t.throws(() => {
Timer.singleShot(-1, function () {})
},
{
code: "GenericFailure",
message: "Duration cannot be negative"
}
);
})

View file

@ -4,9 +4,9 @@
[![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui) [![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/slint-ui)
[Slint](https://slint.dev/) is a UI toolkit that supports different programming languages. [Slint](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 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. 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 ## 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. in detail.
## Installing Slint ## Installing Slint
@ -30,7 +30,7 @@ npm install slint-ui
You need to install the following components: 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/)** * **[npm](https://www.npmjs.com/)**
* **[Rust compiler](https://www.rust-lang.org/tools/install)** (1.70 or newer) * **[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 ## 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: To initialize the API, you first need to import the `slint-ui` module in our code:
```js ```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 ```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: Combining these two steps leads us to the obligatory "Hello World" example:
```js ```js
require("slint-ui"); import * as slint from "slint-ui";
let ui = require("../ui/main.slint"); let ui = slint.loadFile(".ui/main.slint");
let main = new ui.Main(); let main = new ui.Main();
main.run(); 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 ## 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 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. an object which allow to initialize the value of public properties or callbacks.
**`main.js`**
```js ```js
require("slint-ui"); import * as slint from "slint-ui";
// In this example, the main.slint file exports a module which // In this example, the main.slint file exports a module which
// has a counter property and a clicked callback // 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({ let component = new ui.MainWindow({
counter: 42, counter: 42,
clicked: function() { console.log("hello"); } clicked: function() { console.log("hello"); }
@ -81,7 +102,7 @@ let component = new ui.MainWindow({
### Accessing a property ### 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 ```js
component.counter = 42; component.counter = 42;
@ -90,9 +111,32 @@ console.log(component.counter);
### Callbacks ### 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 ```js
import * as slint from "slint-ui";
let ui = slint.loadFile("ui/my-component.slint");
let component = new ui.MyComponent();
// connect to a callback // connect to a callback
component.clicked.setHandler(function() { console.log("hello"); }) component.clicked.setHandler(function() { console.log("hello"); })
// emit a callback // emit a callback
@ -101,65 +145,36 @@ component.clicked();
### Type Mappings ### 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 | | `.slint` Type | JavaScript Type | Notes |
| --- | --- | --- | | --- | --- | --- |
| `int` | `Number` | | | `int` | `Number` | |
| `float` | `Number` | | | `float` | `Number` | |
| `string` | `String` | | | `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` | | | `length` | `Number` | |
| `physical_length` | `Number` | | | `physical_length` | `Number` | |
| `duration` | `Number` | The number of milliseconds | | `duration` | `Number` | The number of milliseconds |
| `angle` | `Number` | The value in degrees | | `angle` | `Number` | The angle in degrees |
| structure | `Object` | Structures are mapped to JavaScrip objects with structure fields mapped to properties. | | structure | `Object` | Structures are mapped to JavaScript objects where each structure field is a property. |
| array | `Array` or Model Object | | | 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. [Array properties](../slint/src/reference/types#arrays-and-models) can be set from JavaScript by passing
In that case, getting the property also return an array. either `Array` objects or implementations of the {@link Model} interface.
If the array was set within the .slint file, the array can be obtained
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 ```js
component.model = [1, 2, 3]; component.model = [1, 2, 3];
// component.model.push(4); // does not work, because it operate on a copy // component.model.push(4); // does not work, because assignment creates a copy.
// but re-assigning works // Use re-assignment instead.
component.model = component.model.concat(4); component.model = component.model.concat(4);
``` ```
Another option is to set a model object. A model object has the following function: 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.
* `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);
```

View file

@ -1,6 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial // 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"; import * as napi from "./rust-module";
export { Diagnostic, DiagnosticLevel, Window, Brush, Color, ImageData, Point, Size, SlintModelNotify } from "./rust-module"; export { Diagnostic, DiagnosticLevel, Window, Brush, Color, ImageData, Point, Size, SlintModelNotify } from "./rust-module";
@ -292,9 +294,12 @@ export class CompileError extends Error {
/** /**
* Loads the given slint file and returns a constructor to create an instance of the exported component. * Loads the given slint file and returns a constructor to create an instance of the exported component.
*/ */
export function loadFile(path: string) : Object { 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 compiler = new napi.ComponentCompiler;
let definition = compiler.buildFromPath(path); let definition = compiler.buildFromPath(absoluteFilePath);
let diagnostics = compiler.diagnostics; let diagnostics = compiler.diagnostics;
@ -305,7 +310,7 @@ export function loadFile(path: string) : Object {
let errors = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Error); let errors = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Error);
if (errors.length > 0) { if (errors.length > 0) {
throw new CompileError("Could not compile " + path, errors); throw new CompileError("Could not compile " + filePath, errors);
} }
} }
@ -316,7 +321,7 @@ export function loadFile(path: string) : Object {
let instance = definition!.create(); let instance = definition!.create();
if (instance == null) { if (instance == null) {
throw Error("Could not create a component handle for" + path); throw Error("Could not create a component handle for" + filePath);
} }
for(var key in properties) { for(var key in properties) {
@ -356,6 +361,14 @@ export function loadFile(path: string) : Object {
return slint_module; 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 * @hidden
*/ */

View file

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

View file

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

View file

@ -1,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
[package]
name = "slint-node"
authors.workspace = true
edition.workspace = true
homepage.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
[lib]
path = "lib.rs"
crate-type = ["cdylib"]
name = "slint_node_native"
[dependencies]
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" }
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"

View file

@ -1,6 +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
fn main() {
neon_build::setup();
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial // 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 std::path::PathBuf;
use super::JsComponentDefinition; use super::JsComponentDefinition;
@ -60,6 +61,16 @@ impl JsComponentCompiler {
.collect() .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)] #[napi(setter)]
pub fn set_style(&mut self, style: String) { pub fn set_style(&mut self, style: String) {
self.internal.set_style(style); self.internal.set_style(style);

View file

@ -7,6 +7,9 @@ pub use interpreter::*;
mod types; mod types;
pub use types::*; pub use types::*;
mod timer;
pub use timer::*;
#[macro_use] #[macro_use]
extern crate napi_derive; extern crate napi_derive;

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

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

View file

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

View file

@ -3,8 +3,9 @@
// main.js // main.js
let slint = require("slint-ui"); import * as slint from "slint-ui";
let ui = require("./memory.slint");
let ui = slint.loadFile("./memory.slint");
let mainWindow = new ui.MainWindow(); let mainWindow = new ui.MainWindow();
let initial_tiles = mainWindow.memory_tiles; let initial_tiles = mainWindow.memory_tiles;

View file

@ -3,8 +3,9 @@
// ANCHOR: main // ANCHOR: main
// main.js // main.js
require("slint-ui"); import * as slint from "slint-ui";
let ui = require("./memory.slint");
let ui = slint.loadFile("./memory.slint");
let mainWindow = new ui.MainWindow(); let mainWindow = new ui.MainWindow();
mainWindow.run(); mainWindow.run();

View file

@ -3,8 +3,8 @@
// ANCHOR: main // ANCHOR: main
// main.js // main.js
let slint = require("slint-ui"); import * as slint from "slint-ui";
let ui = require("./memory.slint"); let ui = slint.loadFile("./memory.slint");
let mainWindow = new ui.MainWindow(); let mainWindow = new ui.MainWindow();
let initial_tiles = mainWindow.memory_tiles; let initial_tiles = mainWindow.memory_tiles;

View file

@ -2,6 +2,7 @@
"name": "memory", "name": "memory",
"version": "1.3.0", "version": "1.3.0",
"main": "main.js", "main": "main.js",
"type": "module",
"dependencies": { "dependencies": {
"slint-ui": "^1.0.0" "slint-ui": "^1.0.0"
}, },

View file

@ -2,10 +2,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const path = require("path"); import * as slint from "slint-ui";
let slint = require("slint-ui");
let demo = require("../ui/carousel_demo.slint"); let demo = slint.loadFile("../ui/carousel_demo.slint");
let app = new demo.MainWindow(); let app = new demo.MainWindow();
app.run(); app.run();

View file

@ -2,6 +2,7 @@
"name": "carousel", "name": "carousel",
"version": "1.3.0", "version": "1.3.0",
"main": "main.js", "main": "main.js",
"type": "module",
"dependencies": { "dependencies": {
"slint-ui": "../../../api/node" "slint-ui": "../../../api/node"
}, },

View file

@ -2,8 +2,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
let slint = require("slint-ui"); import * as slint from "slint-ui";
let ui = require("./memory.slint");
let ui = slint.loadFile("memory.slint");
let window = new ui.MainWindow(); let window = new ui.MainWindow();
let initial_tiles = window.memory_tiles; let initial_tiles = window.memory_tiles;

View file

@ -2,6 +2,7 @@
"name": "memory", "name": "memory",
"version": "1.3.0", "version": "1.3.0",
"main": "main.js", "main": "main.js",
"type": "module",
"dependencies": { "dependencies": {
"slint-ui": "../../api/node" "slint-ui": "../../api/node"
}, },

View file

@ -2,10 +2,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const path = require("path"); import * as slint from "slint-ui";
let slint = require("slint-ui");
let demo = require("../ui/printerdemo.slint"); let demo = slint.loadFile("../ui/printerdemo.slint");
let window = new demo.MainWindow(); let window = new demo.MainWindow();
window.ink_levels = [ window.ink_levels = [

View file

@ -2,6 +2,7 @@
"name": "printerdemo", "name": "printerdemo",
"version": "1.3.0", "version": "1.3.0",
"main": "main.js", "main": "main.js",
"type": "module",
"dependencies": { "dependencies": {
"slint-ui": "../../../api/node" "slint-ui": "../../../api/node"
}, },

View file

@ -2,10 +2,8 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// import "slint"; import * as slint from "slint-ui";
require("slint-ui"); let demo = slint.loadFile("../ui/printerdemo.slint");
// import * as demo from "../ui/printerdemo.slint";
let demo = require("../ui/printerdemo.slint");
let window = new demo.MainWindow(); let window = new demo.MainWindow();
window.ink_levels = [ window.ink_levels = [

View file

@ -2,6 +2,7 @@
"name": "printerdemo", "name": "printerdemo",
"version": "1.3.0", "version": "1.3.0",
"main": "main.js", "main": "main.js",
"type": "module",
"dependencies": { "dependencies": {
"slint-ui": "../../../api/node" "slint-ui": "../../../api/node"
}, },

View file

@ -1,7 +0,0 @@
Run with
# pushd ../../../api/napi
# npm install
# popd
# npm install
# npm start

View file

@ -1,61 +0,0 @@
#!/usr/bin/env node
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import * as slint from "slint-ui";
let demo = slint.loadFile("../ui/todo.slint");
let app = new demo.MainWindow();
let model = new slint.ArrayModel([
{
title: "Implement the .slint file",
checked: true
},
{
title: "Do the Rust part",
checked: false
},
{
title: "Make the C++ code",
checked: false
},
{
title: "Write some JavaScript code",
checked: true
},
{
title: "Test the application",
checked: false
},
{
title: "Ship to customer",
checked: false
},
{
title: "???",
checked: false
},
{
title: "Profit",
checked: false
},
]);
app.todo_model = model;
app.todo_added.setHandler(function (text) {
model.push({ title: text, checked: false })
})
app.remove_done.setHandler(function () {
let offset = 0;
const length = model.length;
for (let i = 0; i < length; ++i) {
if (model.rowData(i - offset).checked) {
model.remove(i - offset, 1);
offset++;
}
}
})
app.run();

View file

@ -1,12 +0,0 @@
{
"name": "todo",
"version": "1.3.0",
"main": "main.js",
"type": "module",
"dependencies": {
"slint-ui": "../../../api/napi"
},
"scripts": {
"start": "node ."
}
}

View file

@ -2,10 +2,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// import "slint"; import * as slint from "slint-ui";
let slint = require("slint-ui");
// import * as demo from "../ui/todo.slint"; let demo = slint.loadFile("../ui/todo.slint");
let demo = require("../ui/todo.slint");
let app = new demo.MainWindow(); let app = new demo.MainWindow();
let model = new slint.ArrayModel([ let model = new slint.ArrayModel([

View file

@ -2,6 +2,7 @@
"name": "todo", "name": "todo",
"version": "1.3.0", "version": "1.3.0",
"main": "main.js", "main": "main.js",
"type": "module",
"dependencies": { "dependencies": {
"slint-ui": "../../../api/node" "slint-ui": "../../../api/node"
}, },

View file

@ -1,28 +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
[package]
name = "test-driver-napi"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
publish = false
[[bin]]
path = "main.rs"
name = "test-driver-napi"
[dev-dependencies]
test_driver_lib = { path = "../driverlib" }
# Require `artifact` dependencies tracked by rust RFC 3028
#slint-napi = { path = "../../../api/napi", artifact = ["cdylib"] }
lazy_static = "1.4.0"
which = "4.0.2"
tempfile = "3.2"
[build-dependencies]
test_driver_lib = { path = "../driverlib" }

View file

@ -1,48 +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 std::io::Write;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// target/{debug|release}/build/package/out/ -> target/{debug|release}
let mut target_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
target_dir.pop();
target_dir.pop();
target_dir.pop();
println!("cargo:rustc-env=SLINT_ENABLE_EXPERIMENTAL_FEATURES=1",);
let tests_file_path =
std::path::Path::new(&std::env::var_os("OUT_DIR").unwrap()).join("test_functions.rs");
let mut tests_file = std::fs::File::create(&tests_file_path)?;
for testcase in test_driver_lib::collect_test_cases("cases")? {
println!("cargo:rerun-if-changed={}", testcase.absolute_path.display());
let test_function_name = testcase.identifier();
let ignored = testcase.is_ignored("js");
write!(
tests_file,
r##"
#[test]
{ignore}
fn test_nodejs_{function_name}() {{
nodejs::test(&test_driver_lib::TestCase{{
absolute_path: std::path::PathBuf::from(r#"{absolute_path}"#),
relative_path: std::path::PathBuf::from(r#"{relative_path}"#),
}}).unwrap();
}}
"##,
ignore = if ignored { "#[ignore]" } else { "" },
function_name = test_function_name,
absolute_path = testcase.absolute_path.to_string_lossy(),
relative_path = testcase.relative_path.to_string_lossy(),
)?;
}
println!("cargo:rustc-env=TEST_FUNCTIONS={}", tests_file_path.to_string_lossy());
Ok(())
}

View file

@ -1,11 +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
#[cfg(test)]
mod nodejs;
include!(env!("TEST_FUNCTIONS"));
fn main() {
println!("Nothing to see here, please run me through cargo test :)");
}

View file

@ -1,78 +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 std::error::Error;
use std::{fs::File, io::Write, path::PathBuf};
lazy_static::lazy_static! {
static ref NODE_API_JS_PATH: PathBuf = {
let node_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../api/napi");
// On Windows npm is 'npm.cmd', which Rust's process::Command doesn't look for as extension, because
// it tries to emulate CreateProcess.
let npm = which::which("npm").unwrap();
// builds and installs the slint node package
std::process::Command::new(npm.clone())
.arg("install")
.arg("--no-audit")
.current_dir(node_dir.clone())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|err| format!("Could not launch npm install: {}", err)).unwrap();
node_dir.join("index.js")
};
}
pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>> {
let slintpath = NODE_API_JS_PATH.clone();
let dir = tempfile::tempdir()?;
let mut main_js = File::create(dir.path().join("main.js"))?;
write!(
main_js,
r#"
const assert = require('assert').strict;
let slintlib = require(String.raw`{slintpath}`);
let slint = slintlib.loadFile(String.raw`{path}`);
"#,
slintpath = slintpath.to_string_lossy(),
path = testcase.absolute_path.to_string_lossy()
)?;
let source = std::fs::read_to_string(&testcase.absolute_path)?;
let include_paths = test_driver_lib::extract_include_paths(&source);
let library_paths = test_driver_lib::extract_library_paths(&source)
.map(|(k, v)| {
let mut abs_path = testcase.absolute_path.clone();
abs_path.pop();
abs_path.push(v);
format!("{}={}", k, abs_path.to_string_lossy())
})
.collect::<Vec<_>>();
for x in test_driver_lib::extract_test_functions(&source).filter(|x| x.language_id == "js") {
write!(main_js, "{{\n {}\n}}\n", x.source.replace("\n", "\n "))?;
}
let output = std::process::Command::new("node")
.arg(dir.path().join("main.js"))
.current_dir(dir.path())
.env("SLINT_INCLUDE_PATH", std::env::join_paths(include_paths).unwrap())
.env("SLINT_LIBRARY_PATH", std::env::join_paths(library_paths).unwrap())
.env("SLINT_SCALE_FACTOR", "1") // We don't have a testing backend, but we can try to force a SF1 as the tests expect.
.env("SLINT_ENABLE_EXPERIMENTAL_FEATURES", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|err| format!("Could not launch npm start: {}", err))?;
if !output.status.success() {
print!("{}", String::from_utf8_lossy(output.stdout.as_ref()));
print!("{}", String::from_utf8_lossy(output.stderr.as_ref()));
return Err(String::from_utf8_lossy(output.stderr.as_ref()).to_owned().into());
}
Ok(())
}

View file

@ -19,7 +19,7 @@ name = "test-driver-nodejs"
[dev-dependencies] [dev-dependencies]
test_driver_lib = { path = "../driverlib" } test_driver_lib = { path = "../driverlib" }
# Require `artifact` dependencies tracked by rust RFC 3028 # Require `artifact` dependencies tracked by rust RFC 3028
#slint-node = { path = "../../../api/node/native", artifact = ["cdylib"] } #slint-node = { path = "../../../api/node", artifact = ["cdylib"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
which = "4.0.2" which = "4.0.2"
tempfile = "3.2" tempfile = "3.2"

View file

@ -4,16 +4,6 @@
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
fn os_dylib_prefix_and_suffix() -> (&'static str, &'static str) {
if cfg!(target_os = "windows") {
("", "dll")
} else if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
("lib", "dylib")
} else {
("lib", "so")
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
// target/{debug|release}/build/package/out/ -> target/{debug|release} // target/{debug|release}/build/package/out/ -> target/{debug|release}
let mut target_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); let mut target_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
@ -21,14 +11,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
target_dir.pop(); target_dir.pop();
target_dir.pop(); target_dir.pop();
let nodejs_native_lib_name = {
let (prefix, suffix) = os_dylib_prefix_and_suffix();
format!("{}slint_node_native.{}", prefix, suffix)
};
println!(
"cargo:rustc-env=SLINT_NODE_NATIVE_LIB={}",
target_dir.join(nodejs_native_lib_name).display()
);
println!("cargo:rustc-env=SLINT_ENABLE_EXPERIMENTAL_FEATURES=1",); println!("cargo:rustc-env=SLINT_ENABLE_EXPERIMENTAL_FEATURES=1",);
let tests_file_path = let tests_file_path =

View file

@ -12,10 +12,9 @@ lazy_static::lazy_static! {
// it tries to emulate CreateProcess. // it tries to emulate CreateProcess.
let npm = which::which("npm").unwrap(); let npm = which::which("npm").unwrap();
// Ensure TypeScript is installed // builds and installs the slint node package
std::process::Command::new(npm.clone()) std::process::Command::new(npm.clone())
.arg("install") .arg("install")
.arg("--ignore-scripts")
.arg("--no-audit") .arg("--no-audit")
.current_dir(node_dir.clone()) .current_dir(node_dir.clone())
.stdout(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped())
@ -23,17 +22,7 @@ lazy_static::lazy_static! {
.output() .output()
.map_err(|err| format!("Could not launch npm install: {}", err)).unwrap(); .map_err(|err| format!("Could not launch npm install: {}", err)).unwrap();
// Build the .js file of the NodeJS API from the .ts file node_dir.join("index.js")
std::process::Command::new(npm)
.arg("run")
.arg("build")
.current_dir(node_dir.clone())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|err| format!("Could not launch npm run build: {}", err)).unwrap();
node_dir.join("dist").join("index.js")
}; };
} }
@ -48,7 +37,7 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
r#" r#"
const assert = require('assert').strict; const assert = require('assert').strict;
let slintlib = require(String.raw`{slintpath}`); let slintlib = require(String.raw`{slintpath}`);
let slint = require(String.raw`{path}`); let slint = slintlib.loadFile(String.raw`{path}`);
"#, "#,
slintpath = slintpath.to_string_lossy(), slintpath = slintpath.to_string_lossy(),
path = testcase.absolute_path.to_string_lossy() path = testcase.absolute_path.to_string_lossy()
@ -70,7 +59,6 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
let output = std::process::Command::new("node") let output = std::process::Command::new("node")
.arg(dir.path().join("main.js")) .arg(dir.path().join("main.js"))
.current_dir(dir.path()) .current_dir(dir.path())
.env("SLINT_NODE_NATIVE_LIB", std::env::var_os("SLINT_NODE_NATIVE_LIB").unwrap())
.env("SLINT_INCLUDE_PATH", std::env::join_paths(include_paths).unwrap()) .env("SLINT_INCLUDE_PATH", std::env::join_paths(include_paths).unwrap())
.env("SLINT_LIBRARY_PATH", std::env::join_paths(library_paths).unwrap()) .env("SLINT_LIBRARY_PATH", std::env::join_paths(library_paths).unwrap())
.env("SLINT_SCALE_FACTOR", "1") // We don't have a testing backend, but we can try to force a SF1 as the tests expect. .env("SLINT_SCALE_FACTOR", "1") // We don't have a testing backend, but we can try to force a SF1 as the tests expect.