mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-28 21:04:47 +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
932
api/node/typescript/index.ts
Normal file
932
api/node/typescript/index.ts
Normal file
|
@ -0,0 +1,932 @@
|
|||
// 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";
|
||||
export {
|
||||
Diagnostic,
|
||||
DiagnosticLevel,
|
||||
RgbaColor,
|
||||
Brush,
|
||||
} 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.
|
||||
*/
|
||||
export interface Point {
|
||||
/**
|
||||
* Defines the x coordinate of the point.
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* Defines the y coordinate of the point.
|
||||
*/
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a two-dimensional size.
|
||||
*/
|
||||
export interface Size {
|
||||
/**
|
||||
* Defines the width length of the size.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* Defines the height length of the size.
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This type represents a window towards the windowing system, that's used to render the
|
||||
* scene of a component. It provides API to control windowing system specific aspects such
|
||||
* as the position on the screen.
|
||||
*/
|
||||
export interface Window {
|
||||
/** Gets or sets the logical position of the window on the screen. */
|
||||
logicalPosition: Point;
|
||||
|
||||
/** Gets or sets the physical position of the window on the screen. */
|
||||
physicalPosition: Point;
|
||||
|
||||
/** Gets or sets the logical size of the window on the screen, */
|
||||
logicalSize: Size;
|
||||
|
||||
/** Gets or sets the physical size of the window on the screen, */
|
||||
physicalSize: Size;
|
||||
|
||||
/** Gets or sets the window's fullscreen state **/
|
||||
fullscreen: boolean;
|
||||
|
||||
/** Gets or sets the window's maximized state **/
|
||||
maximized: boolean;
|
||||
|
||||
/** Gets or sets teh window's minimized state **/
|
||||
minimized: boolean;
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the window. This function can return false even if you previously called show()
|
||||
* on it, for example if the user minimized the window.
|
||||
*/
|
||||
get visible(): boolean;
|
||||
|
||||
/**
|
||||
* Shows the window on the screen. An additional strong reference on the
|
||||
* associated component is maintained while the window is visible.
|
||||
*/
|
||||
show(): void;
|
||||
|
||||
/** Hides the window, so that it is not visible anymore. */
|
||||
hide(): void;
|
||||
|
||||
/** Issues a request to the windowing system to re-render the contents of the window. */
|
||||
requestRedraw(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An image data type that can be displayed by the Image element.
|
||||
*
|
||||
* This interface is inspired by the web [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) interface.
|
||||
*/
|
||||
export interface ImageData {
|
||||
/**
|
||||
* Returns the path of the image, if it was loaded from disk. Otherwise
|
||||
* the property is undefined.
|
||||
*/
|
||||
readonly path?: string;
|
||||
|
||||
/**
|
||||
* Returns the image as buffer.
|
||||
*/
|
||||
get data(): Uint8Array;
|
||||
|
||||
/**
|
||||
* Returns the width of the image in pixels.
|
||||
*/
|
||||
get width(): number;
|
||||
|
||||
/**
|
||||
* Returns the height of the image in pixels.
|
||||
*/
|
||||
get height(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface describes the public API of a Slint component that is common to all instances. Use this to
|
||||
* show() the window on the screen, access the window and subsequent window properties, or start the
|
||||
* Slint event loop with run().
|
||||
*/
|
||||
export interface ComponentHandle {
|
||||
/**
|
||||
* Shows the window and runs the event loop. The returned promise is resolved when the event loop
|
||||
* is terminated, for example when the last window was closed, or {@link quitEventLoop} was called.
|
||||
*
|
||||
* This function is a convenience for calling {@link show}, followed by {@link runEventLoop}, and
|
||||
* {@link hide} when the event loop's promise is resolved.
|
||||
*/
|
||||
run(): Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Shows the component's window on the screen.
|
||||
*/
|
||||
show();
|
||||
|
||||
/**
|
||||
* Hides the component's window, so that it is not visible anymore.
|
||||
*/
|
||||
hide();
|
||||
|
||||
/**
|
||||
* Returns the {@link Window} associated with this component instance.
|
||||
* The window API can be used to control different aspects of the integration into the windowing system, such as the position on the screen.
|
||||
*/
|
||||
get window(): Window;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
class Component implements ComponentHandle {
|
||||
#instance: napi.ComponentInstance;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
constructor(instance: napi.ComponentInstance) {
|
||||
this.#instance = instance;
|
||||
}
|
||||
|
||||
get window(): Window {
|
||||
return this.#instance.window();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
get component_instance(): napi.ComponentInstance {
|
||||
return this.#instance;
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.show();
|
||||
await runEventLoop();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.#instance.window().show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.#instance.window().hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an errors that can be emitted by the compiler.
|
||||
*/
|
||||
export class CompileError extends Error {
|
||||
/**
|
||||
* List of {@link Diagnostic} items emitted while compiling .slint code.
|
||||
*/
|
||||
diagnostics: napi.Diagnostic[];
|
||||
|
||||
/**
|
||||
* Creates a new CompileError.
|
||||
*
|
||||
* @param message human-readable description of the error.
|
||||
* @param diagnostics represent a list of diagnostic items emitted while compiling .slint code.
|
||||
*/
|
||||
constructor(message: string, diagnostics: napi.Diagnostic[]) {
|
||||
const formattedDiagnostics = diagnostics
|
||||
.map(
|
||||
(d) =>
|
||||
`[${d.fileName}:${d.lineNumber}:${d.columnNumber}] ${d.message}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
let formattedMessage = message;
|
||||
if (diagnostics.length > 0) {
|
||||
formattedMessage += `\nDiagnostics:\n${formattedDiagnostics}`;
|
||||
}
|
||||
|
||||
super(formattedMessage);
|
||||
this.diagnostics = diagnostics;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoadFileOptions are used to defines different optional parameters that can be used to configure the compiler.
|
||||
*/
|
||||
export interface LoadFileOptions {
|
||||
/**
|
||||
* If set to true warnings from the compiler will not be printed to the console.
|
||||
*/
|
||||
quiet?: boolean;
|
||||
|
||||
/**
|
||||
* Sets the widget style the compiler is currently using when compiling .slint files.
|
||||
*/
|
||||
style?: string;
|
||||
|
||||
/**
|
||||
* Sets the include paths used for looking up `.slint` imports to the specified vector of paths.
|
||||
*/
|
||||
includePaths?: Array<string>;
|
||||
|
||||
/**
|
||||
* Sets library paths used for looking up `@library` imports to the specified map of library names to paths.
|
||||
*/
|
||||
libraryPaths?: Record<string, string>;
|
||||
}
|
||||
|
||||
type LoadData =
|
||||
| {
|
||||
fileData: {
|
||||
filePath: string;
|
||||
options?: LoadFileOptions;
|
||||
};
|
||||
from: "file";
|
||||
}
|
||||
| {
|
||||
fileData: {
|
||||
source: string;
|
||||
filePath: string;
|
||||
options?: LoadFileOptions;
|
||||
};
|
||||
from: "source";
|
||||
};
|
||||
|
||||
function loadSlint(loadData: LoadData): Object {
|
||||
const { filePath, options } = loadData.fileData;
|
||||
|
||||
const compiler = new napi.ComponentCompiler();
|
||||
|
||||
if (typeof options !== "undefined") {
|
||||
if (typeof options.style !== "undefined") {
|
||||
compiler.style = options.style;
|
||||
}
|
||||
if (typeof options.includePaths !== "undefined") {
|
||||
compiler.includePaths = options.includePaths;
|
||||
}
|
||||
if (typeof options.libraryPaths !== "undefined") {
|
||||
compiler.libraryPaths = options.libraryPaths;
|
||||
}
|
||||
}
|
||||
|
||||
const definitions =
|
||||
loadData.from === "file"
|
||||
? compiler.buildFromPath(filePath)
|
||||
: compiler.buildFromSource(loadData.fileData.source, filePath);
|
||||
const diagnostics = compiler.diagnostics;
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
const warnings = diagnostics.filter(
|
||||
(d) => d.level === napi.DiagnosticLevel.Warning,
|
||||
);
|
||||
|
||||
if (typeof options !== "undefined" && options.quiet !== true) {
|
||||
warnings.forEach((w) => console.warn("Warning: " + w));
|
||||
}
|
||||
|
||||
const errors = diagnostics.filter(
|
||||
(d) => d.level === napi.DiagnosticLevel.Error,
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new CompileError("Could not compile " + filePath, errors);
|
||||
}
|
||||
}
|
||||
|
||||
const slint_module = Object.create({});
|
||||
|
||||
Object.keys(definitions).forEach((key) => {
|
||||
const definition = definitions[key];
|
||||
|
||||
Object.defineProperty(
|
||||
slint_module,
|
||||
definition.name.replace(/-/g, "_"),
|
||||
{
|
||||
value: function (properties: any) {
|
||||
const instance = definition.create();
|
||||
|
||||
if (instance == null) {
|
||||
throw Error(
|
||||
"Could not create a component handle for" +
|
||||
filePath,
|
||||
);
|
||||
}
|
||||
|
||||
for (var key in properties) {
|
||||
const value = properties[key];
|
||||
|
||||
if (value instanceof Function) {
|
||||
instance.setCallback(key, value);
|
||||
} else {
|
||||
instance.setProperty(key, properties[key]);
|
||||
}
|
||||
}
|
||||
|
||||
const componentHandle = new Component(instance!);
|
||||
instance!.definition().properties.forEach((prop) => {
|
||||
const propName = prop.name.replace(/-/g, "_");
|
||||
|
||||
if (componentHandle[propName] !== undefined) {
|
||||
console.warn(
|
||||
"Duplicated property name " + propName,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty(componentHandle, propName, {
|
||||
get() {
|
||||
return instance!.getProperty(prop.name);
|
||||
},
|
||||
set(value) {
|
||||
instance!.setProperty(prop.name, value);
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
instance!.definition().callbacks.forEach((cb) => {
|
||||
const callbackName = cb.replace(/-/g, "_");
|
||||
|
||||
if (componentHandle[callbackName] !== undefined) {
|
||||
console.warn(
|
||||
"Duplicated callback name " + callbackName,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty(
|
||||
componentHandle,
|
||||
cb.replace(/-/g, "_"),
|
||||
{
|
||||
get() {
|
||||
return function () {
|
||||
return instance!.invoke(
|
||||
cb,
|
||||
Array.from(arguments),
|
||||
);
|
||||
};
|
||||
},
|
||||
set(callback) {
|
||||
instance!.setCallback(cb, callback);
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
instance!.definition().functions.forEach((cb) => {
|
||||
const functionName = cb.replace(/-/g, "_");
|
||||
|
||||
if (componentHandle[functionName] !== undefined) {
|
||||
console.warn(
|
||||
"Duplicated function name " + functionName,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty(
|
||||
componentHandle,
|
||||
cb.replace(/-/g, "_"),
|
||||
{
|
||||
get() {
|
||||
return function () {
|
||||
return instance!.invoke(
|
||||
cb,
|
||||
Array.from(arguments),
|
||||
);
|
||||
};
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// globals
|
||||
instance!.definition().globals.forEach((globalName) => {
|
||||
if (componentHandle[globalName] !== undefined) {
|
||||
console.warn(
|
||||
"Duplicated property name " + globalName,
|
||||
);
|
||||
} else {
|
||||
const globalObject = Object.create({});
|
||||
|
||||
instance!
|
||||
.definition()
|
||||
.globalProperties(globalName)
|
||||
.forEach((prop) => {
|
||||
const propName = prop.name.replace(
|
||||
/-/g,
|
||||
"_",
|
||||
);
|
||||
|
||||
if (globalObject[propName] !== undefined) {
|
||||
console.warn(
|
||||
"Duplicated property name " +
|
||||
propName +
|
||||
" on global " +
|
||||
global,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty(
|
||||
globalObject,
|
||||
propName,
|
||||
{
|
||||
get() {
|
||||
return instance!.getGlobalProperty(
|
||||
globalName,
|
||||
prop.name,
|
||||
);
|
||||
},
|
||||
set(value) {
|
||||
instance!.setGlobalProperty(
|
||||
globalName,
|
||||
prop.name,
|
||||
value,
|
||||
);
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
instance!
|
||||
.definition()
|
||||
.globalCallbacks(globalName)
|
||||
.forEach((cb) => {
|
||||
const callbackName = cb.replace(/-/g, "_");
|
||||
|
||||
if (
|
||||
globalObject[callbackName] !== undefined
|
||||
) {
|
||||
console.warn(
|
||||
"Duplicated property name " +
|
||||
cb +
|
||||
" on global " +
|
||||
global,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty(
|
||||
globalObject,
|
||||
cb.replace(/-/g, "_"),
|
||||
{
|
||||
get() {
|
||||
return function () {
|
||||
return instance!.invokeGlobal(
|
||||
globalName,
|
||||
cb,
|
||||
Array.from(
|
||||
arguments,
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
set(callback) {
|
||||
instance!.setGlobalCallback(
|
||||
globalName,
|
||||
cb,
|
||||
callback,
|
||||
);
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
instance!
|
||||
.definition()
|
||||
.globalFunctions(globalName)
|
||||
.forEach((cb) => {
|
||||
const functionName = cb.replace(/-/g, "_");
|
||||
|
||||
if (
|
||||
globalObject[functionName] !== undefined
|
||||
) {
|
||||
console.warn(
|
||||
"Duplicated function name " +
|
||||
cb +
|
||||
" on global " +
|
||||
global,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty(
|
||||
globalObject,
|
||||
cb.replace(/-/g, "_"),
|
||||
{
|
||||
get() {
|
||||
return function () {
|
||||
return instance!.invokeGlobal(
|
||||
globalName,
|
||||
cb,
|
||||
Array.from(
|
||||
arguments,
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(componentHandle, globalName, {
|
||||
get() {
|
||||
return globalObject;
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Object.seal(componentHandle);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
return Object.seal(slint_module);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified Slint file and returns an object containing functions to construct the exported
|
||||
* components defined within the Slint file.
|
||||
*
|
||||
* The following example loads a "Hello World" style Slint file and changes the Text label to a new greeting:
|
||||
* **`main.slint`**:
|
||||
* ```
|
||||
* export component Main inherits Window {
|
||||
* in-out property <string> greeting <=> label.text;
|
||||
* label := Text {
|
||||
* text: "Hello World";
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **`index.js`**:
|
||||
* ```javascript
|
||||
* import * as slint from "slint-ui";
|
||||
* let ui = slint.loadFile("main.slint");
|
||||
* let main = new ui.Main();
|
||||
* main.greeting = "Hello friends";
|
||||
* ```
|
||||
*
|
||||
* @param filePath The path to the file to load. Relative paths are resolved against the process' current working directory.
|
||||
* @param options An optional {@link LoadFileOptions} to configure additional Slint compilation settings,
|
||||
* such as include search paths, library imports, or the widget style.
|
||||
* @returns Returns an object that is immutable and provides a constructor function for each exported Window component found in the `.slint` file.
|
||||
* For instance, in the example above, a `Main` property is available, which can be used to create instances of the `Main` component using the `new` keyword.
|
||||
* These instances offer properties and event handlers, adhering to the {@link ComponentHandle} interface.
|
||||
* For further information on the available properties, refer to [Instantiating A Component](../index.html#md:instantiating-a-component).
|
||||
* @throws {@link CompileError} if errors occur during compilation.
|
||||
*/
|
||||
export function loadFile(filePath: string, options?: LoadFileOptions): Object {
|
||||
return loadSlint({
|
||||
fileData: { filePath, options },
|
||||
from: "file",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given Slint source code and returns an object that contains a functions to construct the exported
|
||||
* components of the Slint source code.
|
||||
*
|
||||
* The following example loads a "Hello World" style Slint source code and changes the Text label to a new greeting:
|
||||
* ```js
|
||||
* import * as slint from "slint-ui";
|
||||
* const source = `export component Main {
|
||||
* in-out property <string> greeting <=> label.text;
|
||||
* label := Text {
|
||||
* text: "Hello World";
|
||||
* }
|
||||
* }`; // The content of main.slint
|
||||
* let ui = slint.loadSource(source, "main.js");
|
||||
* let main = new ui.Main();
|
||||
* main.greeting = "Hello friends";
|
||||
* ```
|
||||
* @param source The Slint source code to load.
|
||||
* @param filePath A path to the file to show log and resolve relative import and images.
|
||||
* Relative paths are resolved against the process' current working directory.
|
||||
* @param options An optional {@link LoadFileOptions} to configure additional Slint compilation settings,
|
||||
* such as include search paths, library imports, or the widget style.
|
||||
* @returns Returns an object that is immutable and provides a constructor function for each exported Window component found in the `.slint` file.
|
||||
* For instance, in the example above, a `Main` property is available, which can be used to create instances of the `Main` component using the `new` keyword.
|
||||
* These instances offer properties and event handlers, adhering to the {@link ComponentHandle} interface.
|
||||
* For further information on the available properties, refer to [Instantiating A Component](../index.html#md:instantiating-a-component).
|
||||
* @throws {@link CompileError} if errors occur during compilation.
|
||||
*/
|
||||
export function loadSource(
|
||||
source: string,
|
||||
filePath: string,
|
||||
options?: LoadFileOptions,
|
||||
): Object {
|
||||
return loadSlint({
|
||||
fileData: { filePath, options, source },
|
||||
from: "source",
|
||||
});
|
||||
}
|
||||
|
||||
class EventLoop {
|
||||
#quit_loop: boolean = false;
|
||||
#terminationPromise: Promise<unknown> | null = null;
|
||||
#terminateResolveFn: ((_value: unknown) => void) | null;
|
||||
|
||||
start(
|
||||
running_callback?: Function,
|
||||
quitOnLastWindowClosed: boolean = true,
|
||||
): Promise<unknown> {
|
||||
if (this.#terminationPromise != null) {
|
||||
return this.#terminationPromise;
|
||||
}
|
||||
|
||||
this.#terminationPromise = new Promise((resolve) => {
|
||||
this.#terminateResolveFn = resolve;
|
||||
});
|
||||
this.#quit_loop = false;
|
||||
|
||||
napi.setQuitOnLastWindowClosed(quitOnLastWindowClosed);
|
||||
|
||||
if (running_callback !== undefined) {
|
||||
napi.invokeFromEventLoop(() => {
|
||||
running_callback();
|
||||
running_callback = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Give the nodejs event loop 16 ms to tick. This polling is sub-optimal, but it's the best we
|
||||
// can do right now.
|
||||
const nodejsPollInterval = 16;
|
||||
const id = setInterval(() => {
|
||||
if (
|
||||
napi.processEvents() === napi.ProcessEventsResult.Exited ||
|
||||
this.#quit_loop
|
||||
) {
|
||||
clearInterval(id);
|
||||
this.#terminateResolveFn!(undefined);
|
||||
this.#terminateResolveFn = null;
|
||||
this.#terminationPromise = null;
|
||||
return;
|
||||
}
|
||||
}, nodejsPollInterval);
|
||||
|
||||
return this.#terminationPromise;
|
||||
}
|
||||
|
||||
quit() {
|
||||
this.#quit_loop = true;
|
||||
}
|
||||
}
|
||||
|
||||
var globalEventLoop: EventLoop = new EventLoop();
|
||||
|
||||
/**
|
||||
* Spins the Slint event loop and returns a promise that resolves when the loop terminates.
|
||||
*
|
||||
* If the event loop is already running, then this function returns the same promise as from
|
||||
* the earlier invocation.
|
||||
*
|
||||
* @param args As Function it defines a callback that's invoked once when the event loop is running.
|
||||
* @param args.runningCallback Optional callback that's invoked once when the event loop is running.
|
||||
* The function's return value is ignored.
|
||||
* @param args.quitOnLastWindowClosed if set to `true` event loop is quit after last window is closed otherwise
|
||||
* it is closed after {@link quitEventLoop} is called.
|
||||
* This is useful for system tray applications where the application needs to stay alive even if no windows are visible.
|
||||
* (default true).
|
||||
*
|
||||
* Note that the event loop integration with Node.js is slightly imperfect. Due to conflicting
|
||||
* implementation details between Slint's and Node.js' event loop, the two loops are merged
|
||||
* by spinning one after the other, at 16 millisecond intervals. This means that when the
|
||||
* application is idle, it continues to consume a low amount of CPU cycles, checking if either
|
||||
* event loop has any pending events.
|
||||
*/
|
||||
export function runEventLoop(
|
||||
args?:
|
||||
| Function
|
||||
| { runningCallback?: Function; quitOnLastWindowClosed?: boolean },
|
||||
): Promise<unknown> {
|
||||
if (args === undefined) {
|
||||
return globalEventLoop.start(undefined);
|
||||
}
|
||||
|
||||
if (args instanceof Function) {
|
||||
return globalEventLoop.start(args);
|
||||
}
|
||||
|
||||
return globalEventLoop.start(
|
||||
args.runningCallback,
|
||||
args.quitOnLastWindowClosed,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a spinning event loop. This function returns immediately, and the promise returned
|
||||
from run_event_loop() will resolve in a later tick of the nodejs event loop.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export namespace private_api {
|
||||
export import mock_elapsed_time = napi.mockElapsedTime;
|
||||
export import get_mocked_time = napi.getMockedTime;
|
||||
export import ComponentCompiler = napi.ComponentCompiler;
|
||||
export import ComponentDefinition = napi.ComponentDefinition;
|
||||
export import ComponentInstance = napi.ComponentInstance;
|
||||
export import ValueType = napi.ValueType;
|
||||
export import Window = napi.Window;
|
||||
|
||||
export import SlintBrush = napi.SlintBrush;
|
||||
export import SlintRgbaColor = napi.SlintRgbaColor;
|
||||
export import SlintSize = napi.SlintSize;
|
||||
export import SlintPoint = napi.SlintPoint;
|
||||
export import SlintImageData = napi.SlintImageData;
|
||||
|
||||
export function send_mouse_click(
|
||||
component: Component,
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
component.component_instance.sendMouseClick(x, y);
|
||||
}
|
||||
|
||||
export function send_keyboard_string_sequence(
|
||||
component: Component,
|
||||
s: string,
|
||||
) {
|
||||
component.component_instance.sendKeyboardStringSequence(s);
|
||||
}
|
||||
|
||||
export import initTesting = napi.initTesting;
|
||||
}
|
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();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue