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
JavaScript
;
// 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 = {}));