UNPKG

slint-ui

Version:

Slint is a declarative GUI toolkit to build native user interfaces for desktop and embedded applications.

666 lines (665 loc) 27 kB
"use strict"; // 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 Object.defineProperty(exports, "__esModule", { value: true }); exports.private_api = exports.CompileError = exports.ArrayModel = exports.Model = void 0; exports.loadFile = loadFile; exports.loadSource = loadSource; exports.runEventLoop = runEventLoop; exports.quitEventLoop = quitEventLoop; exports.initTranslations = initTranslations; exports.setXdgAppId = setXdgAppId; const napi = require("../rust-module.cjs"); const models_1 = require("./models"); Object.defineProperty(exports, "Model", { enumerable: true, get: function () { return models_1.Model; } }); var models_2 = require("./models"); Object.defineProperty(exports, "ArrayModel", { enumerable: true, get: function () { return models_2.ArrayModel; } }); const node_url_1 = require("node:url"); /** * @hidden */ class Component { #instance; /** * @hidden */ constructor(instance) { this.#instance = instance; } get window() { return this.#instance.window(); } /** * @hidden */ get component_instance() { 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. */ class CompileError extends Error { /** * List of {@link Diagnostic} items emitted while compiling .slint code. */ diagnostics; /** * 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, diagnostics) { 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; } } exports.CompileError = CompileError; function translateName(key) { return key.replace(/-/g, "_"); } function loadSlint(loadData) { 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; } if (typeof options.fileLoader !== "undefined") { compiler.fileLoader = options.fileLoader; } } 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 === 1 /* napi.DiagnosticLevel.Warning */); if (typeof options !== "undefined" && options.quiet !== true) { warnings.forEach((w) => console.warn("Warning: " + w)); } const errors = diagnostics.filter((d) => d.level === 0 /* napi.DiagnosticLevel.Error */); if (errors.length > 0) { throw new CompileError("Could not compile " + filePath, errors); } } const slint_module = Object.create({}); // generate structs const structs = compiler.structs; for (const key in compiler.structs) { Object.defineProperty(slint_module, translateName(key), { value: function (properties) { const defaultObject = structs[key]; const newObject = Object.create({}); for (const propertyKey in defaultObject) { const propertyName = translateName(propertyKey); const propertyValue = properties !== undefined && Object.hasOwn(properties, propertyName) ? properties[propertyName] : defaultObject[propertyKey]; Object.defineProperty(newObject, propertyName, { value: propertyValue, writable: true, enumerable: true, }); } return Object.seal(newObject); }, }); } // generate enums const enums = compiler.enums; for (const key in enums) { Object.defineProperty(slint_module, translateName(key), { value: Object.seal(enums[key]), enumerable: true, }); } Object.keys(definitions).forEach((key) => { const definition = definitions[key]; Object.defineProperty(slint_module, translateName(definition.name), { value: function (properties) { 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 = translateName(prop.name); 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 = translateName(cb); if (componentHandle[callbackName] !== undefined) { console.warn("Duplicated callback name " + callbackName); } else { Object.defineProperty(componentHandle, translateName(cb), { 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 = translateName(cb); if (componentHandle[functionName] !== undefined) { console.warn("Duplicated function name " + functionName); } else { Object.defineProperty(componentHandle, translateName(cb), { get() { return function () { return instance.invoke(cb, Array.from(arguments)); }; }, enumerable: true, }); } }); // globals instance.definition().globals.forEach((globalName) => { const jsName = translateName(globalName); if (componentHandle[jsName] !== undefined) { console.warn("Duplicated property name " + globalName + " (In JS: " + jsName + ")"); } else { const globalObject = Object.create({}); instance .definition() .globalProperties(globalName) .forEach((prop) => { const propName = translateName(prop.name); 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 = translateName(cb); if (globalObject[callbackName] !== undefined) { console.warn("Duplicated property name " + cb + " on global " + global); } else { Object.defineProperty(globalObject, translateName(cb), { 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 = translateName(cb); if (globalObject[functionName] !== undefined) { console.warn("Duplicated function name " + cb + " on global " + global); } else { Object.defineProperty(globalObject, translateName(cb), { get() { return function () { return instance.invokeGlobal(globalName, cb, Array.from(arguments)); }; }, enumerable: true, }); } }); Object.defineProperty(componentHandle, jsName, { 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 as `string` or `URL`. 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#instantiating-a-component). * @throws {@link CompileError} if errors occur during compilation. */ function loadFile(filePath, options) { const pathname = filePath instanceof URL ? (0, node_url_1.fileURLToPath)(filePath) : filePath; return loadSlint({ fileData: { filePath: pathname, 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#instantiating-a-component). * @throws {@link CompileError} if errors occur during compilation. */ function loadSource(source, filePath, options) { return loadSlint({ fileData: { filePath, options, source }, from: "source", }); } class EventLoop { #quit_loop = false; #terminationPromise = null; #terminateResolveFn; start(running_callback, quitOnLastWindowClosed = true) { 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() === 1 /* 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 = 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. */ function runEventLoop(args) { 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. */ function quitEventLoop() { globalEventLoop.quit(); } var private_api; (function (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)); * ``` */ class MapModel extends models_1.Model { sourceModel; #mapFunction; /** * 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, mapFunction) { super(sourceModel.modelNotify); this.sourceModel = sourceModel; this.#mapFunction = mapFunction; } /** * Returns the number of entries in the model. */ rowCount() { 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) { const data = this.sourceModel.rowData(row); if (data === undefined) { return undefined; } return this.#mapFunction(data); } } private_api.MapModel = MapModel; })(private_api || (exports.private_api = private_api = {})); /** * Initialize translations. * * Call this with the path where translations are located. This function internally calls the [bindtextdomain](https://man7.org/linux/man-pages/man3/bindtextdomain.3.html) function from gettext. * * Translations are expected to be found at <path>/<locale>/LC_MESSAGES/<domain>.mo, where path is the directory passed as an argument to this function, locale is a locale name (e.g., en, en_GB, fr), and domain is the package name. * * @param domain defines the domain name e.g. name of the package. * @param path specifies the directory as `string` or as `URL` in which gettext should search for translations. * * For example, assuming this is in a package called example and the default locale is configured to be French, it will load translations at runtime from ``/path/to/example/translations/fr/LC_MESSAGES/example.mo`. * * ```js * import * as slint from "slint-ui"; * slint.initTranslations("example", new URL("translations/", import.meta.url)); * ```` */ function initTranslations(domain, path) { const pathname = path instanceof URL ? (0, node_url_1.fileURLToPath)(path) : path; napi.initTranslations(domain, pathname); } /** * Sets the application id for use on Wayland or X11 with [xdg](https://specifications.freedesktop.org/desktop-entry-spec/latest/) * compliant window managers. This must be set before the window is shown. */ function setXdgAppId(app_id) { napi.setXdgAppId(app_id); } /** * @hidden */ (function (private_api) { private_api.mock_elapsed_time = napi.mockElapsedTime; private_api.get_mocked_time = napi.getMockedTime; private_api.ComponentCompiler = napi.ComponentCompiler; private_api.ComponentDefinition = napi.ComponentDefinition; private_api.ComponentInstance = napi.ComponentInstance; private_api.Window = napi.Window; private_api.SlintBrush = napi.SlintBrush; private_api.SlintRgbaColor = napi.SlintRgbaColor; private_api.SlintSize = napi.SlintSize; private_api.SlintPoint = napi.SlintPoint; private_api.SlintImageData = napi.SlintImageData; function send_mouse_click(component, x, y) { component.component_instance.sendMouseClick(x, y); } private_api.send_mouse_click = send_mouse_click; function send_keyboard_string_sequence(component, s) { component.component_instance.sendKeyboardStringSequence(s); } private_api.send_keyboard_string_sequence = send_keyboard_string_sequence; private_api.initTesting = napi.initTesting; })(private_api || (exports.private_api = private_api = {}));