From 77c124dc3e469769225f669c05b8bfe71e36a5c3 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Mon, 16 Sep 2024 12:18:27 +0200 Subject: [PATCH 1/8] Node API: improved structure, make MapModel public --- .gitignore | 2 + CHANGELOG.md | 1 - api/node/Cargo.toml | 1 + api/node/__test__/api.spec.mts | 2 +- api/node/__test__/compiler.spec.mts | 2 +- api/node/__test__/eventloop.spec.mts | 2 +- api/node/__test__/globals.spec.mts | 2 +- .../__test__/js_value_conversion.spec.mts | 16 +- api/node/__test__/types.spec.mts | 2 +- api/node/__test__/window.spec.mts | 2 +- api/node/package.json | 8 +- api/node/{src => rust}/interpreter.rs | 0 .../interpreter/component_compiler.rs | 0 .../interpreter/component_definition.rs | 0 .../interpreter/component_instance.rs | 0 .../{src => rust}/interpreter/diagnostic.rs | 0 api/node/{src => rust}/interpreter/value.rs | 0 api/node/{src => rust}/interpreter/window.rs | 0 api/node/{src => rust}/lib.rs | 0 api/node/{src => rust}/types.rs | 0 api/node/{src => rust}/types/brush.rs | 0 api/node/{src => rust}/types/image_data.rs | 0 api/node/{src => rust}/types/model.rs | 0 api/node/{src => rust}/types/point.rs | 0 api/node/{src => rust}/types/size.rs | 0 api/node/tsconfig.json | 3 +- api/node/{ => typescript}/index.ts | 436 +----------------- api/node/typescript/models.ts | 431 +++++++++++++++++ tests/driver/nodejs/nodejs.rs | 2 +- 29 files changed, 459 insertions(+), 453 deletions(-) rename api/node/{src => rust}/interpreter.rs (100%) rename api/node/{src => rust}/interpreter/component_compiler.rs (100%) rename api/node/{src => rust}/interpreter/component_definition.rs (100%) rename api/node/{src => rust}/interpreter/component_instance.rs (100%) rename api/node/{src => rust}/interpreter/diagnostic.rs (100%) rename api/node/{src => rust}/interpreter/value.rs (100%) rename api/node/{src => rust}/interpreter/window.rs (100%) rename api/node/{src => rust}/lib.rs (100%) rename api/node/{src => rust}/types.rs (100%) rename api/node/{src => rust}/types/brush.rs (100%) rename api/node/{src => rust}/types/image_data.rs (100%) rename api/node/{src => rust}/types/model.rs (100%) rename api/node/{src => rust}/types/point.rs (100%) rename api/node/{src => rust}/types/size.rs (100%) rename api/node/{ => typescript}/index.ts (70%) create mode 100644 api/node/typescript/models.ts diff --git a/.gitignore b/.gitignore index dbb47d0ac39..05f1e17dfc4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ docs/reference/src/language/builtins/structs.md *.node *.d.ts +api/node/dist/* + .env .envrc __pycache__ diff --git a/CHANGELOG.md b/CHANGELOG.md index e7bf15a5673..52fedff8f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/api/node/Cargo.toml b/api/node/Cargo.toml index 9ca39d1cc9a..8767b8c8e50 100644 --- a/api/node/Cargo.toml +++ b/api/node/Cargo.toml @@ -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"] diff --git a/api/node/__test__/api.spec.mts b/api/node/__test__/api.spec.mts index 2a1d6a3ae04..b94b49cdc2d 100644 --- a/api/node/__test__/api.spec.mts +++ b/api/node/__test__/api.spec.mts @@ -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)); diff --git a/api/node/__test__/compiler.spec.mts b/api/node/__test__/compiler.spec.mts index 935a546c11a..62b68b86e45 100644 --- a/api/node/__test__/compiler.spec.mts +++ b/api/node/__test__/compiler.spec.mts @@ -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(); diff --git a/api/node/__test__/eventloop.spec.mts b/api/node/__test__/eventloop.spec.mts index c951127a114..cb2a0e68c3a 100644 --- a/api/node/__test__/eventloop.spec.mts +++ b/api/node/__test__/eventloop.spec.mts @@ -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; diff --git a/api/node/__test__/globals.spec.mts b/api/node/__test__/globals.spec.mts index b931a8e0d7e..8c55a7ed639 100644 --- a/api/node/__test__/globals.spec.mts +++ b/api/node/__test__/globals.spec.mts @@ -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(); diff --git a/api/node/__test__/js_value_conversion.spec.mts b/api/node/__test__/js_value_conversion.spec.mts index e2cd6d39460..633cca10f0f 100644 --- a/api/node/__test__/js_value_conversion.spec.mts +++ b/api/node/__test__/js_value_conversion.spec.mts @@ -11,7 +11,8 @@ import { type ImageData, ArrayModel, type Model, -} from "../index.js"; + MapModel, +} from "../dist/index.js"; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -592,7 +593,7 @@ test("MapModel", (t) => { { first: "Roman", last: "Tisch" }, ]); - const mapModel = new private_api.MapModel(nameModel, (data) => { + const mapModel = new MapModel(nameModel, (data) => { return data.last + ", " + data.first; }); @@ -610,13 +611,10 @@ test("MapModel undefined rowData sourcemodel", (t) => { const nameModel: ArrayModel = new ArrayModel([1, 2, 3]); let mapFunctionCallCount = 0; - const mapModel = new private_api.MapModel( - nameModel, - (data) => { - mapFunctionCallCount++; - return data.toString(); - }, - ); + const mapModel = new MapModel(nameModel, (data) => { + mapFunctionCallCount++; + return data.toString(); + }); for (let i = 0; i < mapModel.rowCount(); ++i) { mapModel.rowData(i); diff --git a/api/node/__test__/types.spec.mts b/api/node/__test__/types.spec.mts index 943e04233b8..420d050e80c 100644 --- a/api/node/__test__/types.spec.mts +++ b/api/node/__test__/types.spec.mts @@ -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); diff --git a/api/node/__test__/window.spec.mts b/api/node/__test__/window.spec.mts index 71cf6d224e3..152932f8058 100644 --- a/api/node/__test__/window.spec.mts +++ b/api/node/__test__/window.spec.mts @@ -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( diff --git a/api/node/package.json b/api/node/package.json index 6091ce1d4df..ddbcfac6058 100644 --- a/api/node/package.json +++ b/api/node/package.json @@ -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", diff --git a/api/node/src/interpreter.rs b/api/node/rust/interpreter.rs similarity index 100% rename from api/node/src/interpreter.rs rename to api/node/rust/interpreter.rs diff --git a/api/node/src/interpreter/component_compiler.rs b/api/node/rust/interpreter/component_compiler.rs similarity index 100% rename from api/node/src/interpreter/component_compiler.rs rename to api/node/rust/interpreter/component_compiler.rs diff --git a/api/node/src/interpreter/component_definition.rs b/api/node/rust/interpreter/component_definition.rs similarity index 100% rename from api/node/src/interpreter/component_definition.rs rename to api/node/rust/interpreter/component_definition.rs diff --git a/api/node/src/interpreter/component_instance.rs b/api/node/rust/interpreter/component_instance.rs similarity index 100% rename from api/node/src/interpreter/component_instance.rs rename to api/node/rust/interpreter/component_instance.rs diff --git a/api/node/src/interpreter/diagnostic.rs b/api/node/rust/interpreter/diagnostic.rs similarity index 100% rename from api/node/src/interpreter/diagnostic.rs rename to api/node/rust/interpreter/diagnostic.rs diff --git a/api/node/src/interpreter/value.rs b/api/node/rust/interpreter/value.rs similarity index 100% rename from api/node/src/interpreter/value.rs rename to api/node/rust/interpreter/value.rs diff --git a/api/node/src/interpreter/window.rs b/api/node/rust/interpreter/window.rs similarity index 100% rename from api/node/src/interpreter/window.rs rename to api/node/rust/interpreter/window.rs diff --git a/api/node/src/lib.rs b/api/node/rust/lib.rs similarity index 100% rename from api/node/src/lib.rs rename to api/node/rust/lib.rs diff --git a/api/node/src/types.rs b/api/node/rust/types.rs similarity index 100% rename from api/node/src/types.rs rename to api/node/rust/types.rs diff --git a/api/node/src/types/brush.rs b/api/node/rust/types/brush.rs similarity index 100% rename from api/node/src/types/brush.rs rename to api/node/rust/types/brush.rs diff --git a/api/node/src/types/image_data.rs b/api/node/rust/types/image_data.rs similarity index 100% rename from api/node/src/types/image_data.rs rename to api/node/rust/types/image_data.rs diff --git a/api/node/src/types/model.rs b/api/node/rust/types/model.rs similarity index 100% rename from api/node/src/types/model.rs rename to api/node/rust/types/model.rs diff --git a/api/node/src/types/point.rs b/api/node/rust/types/point.rs similarity index 100% rename from api/node/src/types/point.rs rename to api/node/rust/types/point.rs diff --git a/api/node/src/types/size.rs b/api/node/rust/types/size.rs similarity index 100% rename from api/node/src/types/size.rs rename to api/node/rust/types/size.rs diff --git a/api/node/tsconfig.json b/api/node/tsconfig.json index f169667d70b..f7ff0850842 100644 --- a/api/node/tsconfig.json +++ b/api/node/tsconfig.json @@ -3,8 +3,9 @@ "module": "CommonJS", "target": "esnext", "declaration": true, + "outDir": "dist" }, "include": [ - "index.ts" + "typescript/" ], } diff --git a/api/node/index.ts b/api/node/typescript/index.ts similarity index 70% rename from api/node/index.ts rename to api/node/typescript/index.ts index d8636d556cd..3f39191cc73 100644 --- a/api/node/index.ts +++ b/api/node/typescript/index.ts @@ -1,15 +1,17 @@ // Copyright © SixtyFPS GmbH // 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"; +export { Model, ArrayModel, MapModel } from "./models"; + +import { Diagnostic } from "../rust-module.cjs"; /** * Represents a two-dimensional point. @@ -115,434 +117,6 @@ export interface ImageData { get height(): number; } -class ModelIterator implements Iterator { - private row: number; - private model: Model; - - constructor(model: Model) { - this.model = model; - this.row = 0; - } - - public next(): IteratorResult { - 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 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 extends Model { - * private a: Array - * - * constructor(arr: Array) { - * 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 { - * return this.a.values(); - * } - * - * entries(): IterableIterator<[number, T]> { - * return this.a.entries() - * } - *} - * ``` - */ -export abstract class Model implements Iterable { - /** - * @hidden - */ - modelNotify: napi.ExternalObject; - - 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( - // mapFunction: (data: T) => U - // ): MapModel { - // 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 { - 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 extends Model { - /** - * @hidden - */ - #array: Array; - - /** - * Creates a new ArrayModel. - * - * @param arr - */ - constructor(arr: Array) { - 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 { - 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([ - * { - * 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([ - * { - * 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([ - * { - * 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 extends Model { - readonly sourceModel: Model; - #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, 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 diff --git a/api/node/typescript/models.ts b/api/node/typescript/models.ts new file mode 100644 index 00000000000..3357184c253 --- /dev/null +++ b/api/node/typescript/models.ts @@ -0,0 +1,431 @@ +// Copyright © SixtyFPS GmbH +// 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 implements Iterator { + private row: number; + private model: Model; + + constructor(model: Model) { + this.model = model; + this.row = 0; + } + + public next(): IteratorResult { + 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 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 extends Model { + * private a: Array + * + * constructor(arr: Array) { + * 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 { + * return this.a.values(); + * } + * + * entries(): IterableIterator<[number, T]> { + * return this.a.entries() + * } + *} + * ``` + */ +export abstract class Model implements Iterable { + /** + * @hidden + */ + modelNotify: napi.ExternalObject; + + 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( + // mapFunction: (data: T) => U + // ): MapModel { + // 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 { + 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 extends Model { + /** + * @hidden + */ + #array: Array; + + /** + * Creates a new ArrayModel. + * + * @param arr + */ + constructor(arr: Array) { + 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 { + 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(); + } +} + +/** + * 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([ + * { + * 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([ + * { + * 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([ + * { + * 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 extends Model { + readonly sourceModel: Model; + #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, 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); + } +} diff --git a/tests/driver/nodejs/nodejs.rs b/tests/driver/nodejs/nodejs.rs index 3d9fb4ad966..085c996e2eb 100644 --- a/tests/driver/nodejs/nodejs.rs +++ b/tests/driver/nodejs/nodejs.rs @@ -49,7 +49,7 @@ lazy_static::lazy_static! { check_output(o); - node_dir.join("index.js") + node_dir.join("dist/index.js") }; } From d006128929df17a66f98ab9dbcd4dc0040d5559e Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Wed, 18 Sep 2024 06:20:35 +0200 Subject: [PATCH 2/8] Finish node MapModel --- api/node/__test__/api.spec.mts | 2 - .../__test__/js_value_conversion.spec.mts | 7 +-- api/node/typescript/models.ts | 11 +++-- tests/cases/models/map_model.slint | 44 +++++++++++++++++++ 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 tests/cases/models/map_model.slint diff --git a/api/node/__test__/api.spec.mts b/api/node/__test__/api.spec.mts index b94b49cdc2d..cf24e71a042 100644 --- a/api/node/__test__/api.spec.mts +++ b/api/node/__test__/api.spec.mts @@ -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 check: "Test"; }`; diff --git a/api/node/__test__/js_value_conversion.spec.mts b/api/node/__test__/js_value_conversion.spec.mts index 633cca10f0f..7313ce7f386 100644 --- a/api/node/__test__/js_value_conversion.spec.mts +++ b/api/node/__test__/js_value_conversion.spec.mts @@ -599,11 +599,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; - 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"); }); diff --git a/api/node/typescript/models.ts b/api/node/typescript/models.ts index 3357184c253..a73ffc2bf74 100644 --- a/api/node/typescript/models.ts +++ b/api/node/typescript/models.ts @@ -93,8 +93,13 @@ export abstract class Model implements Iterable { */ modelNotify: napi.ExternalObject; - constructor() { - this.modelNotify = napi.jsModelNotifyNew(this); + constructor(modelNotify?: napi.ExternalObject) { + if (modelNotify === undefined) { + this.modelNotify = napi.jsModelNotifyNew(this); + return; + } + + this.modelNotify = modelNotify; } // /** @@ -404,7 +409,7 @@ export class MapModel extends Model { * @param mapFunction maps the data from T to U. */ constructor(sourceModel: Model, mapFunction: (data: T) => U) { - super(); + super(sourceModel.modelNotify); this.sourceModel = sourceModel; this.#mapFunction = mapFunction; } diff --git a/tests/cases/models/map_model.slint b/tests/cases/models/map_model.slint new file mode 100644 index 00000000000..b65a9af77fb --- /dev/null +++ b/tests/cases/models/map_model.slint @@ -0,0 +1,44 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +TestCase := Rectangle { + in-out property <[string]> model; + in-out property changed-items; + + for item in root.model : Text { + text: item; + + + changed text => { + root.changed-items += text; + } + } +} + +/* + +```js +var instance = new slint.TestCase(); + +let nameModel = new slintlib.ArrayModel([ + { first: "Hans", last: "Emil" }, + { first: "Max", last: "Mustermann" }, + { first: "Roman", last: "Tisch" }, +]); + +let mapModel = new slintlib.MapModel(nameModel, (data) => { + return data.last + ", " + data.first; +}); +instance.model = mapModel; + +slintlib.private_api.send_mouse_click(instance, 5., 5.); + +nameModel.setRowData(0, { first: "Simon", last: "Hausmann" }); +nameModel.setRowData(1, { first: "Olivier", last: "Goffart"}); + +slintlib.private_api.send_mouse_click(instance, 5., 5.); +const changedItems = instance.changed_items; +assert.equal(changedItems, "Goffart, OlivierHausmann, Simon"); + +``` +*/ From 744bcff94a4686f2e6954f432e258ddf11721ff6 Mon Sep 17 00:00:00 2001 From: FloVanGH Date: Wed, 18 Sep 2024 06:27:02 +0000 Subject: [PATCH 3/8] Update api/node/typescript/models.ts Co-authored-by: Simon Hausmann --- api/node/typescript/models.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/api/node/typescript/models.ts b/api/node/typescript/models.ts index a73ffc2bf74..f301c2f24aa 100644 --- a/api/node/typescript/models.ts +++ b/api/node/typescript/models.ts @@ -94,12 +94,7 @@ export abstract class Model implements Iterable { modelNotify: napi.ExternalObject; constructor(modelNotify?: napi.ExternalObject) { - if (modelNotify === undefined) { - this.modelNotify = napi.jsModelNotifyNew(this); - return; - } - - this.modelNotify = modelNotify; + this.modelNotify = modelNotify ?? napi.jsModelNotifyNew(this); } // /** From 5069fc31691b39c6d1f6a033194499a035f6859b Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Wed, 18 Sep 2024 09:25:33 +0200 Subject: [PATCH 4/8] Code review feedback --- .../__test__/js_value_conversion.spec.mts | 5 ++ api/node/__test__/models.spec.mts | 57 +++++++++++++++++++ api/node/typescript/models.ts | 3 + tests/cases/models/map_model.slint | 44 -------------- 4 files changed, 65 insertions(+), 44 deletions(-) create mode 100644 api/node/__test__/models.spec.mts delete mode 100644 tests/cases/models/map_model.slint diff --git a/api/node/__test__/js_value_conversion.spec.mts b/api/node/__test__/js_value_conversion.spec.mts index 7313ce7f386..af6738bacab 100644 --- a/api/node/__test__/js_value_conversion.spec.mts +++ b/api/node/__test__/js_value_conversion.spec.mts @@ -606,6 +606,11 @@ test("MapModel", (t) => { t.is(checkModel.rowData(0), "Hausmann, Simon"); t.is(checkModel.rowData(1), "Goffart, Olivier"); t.is(checkModel.rowData(2), "Tisch, Roman"); + + + + // const changedItems = instance!.getProperty("changed-items"); + // t.is(changedItems, "Goffart, OlivierHausmann, Simon"); }); test("MapModel undefined rowData sourcemodel", (t) => { diff --git a/api/node/__test__/models.spec.mts b/api/node/__test__/models.spec.mts new file mode 100644 index 00000000000..3f3b89ee228 --- /dev/null +++ b/api/node/__test__/models.spec.mts @@ -0,0 +1,57 @@ +// Copyright © SixtyFPS GmbH +// 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, MapModel, ArrayModel, private_api } from "../dist/index.js"; + +test("MapModel notify rowChanged", (t) => { + const source = ` + export component App { + + in-out property <[string]> model; + in-out property 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 = new ArrayModel([ + { first: "Hans", last: "Emil" }, + { first: "Max", last: "Mustermann" }, + { first: "Roman", last: "Tisch" }, + ]); + + const mapModel = new 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"); + }); diff --git a/api/node/typescript/models.ts b/api/node/typescript/models.ts index f301c2f24aa..dbd0df7a560 100644 --- a/api/node/typescript/models.ts +++ b/api/node/typescript/models.ts @@ -93,6 +93,9 @@ export abstract class Model implements Iterable { */ modelNotify: napi.ExternalObject; + /** + * @hidden + */ constructor(modelNotify?: napi.ExternalObject) { this.modelNotify = modelNotify ?? napi.jsModelNotifyNew(this); } diff --git a/tests/cases/models/map_model.slint b/tests/cases/models/map_model.slint deleted file mode 100644 index b65a9af77fb..00000000000 --- a/tests/cases/models/map_model.slint +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -TestCase := Rectangle { - in-out property <[string]> model; - in-out property changed-items; - - for item in root.model : Text { - text: item; - - - changed text => { - root.changed-items += text; - } - } -} - -/* - -```js -var instance = new slint.TestCase(); - -let nameModel = new slintlib.ArrayModel([ - { first: "Hans", last: "Emil" }, - { first: "Max", last: "Mustermann" }, - { first: "Roman", last: "Tisch" }, -]); - -let mapModel = new slintlib.MapModel(nameModel, (data) => { - return data.last + ", " + data.first; -}); -instance.model = mapModel; - -slintlib.private_api.send_mouse_click(instance, 5., 5.); - -nameModel.setRowData(0, { first: "Simon", last: "Hausmann" }); -nameModel.setRowData(1, { first: "Olivier", last: "Goffart"}); - -slintlib.private_api.send_mouse_click(instance, 5., 5.); -const changedItems = instance.changed_items; -assert.equal(changedItems, "Goffart, OlivierHausmann, Simon"); - -``` -*/ From 1867f3344b95e13a943e24839084a7c3c2edc9dd Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Wed, 18 Sep 2024 20:37:57 +0200 Subject: [PATCH 5/8] wip tests --- .../__test__/js_value_conversion.spec.mts | 5 --- api/node/__test__/models.spec.mts | 35 ++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/api/node/__test__/js_value_conversion.spec.mts b/api/node/__test__/js_value_conversion.spec.mts index af6738bacab..7313ce7f386 100644 --- a/api/node/__test__/js_value_conversion.spec.mts +++ b/api/node/__test__/js_value_conversion.spec.mts @@ -606,11 +606,6 @@ test("MapModel", (t) => { t.is(checkModel.rowData(0), "Hausmann, Simon"); t.is(checkModel.rowData(1), "Goffart, Olivier"); t.is(checkModel.rowData(2), "Tisch, Roman"); - - - - // const changedItems = instance!.getProperty("changed-items"); - // t.is(changedItems, "Goffart, OlivierHausmann, Simon"); }); test("MapModel undefined rowData sourcemodel", (t) => { diff --git a/api/node/__test__/models.spec.mts b/api/node/__test__/models.spec.mts index 3f3b89ee228..a93e3619378 100644 --- a/api/node/__test__/models.spec.mts +++ b/api/node/__test__/models.spec.mts @@ -5,7 +5,7 @@ import test from "ava"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { loadFile, loadSource, CompileError, MapModel, ArrayModel, private_api } from "../dist/index.js"; +import { loadFile, loadSource, CompileError, MapModel, ArrayModel, private_api, Model } from "../dist/index.js"; test("MapModel notify rowChanged", (t) => { const source = ` @@ -55,3 +55,36 @@ test("MapModel notify rowChanged", (t) => { t.is(instance.changed_items, "Goffart, OlivierHausmann, Simon"); }); + +test("MapModel wraps slint model", (t) => { + const source = ` + export component App { + in-out property <[int]> source: [1, 2, 3]; + out property <[int]> target; + pure callback map([int]) -> [int]; + + map(source) => { + source + } + + }`; + + const path = "api.spec.ts"; + + private_api.initTesting(); + const demo = loadSource(source, path) as any; + const instance = new demo.App({ + map: function(source) { return new MapModel(source, (value) => { return value + 1}); } + }); + + + + + + t.is(instance.target.rowData(0), 2); + + // private_api.send_mouse_click(instance, 5., 5.); + + // private_api.send_mouse_click(instance, 5., 5.); + + }); From 19b8cd1561c7f3a0fab22b54e6e85a56261714c6 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Mon, 30 Sep 2024 08:10:11 +0200 Subject: [PATCH 6/8] Move MapModel back to private_api --- .../__test__/js_value_conversion.spec.mts | 14 +- api/node/__test__/models.spec.mts | 60 ++----- api/node/typescript/index.ts | 166 +++++++++++++++++- api/node/typescript/models.ts | 156 ---------------- 4 files changed, 190 insertions(+), 206 deletions(-) diff --git a/api/node/__test__/js_value_conversion.spec.mts b/api/node/__test__/js_value_conversion.spec.mts index 7313ce7f386..6dea636e708 100644 --- a/api/node/__test__/js_value_conversion.spec.mts +++ b/api/node/__test__/js_value_conversion.spec.mts @@ -11,7 +11,6 @@ import { type ImageData, ArrayModel, type Model, - MapModel, } from "../dist/index.js"; const filename = fileURLToPath(import.meta.url); @@ -593,7 +592,7 @@ test("MapModel", (t) => { { first: "Roman", last: "Tisch" }, ]); - const mapModel = new MapModel(nameModel, (data) => { + const mapModel = new private_api.MapModel(nameModel, (data) => { return data.last + ", " + data.first; }); @@ -612,10 +611,13 @@ test("MapModel undefined rowData sourcemodel", (t) => { const nameModel: ArrayModel = new ArrayModel([1, 2, 3]); let mapFunctionCallCount = 0; - const mapModel = new MapModel(nameModel, (data) => { - mapFunctionCallCount++; - return data.toString(); - }); + const mapModel = new private_api.MapModel( + nameModel, + (data) => { + mapFunctionCallCount++; + return data.toString(); + }, + ); for (let i = 0; i < mapModel.rowCount(); ++i) { mapModel.rowData(i); diff --git a/api/node/__test__/models.spec.mts b/api/node/__test__/models.spec.mts index a93e3619378..f39ee0748e2 100644 --- a/api/node/__test__/models.spec.mts +++ b/api/node/__test__/models.spec.mts @@ -5,7 +5,14 @@ import test from "ava"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { loadFile, loadSource, CompileError, MapModel, ArrayModel, private_api, Model } from "../dist/index.js"; +import { + loadFile, + loadSource, + CompileError, + ArrayModel, + private_api, + Model, +} from "../dist/index.js"; test("MapModel notify rowChanged", (t) => { const source = ` @@ -22,13 +29,13 @@ test("MapModel notify rowChanged", (t) => { } } }`; - + 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; @@ -40,51 +47,18 @@ test("MapModel notify rowChanged", (t) => { { first: "Roman", last: "Tisch" }, ]); - const mapModel = new MapModel(nameModel, (data) => { + 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"); - }); - -test("MapModel wraps slint model", (t) => { - const source = ` - export component App { - in-out property <[int]> source: [1, 2, 3]; - out property <[int]> target; - pure callback map([int]) -> [int]; - - map(source) => { - source - } - }`; - - const path = "api.spec.ts"; - - private_api.initTesting(); - const demo = loadSource(source, path) as any; - const instance = new demo.App({ - map: function(source) { return new MapModel(source, (value) => { return value + 1}); } - }); - + private_api.send_mouse_click(instance, 5, 5); - + nameModel.setRowData(0, { first: "Simon", last: "Hausmann" }); + nameModel.setRowData(1, { first: "Olivier", last: "Goffart" }); - - t.is(instance.target.rowData(0), 2); - - // private_api.send_mouse_click(instance, 5., 5.); - - // private_api.send_mouse_click(instance, 5., 5.); + private_api.send_mouse_click(instance, 5, 5); - }); + t.is(instance.changed_items, "Goffart, OlivierHausmann, Simon"); +}); diff --git a/api/node/typescript/index.ts b/api/node/typescript/index.ts index 3f39191cc73..1760ac39692 100644 --- a/api/node/typescript/index.ts +++ b/api/node/typescript/index.ts @@ -9,7 +9,10 @@ export { Brush, } from "../rust-module.cjs"; -export { Model, ArrayModel, MapModel } from "./models"; +import { Model } from "./models"; +export { Model }; + +export { ArrayModel } from "./models"; import { Diagnostic } from "../rust-module.cjs"; @@ -734,6 +737,167 @@ export function quitEventLoop() { globalEventLoop.quit(); } +/** + * @hidden + */ +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([ + * { + * 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([ + * { + * 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([ + * { + * 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 extends Model { + readonly sourceModel: Model; + #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, 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 */ diff --git a/api/node/typescript/models.ts b/api/node/typescript/models.ts index dbd0df7a560..ebf98590666 100644 --- a/api/node/typescript/models.ts +++ b/api/node/typescript/models.ts @@ -276,159 +276,3 @@ export class ArrayModel extends Model { return this.#array.entries(); } } - -/** - * 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([ - * { - * 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([ - * { - * 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([ - * { - * 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 extends Model { - readonly sourceModel: Model; - #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, 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); - } -} From 1ff5ead61d5429959e1de728a02aba8cdfdc7324 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Mon, 30 Sep 2024 09:34:30 +0200 Subject: [PATCH 7/8] Fix comments for private_api --- api/node/typescript/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/node/typescript/index.ts b/api/node/typescript/index.ts index 1760ac39692..4eb7289c792 100644 --- a/api/node/typescript/index.ts +++ b/api/node/typescript/index.ts @@ -737,9 +737,6 @@ export function quitEventLoop() { globalEventLoop.quit(); } -/** - * @hidden - */ export namespace private_api { /** * Provides rows that are generated by a map function based on the rows of another Model. From 6c00561844a2db5896617b812e7c26a80e056759 Mon Sep 17 00:00:00 2001 From: FloVanGH Date: Mon, 30 Sep 2024 10:21:35 +0200 Subject: [PATCH 8/8] Code review feedback --- .gitignore | 2 -- api/node/.gitignore | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 05f1e17dfc4..dbb47d0ac39 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,6 @@ docs/reference/src/language/builtins/structs.md *.node *.d.ts -api/node/dist/* - .env .envrc __pycache__ diff --git a/api/node/.gitignore b/api/node/.gitignore index 8811ea0c215..58c6fb2465d 100644 --- a/api/node/.gitignore +++ b/api/node/.gitignore @@ -3,3 +3,4 @@ rust-module.d.ts index.js docs/ npm/ +dist/