// 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"; export { Diagnostic, DiagnosticLevel, RgbaColor, Brush, } from "./rust-module"; 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; } 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 * 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; /** * 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; /** * Sets library paths used for looking up `@library` imports to the specified map of library names to paths. */ libraryPaths?: Record; } 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 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 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 | null = null; #terminateResolveFn: ((_value: unknown) => void) | null; start( running_callback?: Function, quitOnLastWindowClosed: boolean = true, ): Promise { 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 { 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(); } /** * @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; }