mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-29 13:24:48 +00:00
Improve source structure in the node api (#6164)
* Update api/node/typescript/models.ts Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev> * Code review feedback --------- Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>
This commit is contained in:
parent
a2ded25914
commit
25ae55b5dd
30 changed files with 528 additions and 450 deletions
|
@ -100,7 +100,6 @@ All notable changes to this project are documented in this file.
|
|||
- Skia renderer: Improve rendering quality of layers
|
||||
- GridLayout: Fixed panic when rowspan or colspan is 0 (#6181)
|
||||
|
||||
|
||||
## [1.7.2] - 2024-08-14
|
||||
|
||||
### General
|
||||
|
|
1
api/node/.gitignore
vendored
1
api/node/.gitignore
vendored
|
@ -3,3 +3,4 @@ rust-module.d.ts
|
|||
index.js
|
||||
docs/
|
||||
npm/
|
||||
dist/
|
||||
|
|
|
@ -18,6 +18,7 @@ build = "build.rs"
|
|||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
path = "rust/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["backend-winit", "renderer-femtovg", "renderer-software", "backend-qt", "accessibility"]
|
||||
|
|
|
@ -5,7 +5,7 @@ import test from "ava";
|
|||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { loadFile, loadSource, CompileError } from "../index.js";
|
||||
import { loadFile, loadSource, CompileError } from "../dist/index.js";
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
@ -81,7 +81,6 @@ test("loadFile constructor parameters", (t) => {
|
|||
});
|
||||
|
||||
test("loadFile component instances and modules are sealed", (t) => {
|
||||
"use strict";
|
||||
const demo = loadFile(path.join(dirname, "resources/test.slint")) as any;
|
||||
|
||||
t.throws(
|
||||
|
@ -182,7 +181,6 @@ test("loadSource constructor parameters", (t) => {
|
|||
});
|
||||
|
||||
test("loadSource component instances and modules are sealed", (t) => {
|
||||
"use strict";
|
||||
const source = `export component Test {
|
||||
out property <string> check: "Test";
|
||||
}`;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import test from "ava";
|
||||
|
||||
import { private_api } from "../index.js";
|
||||
import { private_api } from "../dist/index.js";
|
||||
|
||||
test("get/set include paths", (t) => {
|
||||
const compiler = new private_api.ComponentCompiler();
|
||||
|
|
|
@ -7,7 +7,7 @@ import test from "ava";
|
|||
import * as http from "node:http";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import { runEventLoop, quitEventLoop, private_api } from "../index.js";
|
||||
import { runEventLoop, quitEventLoop, private_api } from "../dist/index.js";
|
||||
|
||||
test.serial("merged event loops with timer", async (t) => {
|
||||
let invoked = false;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import test from "ava";
|
||||
|
||||
import { private_api } from "../index.js";
|
||||
import { private_api } from "../dist/index.js";
|
||||
|
||||
test("get/set global properties", (t) => {
|
||||
const compiler = new private_api.ComponentCompiler();
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
type ImageData,
|
||||
ArrayModel,
|
||||
type Model,
|
||||
} from "../index.js";
|
||||
} from "../dist/index.js";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
@ -598,11 +598,12 @@ test("MapModel", (t) => {
|
|||
|
||||
instance!.setProperty("model", mapModel);
|
||||
|
||||
nameModel.setRowData(1, { first: "Simon", last: "Hausmann" });
|
||||
nameModel.setRowData(0, { first: "Simon", last: "Hausmann" });
|
||||
nameModel.setRowData(1, { first: "Olivier", last: "Goffart" });
|
||||
|
||||
const checkModel = instance!.getProperty("model") as Model<string>;
|
||||
t.is(checkModel.rowData(0), "Emil, Hans");
|
||||
t.is(checkModel.rowData(1), "Hausmann, Simon");
|
||||
t.is(checkModel.rowData(0), "Hausmann, Simon");
|
||||
t.is(checkModel.rowData(1), "Goffart, Olivier");
|
||||
t.is(checkModel.rowData(2), "Tisch, Roman");
|
||||
});
|
||||
|
||||
|
|
64
api/node/__test__/models.spec.mts
Normal file
64
api/node/__test__/models.spec.mts
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
import test from "ava";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
loadFile,
|
||||
loadSource,
|
||||
CompileError,
|
||||
ArrayModel,
|
||||
private_api,
|
||||
Model,
|
||||
} from "../dist/index.js";
|
||||
|
||||
test("MapModel notify rowChanged", (t) => {
|
||||
const source = `
|
||||
export component App {
|
||||
|
||||
in-out property <[string]> model;
|
||||
in-out property <string> changed-items;
|
||||
|
||||
for item in root.model : Text {
|
||||
text: item;
|
||||
|
||||
changed text => {
|
||||
root.changed-items += self.text;
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const path = "api.spec.ts";
|
||||
|
||||
private_api.initTesting();
|
||||
const demo = loadSource(source, path) as any;
|
||||
const instance = new demo.App();
|
||||
|
||||
interface Name {
|
||||
first: string;
|
||||
last: string;
|
||||
}
|
||||
|
||||
const nameModel: ArrayModel<Name> = new ArrayModel([
|
||||
{ first: "Hans", last: "Emil" },
|
||||
{ first: "Max", last: "Mustermann" },
|
||||
{ first: "Roman", last: "Tisch" },
|
||||
]);
|
||||
|
||||
const mapModel = new private_api.MapModel(nameModel, (data) => {
|
||||
return data.last + ", " + data.first;
|
||||
});
|
||||
|
||||
instance.model = mapModel;
|
||||
|
||||
private_api.send_mouse_click(instance, 5, 5);
|
||||
|
||||
nameModel.setRowData(0, { first: "Simon", last: "Hausmann" });
|
||||
nameModel.setRowData(1, { first: "Olivier", last: "Goffart" });
|
||||
|
||||
private_api.send_mouse_click(instance, 5, 5);
|
||||
|
||||
t.is(instance.changed_items, "Goffart, OlivierHausmann, Simon");
|
||||
});
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import test from "ava";
|
||||
|
||||
import { private_api, ArrayModel } from "../index.js";
|
||||
import { private_api, ArrayModel } from "../dist/index.js";
|
||||
|
||||
test("SlintColor from fromRgb", (t) => {
|
||||
const color = private_api.SlintRgbaColor.fromRgb(100, 110, 120);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import test from "ava";
|
||||
|
||||
import { private_api, Window } from "../index.js";
|
||||
import { private_api, Window } from "../dist/index.js";
|
||||
|
||||
test("Window constructor", (t) => {
|
||||
t.throws(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "slint-ui",
|
||||
"version": "1.9.0",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"homepage": "https://github.com/slint-ui/slint",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"repository": {
|
||||
|
@ -20,8 +20,8 @@
|
|||
],
|
||||
"description": "Slint is a declarative GUI toolkit to build native user interfaces for desktop and embedded applications.",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@ava/typescript": "^4.1.0",
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@types/node": "^20.8.6",
|
||||
"@types/node-fetch": "^2.6.7",
|
||||
"ava": "^5.3.0",
|
||||
|
@ -40,7 +40,7 @@
|
|||
"build:debug": "napi build --platform --js rust-module.cjs --dts rust-module.d.ts -c binaries.json && npm run compile",
|
||||
"build:testing": "napi build --platform --js rust-module.cjs --dts rust-module.d.ts -c binaries.json --features testing && npm run compile",
|
||||
"install": "node build-on-demand.mjs",
|
||||
"docs": "npm run build && typedoc --hideGenerator --treatWarningsAsErrors --readme cover.md index.ts",
|
||||
"docs": "npm run build && typedoc --hideGenerator --treatWarningsAsErrors --readme cover.md typescript/index.ts",
|
||||
"check": "biome check",
|
||||
"format": "biome format",
|
||||
"format:fix": "biome format --write",
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
"module": "CommonJS",
|
||||
"target": "esnext",
|
||||
"declaration": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"index.ts"
|
||||
"typescript/"
|
||||
],
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
import * as napi from "./rust-module.cjs";
|
||||
import * as napi from "../rust-module.cjs";
|
||||
export {
|
||||
Diagnostic,
|
||||
DiagnosticLevel,
|
||||
RgbaColor,
|
||||
Brush,
|
||||
} from "./rust-module";
|
||||
} from "../rust-module.cjs";
|
||||
|
||||
import { Diagnostic } from "./rust-module.cjs";
|
||||
import { Model } from "./models";
|
||||
export { Model };
|
||||
|
||||
export { ArrayModel } from "./models";
|
||||
|
||||
import { Diagnostic } from "../rust-module.cjs";
|
||||
|
||||
/**
|
||||
* Represents a two-dimensional point.
|
||||
|
@ -115,434 +120,6 @@ export interface ImageData {
|
|||
get height(): number;
|
||||
}
|
||||
|
||||
class ModelIterator<T> implements Iterator<T> {
|
||||
private row: number;
|
||||
private model: Model<T>;
|
||||
|
||||
constructor(model: Model<T>) {
|
||||
this.model = model;
|
||||
this.row = 0;
|
||||
}
|
||||
|
||||
public next(): IteratorResult<T> {
|
||||
if (this.row < this.model.rowCount()) {
|
||||
const row = this.row;
|
||||
this.row++;
|
||||
return {
|
||||
done: false,
|
||||
value: this.model.rowData(row),
|
||||
};
|
||||
}
|
||||
return {
|
||||
done: true,
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @template T the type of the model's items.
|
||||
*
|
||||
* ### Example
|
||||
* As an example let's see the implementation of {@link ArrayModel}
|
||||
*
|
||||
* ```js
|
||||
* export class ArrayModel<T> extends Model<T> {
|
||||
* private a: Array<T>
|
||||
*
|
||||
* constructor(arr: Array<T>) {
|
||||
* super();
|
||||
* this.a = arr;
|
||||
* }
|
||||
*
|
||||
* rowCount() {
|
||||
* return this.a.length;
|
||||
* }
|
||||
*
|
||||
* rowData(row: number) {
|
||||
* return this.a[row];
|
||||
* }
|
||||
*
|
||||
* setRowData(row: number, data: T) {
|
||||
* this.a[row] = data;
|
||||
* this.notifyRowDataChanged(row);
|
||||
* }
|
||||
*
|
||||
* push(...values: T[]) {
|
||||
* let size = this.a.length;
|
||||
* Array.prototype.push.apply(this.a, values);
|
||||
* this.notifyRowAdded(size, arguments.length);
|
||||
* }
|
||||
*
|
||||
* remove(index: number, size: number) {
|
||||
* let r = this.a.splice(index, size);
|
||||
* this.notifyRowRemoved(index, size);
|
||||
* }
|
||||
*
|
||||
* get length(): number {
|
||||
* return this.a.length;
|
||||
* }
|
||||
*
|
||||
* values(): IterableIterator<T> {
|
||||
* return this.a.values();
|
||||
* }
|
||||
*
|
||||
* entries(): IterableIterator<[number, T]> {
|
||||
* return this.a.entries()
|
||||
* }
|
||||
*}
|
||||
* ```
|
||||
*/
|
||||
export abstract class Model<T> implements Iterable<T> {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
modelNotify: napi.ExternalObject<napi.SharedModelNotify>;
|
||||
|
||||
constructor() {
|
||||
this.modelNotify = napi.jsModelNotifyNew(this);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Returns a new Model where all elements are mapped by the function `mapFunction`.
|
||||
// * @template T the type of the source model's items.
|
||||
// * @param mapFunction functions that maps
|
||||
// * @returns a new {@link MapModel} that wraps the current model.
|
||||
// */
|
||||
// map<U>(
|
||||
// mapFunction: (data: T) => U
|
||||
// ): MapModel<T, U> {
|
||||
// return new MapModel(this, mapFunction);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Implementations of this function must return the current number of rows.
|
||||
*/
|
||||
abstract rowCount(): number;
|
||||
/**
|
||||
* Implementations of this function must return the data at the specified row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @returns undefined if row is out of range otherwise the data.
|
||||
*/
|
||||
abstract rowData(row: number): T | undefined;
|
||||
|
||||
/**
|
||||
* Implementations of this function must store the provided data parameter
|
||||
* in the model at the specified row.
|
||||
* @param _row index in range 0..(rowCount() - 1).
|
||||
* @param _data new data item to store on the given row index
|
||||
*/
|
||||
setRowData(_row: number, _data: T): void {
|
||||
console.log(
|
||||
"setRowData called on a model which does not re-implement this method. This happens when trying to modify a read-only model",
|
||||
);
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<T> {
|
||||
return new ModelIterator(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that the data of the current row is changed.
|
||||
* @param row index of the changed row.
|
||||
*/
|
||||
protected notifyRowDataChanged(row: number): void {
|
||||
napi.jsModelNotifyRowDataChanged(this.modelNotify, row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that multiple rows are added to the model.
|
||||
* @param row index of the first added row.
|
||||
* @param count the number of added items.
|
||||
*/
|
||||
protected notifyRowAdded(row: number, count: number): void {
|
||||
napi.jsModelNotifyRowAdded(this.modelNotify, row, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that multiple rows are removed to the model.
|
||||
* @param row index of the first removed row.
|
||||
* @param count the number of removed items.
|
||||
*/
|
||||
protected notifyRowRemoved(row: number, count: number): void {
|
||||
napi.jsModelNotifyRowRemoved(this.modelNotify, row, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that the complete data must be reload.
|
||||
*/
|
||||
protected notifyReset(): void {
|
||||
napi.jsModelNotifyReset(this.modelNotify);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayModel wraps a JavaScript array for use in `.slint` views. The underlying
|
||||
* array can be modified with the [[ArrayModel.push]] and [[ArrayModel.remove]] methods.
|
||||
*/
|
||||
export class ArrayModel<T> extends Model<T> {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
#array: Array<T>;
|
||||
|
||||
/**
|
||||
* Creates a new ArrayModel.
|
||||
*
|
||||
* @param arr
|
||||
*/
|
||||
constructor(arr: Array<T>) {
|
||||
super();
|
||||
this.#array = arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the array model.
|
||||
*/
|
||||
get length(): number {
|
||||
return this.#array.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the array model.
|
||||
*/
|
||||
rowCount() {
|
||||
return this.#array.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data at the specified row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @returns undefined if row is out of range otherwise the data.
|
||||
*/
|
||||
rowData(row: number) {
|
||||
return this.#array[row];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given data on the given row index and notifies run-time about the changed row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @param data new data item to store on the given row index
|
||||
*/
|
||||
setRowData(row: number, data: T) {
|
||||
this.#array[row] = data;
|
||||
this.notifyRowDataChanged(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes new values to the array that's backing the model and notifies
|
||||
* the run-time about the added rows.
|
||||
* @param values list of values that will be pushed to the array.
|
||||
*/
|
||||
push(...values: T[]) {
|
||||
const size = this.#array.length;
|
||||
Array.prototype.push.apply(this.#array, values);
|
||||
this.notifyRowAdded(size, arguments.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last element from the array and returns it.
|
||||
*
|
||||
* @returns The removed element or undefined if the array is empty.
|
||||
*/
|
||||
pop(): T | undefined {
|
||||
const last = this.#array.pop();
|
||||
if (last !== undefined) {
|
||||
this.notifyRowRemoved(this.#array.length, 1);
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
// 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.
|
||||
* @param index index of first row to remove.
|
||||
* @param size number of rows to remove.
|
||||
*/
|
||||
remove(index: number, size: number) {
|
||||
const r = this.#array.splice(index, size);
|
||||
this.notifyRowRemoved(index, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable of values in the array.
|
||||
*/
|
||||
values(): IterableIterator<T> {
|
||||
return this.#array.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable of key, value pairs for every entry in the array.
|
||||
*/
|
||||
entries(): IterableIterator<[number, T]> {
|
||||
return this.#array.entries();
|
||||
}
|
||||
}
|
||||
|
||||
export namespace private_api {
|
||||
/**
|
||||
* Provides rows that are generated by a map function based on the rows of another Model.
|
||||
*
|
||||
* @template T item type of source model that is mapped to U.
|
||||
* @template U the type of the mapped items
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* Here we have a {@link ArrayModel} holding rows of a custom interface `Name` and a {@link MapModel} that maps the name rows
|
||||
* to single string rows.
|
||||
*
|
||||
* ```ts
|
||||
* import { Model, ArrayModel, MapModel } from "./index";
|
||||
*
|
||||
* interface Name {
|
||||
* first: string;
|
||||
* last: string;
|
||||
* }
|
||||
*
|
||||
* const model = new ArrayModel<Name>([
|
||||
* {
|
||||
* first: "Hans",
|
||||
* last: "Emil",
|
||||
* },
|
||||
* {
|
||||
* first: "Max",
|
||||
* last: "Mustermann",
|
||||
* },
|
||||
* {
|
||||
* first: "Roman",
|
||||
* last: "Tisch",
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* const mappedModel = new MapModel(
|
||||
* model,
|
||||
* (data) => {
|
||||
* return data.last + ", " + data.first;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* // prints "Emil, Hans"
|
||||
* console.log(mappedModel.rowData(0));
|
||||
*
|
||||
* // prints "Mustermann, Max"
|
||||
* console.log(mappedModel.rowData(1));
|
||||
*
|
||||
* // prints "Tisch, Roman"
|
||||
* console.log(mappedModel.rowData(2));
|
||||
*
|
||||
* // Alternatively you can use the shortcut {@link MapModel.map}.
|
||||
*
|
||||
* const model = new ArrayModel<Name>([
|
||||
* {
|
||||
* first: "Hans",
|
||||
* last: "Emil",
|
||||
* },
|
||||
* {
|
||||
* first: "Max",
|
||||
* last: "Mustermann",
|
||||
* },
|
||||
* {
|
||||
* first: "Roman",
|
||||
* last: "Tisch",
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* const mappedModel = model.map(
|
||||
* (data) => {
|
||||
* return data.last + ", " + data.first;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
*
|
||||
* // prints "Emil, Hans"
|
||||
* console.log(mappedModel.rowData(0));
|
||||
*
|
||||
* // prints "Mustermann, Max"
|
||||
* console.log(mappedModel.rowData(1));
|
||||
*
|
||||
* // prints "Tisch, Roman"
|
||||
* console.log(mappedModel.rowData(2));
|
||||
*
|
||||
* // You can modifying the underlying {@link ArrayModel}:
|
||||
*
|
||||
* const model = new ArrayModel<Name>([
|
||||
* {
|
||||
* first: "Hans",
|
||||
* last: "Emil",
|
||||
* },
|
||||
* {
|
||||
* first: "Max",
|
||||
* last: "Mustermann",
|
||||
* },
|
||||
* {
|
||||
* first: "Roman",
|
||||
* last: "Tisch",
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* const mappedModel = model.map(
|
||||
* (data) => {
|
||||
* return data.last + ", " + data.first;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* model.setRowData(1, { first: "Minnie", last: "Musterfrau" } );
|
||||
*
|
||||
* // prints "Emil, Hans"
|
||||
* console.log(mappedModel.rowData(0));
|
||||
*
|
||||
* // prints "Musterfrau, Minnie"
|
||||
* console.log(mappedModel.rowData(1));
|
||||
*
|
||||
* // prints "Tisch, Roman"
|
||||
* console.log(mappedModel.rowData(2));
|
||||
* ```
|
||||
*/
|
||||
export class MapModel<T, U> extends Model<U> {
|
||||
readonly sourceModel: Model<T>;
|
||||
#mapFunction: (data: T) => U;
|
||||
|
||||
/**
|
||||
* Constructs the MapModel with a source model and map functions.
|
||||
* @template T item type of source model that is mapped to U.
|
||||
* @template U the type of the mapped items.
|
||||
* @param sourceModel the wrapped model.
|
||||
* @param mapFunction maps the data from T to U.
|
||||
*/
|
||||
constructor(sourceModel: Model<T>, mapFunction: (data: T) => U) {
|
||||
super();
|
||||
this.sourceModel = sourceModel;
|
||||
this.#mapFunction = mapFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the model.
|
||||
*/
|
||||
rowCount(): number {
|
||||
return this.sourceModel.rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data at the specified row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @returns undefined if row is out of range otherwise the data.
|
||||
*/
|
||||
rowData(row: number): U | undefined {
|
||||
const data = this.sourceModel.rowData(row);
|
||||
if (data === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return this.#mapFunction(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This interface describes the public API of a Slint component that is common to all instances. Use this to
|
||||
* show() the window on the screen, access the window and subsequent window properties, or start the
|
||||
|
@ -1160,6 +737,164 @@ export function quitEventLoop() {
|
|||
globalEventLoop.quit();
|
||||
}
|
||||
|
||||
export namespace private_api {
|
||||
/**
|
||||
* Provides rows that are generated by a map function based on the rows of another Model.
|
||||
*
|
||||
* @template T item type of source model that is mapped to U.
|
||||
* @template U the type of the mapped items
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* Here we have a {@link ArrayModel} holding rows of a custom interface `Name` and a {@link MapModel} that maps the name rows
|
||||
* to single string rows.
|
||||
*
|
||||
* ```ts
|
||||
* import { Model, ArrayModel, MapModel } from "./index";
|
||||
*
|
||||
* interface Name {
|
||||
* first: string;
|
||||
* last: string;
|
||||
* }
|
||||
*
|
||||
* const model = new ArrayModel<Name>([
|
||||
* {
|
||||
* first: "Hans",
|
||||
* last: "Emil",
|
||||
* },
|
||||
* {
|
||||
* first: "Max",
|
||||
* last: "Mustermann",
|
||||
* },
|
||||
* {
|
||||
* first: "Roman",
|
||||
* last: "Tisch",
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* const mappedModel = new MapModel(
|
||||
* model,
|
||||
* (data) => {
|
||||
* return data.last + ", " + data.first;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* // prints "Emil, Hans"
|
||||
* console.log(mappedModel.rowData(0));
|
||||
*
|
||||
* // prints "Mustermann, Max"
|
||||
* console.log(mappedModel.rowData(1));
|
||||
*
|
||||
* // prints "Tisch, Roman"
|
||||
* console.log(mappedModel.rowData(2));
|
||||
*
|
||||
* // Alternatively you can use the shortcut {@link MapModel.map}.
|
||||
*
|
||||
* const model = new ArrayModel<Name>([
|
||||
* {
|
||||
* first: "Hans",
|
||||
* last: "Emil",
|
||||
* },
|
||||
* {
|
||||
* first: "Max",
|
||||
* last: "Mustermann",
|
||||
* },
|
||||
* {
|
||||
* first: "Roman",
|
||||
* last: "Tisch",
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* const mappedModel = model.map(
|
||||
* (data) => {
|
||||
* return data.last + ", " + data.first;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
*
|
||||
* // prints "Emil, Hans"
|
||||
* console.log(mappedModel.rowData(0));
|
||||
*
|
||||
* // prints "Mustermann, Max"
|
||||
* console.log(mappedModel.rowData(1));
|
||||
*
|
||||
* // prints "Tisch, Roman"
|
||||
* console.log(mappedModel.rowData(2));
|
||||
*
|
||||
* // You can modifying the underlying {@link ArrayModel}:
|
||||
*
|
||||
* const model = new ArrayModel<Name>([
|
||||
* {
|
||||
* first: "Hans",
|
||||
* last: "Emil",
|
||||
* },
|
||||
* {
|
||||
* first: "Max",
|
||||
* last: "Mustermann",
|
||||
* },
|
||||
* {
|
||||
* first: "Roman",
|
||||
* last: "Tisch",
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* const mappedModel = model.map(
|
||||
* (data) => {
|
||||
* return data.last + ", " + data.first;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* model.setRowData(1, { first: "Minnie", last: "Musterfrau" } );
|
||||
*
|
||||
* // prints "Emil, Hans"
|
||||
* console.log(mappedModel.rowData(0));
|
||||
*
|
||||
* // prints "Musterfrau, Minnie"
|
||||
* console.log(mappedModel.rowData(1));
|
||||
*
|
||||
* // prints "Tisch, Roman"
|
||||
* console.log(mappedModel.rowData(2));
|
||||
* ```
|
||||
*/
|
||||
export class MapModel<T, U> extends Model<U> {
|
||||
readonly sourceModel: Model<T>;
|
||||
#mapFunction: (data: T) => U;
|
||||
|
||||
/**
|
||||
* Constructs the MapModel with a source model and map functions.
|
||||
* @template T item type of source model that is mapped to U.
|
||||
* @template U the type of the mapped items.
|
||||
* @param sourceModel the wrapped model.
|
||||
* @param mapFunction maps the data from T to U.
|
||||
*/
|
||||
constructor(sourceModel: Model<T>, mapFunction: (data: T) => U) {
|
||||
super(sourceModel.modelNotify);
|
||||
this.sourceModel = sourceModel;
|
||||
this.#mapFunction = mapFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the model.
|
||||
*/
|
||||
rowCount(): number {
|
||||
return this.sourceModel.rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data at the specified row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @returns undefined if row is out of range otherwise the data.
|
||||
*/
|
||||
rowData(row: number): U | undefined {
|
||||
const data = this.sourceModel.rowData(row);
|
||||
if (data === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return this.#mapFunction(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
278
api/node/typescript/models.ts
Normal file
278
api/node/typescript/models.ts
Normal file
|
@ -0,0 +1,278 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
import * as napi from "../rust-module.cjs";
|
||||
|
||||
class ModelIterator<T> implements Iterator<T> {
|
||||
private row: number;
|
||||
private model: Model<T>;
|
||||
|
||||
constructor(model: Model<T>) {
|
||||
this.model = model;
|
||||
this.row = 0;
|
||||
}
|
||||
|
||||
public next(): IteratorResult<T> {
|
||||
if (this.row < this.model.rowCount()) {
|
||||
const row = this.row;
|
||||
this.row++;
|
||||
return {
|
||||
done: false,
|
||||
value: this.model.rowData(row),
|
||||
};
|
||||
}
|
||||
return {
|
||||
done: true,
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @template T the type of the model's items.
|
||||
*
|
||||
* ### Example
|
||||
* As an example let's see the implementation of {@link ArrayModel}
|
||||
*
|
||||
* ```js
|
||||
* export class ArrayModel<T> extends Model<T> {
|
||||
* private a: Array<T>
|
||||
*
|
||||
* constructor(arr: Array<T>) {
|
||||
* super();
|
||||
* this.a = arr;
|
||||
* }
|
||||
*
|
||||
* rowCount() {
|
||||
* return this.a.length;
|
||||
* }
|
||||
*
|
||||
* rowData(row: number) {
|
||||
* return this.a[row];
|
||||
* }
|
||||
*
|
||||
* setRowData(row: number, data: T) {
|
||||
* this.a[row] = data;
|
||||
* this.notifyRowDataChanged(row);
|
||||
* }
|
||||
*
|
||||
* push(...values: T[]) {
|
||||
* let size = this.a.length;
|
||||
* Array.prototype.push.apply(this.a, values);
|
||||
* this.notifyRowAdded(size, arguments.length);
|
||||
* }
|
||||
*
|
||||
* remove(index: number, size: number) {
|
||||
* let r = this.a.splice(index, size);
|
||||
* this.notifyRowRemoved(index, size);
|
||||
* }
|
||||
*
|
||||
* get length(): number {
|
||||
* return this.a.length;
|
||||
* }
|
||||
*
|
||||
* values(): IterableIterator<T> {
|
||||
* return this.a.values();
|
||||
* }
|
||||
*
|
||||
* entries(): IterableIterator<[number, T]> {
|
||||
* return this.a.entries()
|
||||
* }
|
||||
*}
|
||||
* ```
|
||||
*/
|
||||
export abstract class Model<T> implements Iterable<T> {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
modelNotify: napi.ExternalObject<napi.SharedModelNotify>;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
constructor(modelNotify?: napi.ExternalObject<napi.SharedModelNotify>) {
|
||||
this.modelNotify = modelNotify ?? napi.jsModelNotifyNew(this);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Returns a new Model where all elements are mapped by the function `mapFunction`.
|
||||
// * @template T the type of the source model's items.
|
||||
// * @param mapFunction functions that maps
|
||||
// * @returns a new {@link MapModel} that wraps the current model.
|
||||
// */
|
||||
// map<U>(
|
||||
// mapFunction: (data: T) => U
|
||||
// ): MapModel<T, U> {
|
||||
// return new MapModel(this, mapFunction);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Implementations of this function must return the current number of rows.
|
||||
*/
|
||||
abstract rowCount(): number;
|
||||
/**
|
||||
* Implementations of this function must return the data at the specified row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @returns undefined if row is out of range otherwise the data.
|
||||
*/
|
||||
abstract rowData(row: number): T | undefined;
|
||||
|
||||
/**
|
||||
* Implementations of this function must store the provided data parameter
|
||||
* in the model at the specified row.
|
||||
* @param _row index in range 0..(rowCount() - 1).
|
||||
* @param _data new data item to store on the given row index
|
||||
*/
|
||||
setRowData(_row: number, _data: T): void {
|
||||
console.log(
|
||||
"setRowData called on a model which does not re-implement this method. This happens when trying to modify a read-only model",
|
||||
);
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<T> {
|
||||
return new ModelIterator(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that the data of the current row is changed.
|
||||
* @param row index of the changed row.
|
||||
*/
|
||||
protected notifyRowDataChanged(row: number): void {
|
||||
napi.jsModelNotifyRowDataChanged(this.modelNotify, row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that multiple rows are added to the model.
|
||||
* @param row index of the first added row.
|
||||
* @param count the number of added items.
|
||||
*/
|
||||
protected notifyRowAdded(row: number, count: number): void {
|
||||
napi.jsModelNotifyRowAdded(this.modelNotify, row, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that multiple rows are removed to the model.
|
||||
* @param row index of the first removed row.
|
||||
* @param count the number of removed items.
|
||||
*/
|
||||
protected notifyRowRemoved(row: number, count: number): void {
|
||||
napi.jsModelNotifyRowRemoved(this.modelNotify, row, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the view that the complete data must be reload.
|
||||
*/
|
||||
protected notifyReset(): void {
|
||||
napi.jsModelNotifyReset(this.modelNotify);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayModel wraps a JavaScript array for use in `.slint` views. The underlying
|
||||
* array can be modified with the [[ArrayModel.push]] and [[ArrayModel.remove]] methods.
|
||||
*/
|
||||
export class ArrayModel<T> extends Model<T> {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
#array: Array<T>;
|
||||
|
||||
/**
|
||||
* Creates a new ArrayModel.
|
||||
*
|
||||
* @param arr
|
||||
*/
|
||||
constructor(arr: Array<T>) {
|
||||
super();
|
||||
this.#array = arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the array model.
|
||||
*/
|
||||
get length(): number {
|
||||
return this.#array.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the array model.
|
||||
*/
|
||||
rowCount() {
|
||||
return this.#array.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data at the specified row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @returns undefined if row is out of range otherwise the data.
|
||||
*/
|
||||
rowData(row: number) {
|
||||
return this.#array[row];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given data on the given row index and notifies run-time about the changed row.
|
||||
* @param row index in range 0..(rowCount() - 1).
|
||||
* @param data new data item to store on the given row index
|
||||
*/
|
||||
setRowData(row: number, data: T) {
|
||||
this.#array[row] = data;
|
||||
this.notifyRowDataChanged(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes new values to the array that's backing the model and notifies
|
||||
* the run-time about the added rows.
|
||||
* @param values list of values that will be pushed to the array.
|
||||
*/
|
||||
push(...values: T[]) {
|
||||
const size = this.#array.length;
|
||||
Array.prototype.push.apply(this.#array, values);
|
||||
this.notifyRowAdded(size, arguments.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last element from the array and returns it.
|
||||
*
|
||||
* @returns The removed element or undefined if the array is empty.
|
||||
*/
|
||||
pop(): T | undefined {
|
||||
const last = this.#array.pop();
|
||||
if (last !== undefined) {
|
||||
this.notifyRowRemoved(this.#array.length, 1);
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
// 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.
|
||||
* @param index index of first row to remove.
|
||||
* @param size number of rows to remove.
|
||||
*/
|
||||
remove(index: number, size: number) {
|
||||
const r = this.#array.splice(index, size);
|
||||
this.notifyRowRemoved(index, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable of values in the array.
|
||||
*/
|
||||
values(): IterableIterator<T> {
|
||||
return this.#array.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable of key, value pairs for every entry in the array.
|
||||
*/
|
||||
entries(): IterableIterator<[number, T]> {
|
||||
return this.#array.entries();
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ lazy_static::lazy_static! {
|
|||
|
||||
check_output(o);
|
||||
|
||||
node_dir.join("index.js")
|
||||
node_dir.join("dist/index.js")
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue