const VT_CONTAINER = 1
const VT_LABEL = 2
const VT_BUTTON = 3
const VT_ENTRY = 4
const VT_GROUP = 5
const VT_PROGRESS_BAR = 6
const VT_SCROLL = 7
const VT_TEXT_EDIT = 8
const VT_IMAGE = 9;
const CONTEXT2ELEMENT = new WeakMap();
class Clipboard {
async readText() {
return clipboard_read_text();
}
async writeText(text) {
clipboard_write_text(text);
}
}
class Navigator {
clipboard;
constructor() {
this.clipboard = new Clipboard();
}
}
class Process {
exit(code) {
process_exit(code);
}
get argv() {
return process_argv();
}
get isMobilePlatform() {
return process_is_mobile_platform();
}
}
class FileDialog {
show(options) {
return new Promise((resolve, reject) => {
dialog_show_file_dialog({
dialogType: options.dialogType,
}, options.frame?.handle, (result, data) => {
if (result) {
resolve(data);
} else {
reject(data);
}
})
})
}
}
export class Frame {
#eventRegistry;
#eventBinder;
#frameId;
constructor(attrs) {
this.#frameId = Frame_create(attrs || {});
this.#eventBinder = new EventBinder(this.#frameId, Frame_bind_js_event_listener, Frame_unbind_js_event_listener, this);
}
get handle() {
return this.#frameId
}
setBody(view) {
Frame_set_body(this.#frameId, view.el);
}
setTitle(title) {
Frame_set_title(this.#frameId, title);
}
resize(size) {
Frame_resize(this.#frameId, size);
}
setModal(owner) {
Frame_set_modal(this.#frameId, owner.#frameId)
}
close() {
Frame_close(this.#frameId);
}
setVisible(visible) {
Frame_set_visible(this.#frameId, visible);
}
requestFullscreen() {
Frame_request_fullscreen(this.#frameId);
}
exitFullscreen() {
Frame_exit_fullscreen(this.#frameId);
}
bindResize(callback) {
this.#eventBinder.bindEvent("resize", callback);
}
bindClose(callback) {
this.bindEvent("close", callback);
}
bindFocus(callback) {
this.bindEvent("focus", callback);
}
bindBlur(callback) {
this.bindEvent("blur", callback);
}
bindEvent(type, callback) {
this.#eventBinder.bindEvent(type, callback);
}
addEventListener(type, callback) {
this.#eventBinder.addEventListener(type, callback);
}
removeEventListener(type, callback) {
this.#eventBinder.removeEventListener(type, callback);
}
}
export class EventObject {
_propagationCancelled = false
_preventDefault = false
type;
detail;
target;
currentTarget;
constructor(type, detail, target, currentTarget) {
this.type = type;
this.detail = detail;
this.target = target;
this.currentTarget = currentTarget;
}
stopPropagation() {
this._propagationCancelled = true;
}
preventDefault() {
this._preventDefault = true;
}
result() {
return {
propagationCancelled: this._propagationCancelled,
preventDefault: this._preventDefault,
}
}
}
export class EventRegistry {
eventListeners = Object.create(null);
_id;
_remove_api;
_add_api;
#contextGetter;
#self;
constructor(id, addApi, removeApi, self, contextGetter) {
this._id = id;
this._add_api = addApi;
this._remove_api = removeApi;
this.#contextGetter = contextGetter;
this.#self = self;
}
bindEvent(type, callback) {
type = type.toLowerCase();
if (typeof callback !== "function") {
throw new Error("invalid callback");
}
let oldListenerId = this.eventListeners[type];
if (oldListenerId) {
this._remove_api(this._id, type, oldListenerId);
}
const getJsContext = (target) => {
if (target && this.#contextGetter) {
return this.#contextGetter(target);
}
return target;
}
const self = this.#self;
function eventCallback(type, detail, target) {
const event = new EventObject(type, detail, getJsContext(target), self);
try {
callback && callback(event);
} catch (error) {
console.error(`${type} event handling error, detail=`, detail ,error.message || error);
}
return event.result();
}
this.eventListeners[type] = this._add_api(this._id, type, eventCallback);
}
}
export class EventBinder {
#eventListeners = Object.create(null);
#target;
#removeEventListenerApi;
#addEventListenerApi;
#contextGetter;
#self;
#allEventListeners = Object.create(null);
constructor(target, addApi, removeApi, self, contextGetter) {
this.#target = target;
this.#addEventListenerApi = addApi;
this.#removeEventListenerApi = removeApi;
this.#contextGetter = contextGetter;
this.#self = self;
}
bindEvent(type, callback) {
type = type.toLowerCase();
if (typeof callback !== "function") {
throw new Error("invalid callback");
}
let oldListenerId = this.#eventListeners[type];
if (oldListenerId) {
this.#removeEventListenerApi(this.#target, oldListenerId);
}
this.#eventListeners[type] = this.addEventListener(type, callback);
}
addEventListener(type, callback) {
const getJsContext = (target) => {
if (target && this.#contextGetter) {
return this.#contextGetter(target);
}
return target;
}
const self = this.#self;
function eventCallback(detail, target) {
const event = new EventObject(type, detail, getJsContext(target), self);
try {
callback && callback(event);
} catch (error) {
console.error(`${type} event handling error, detail=`, detail ,error.message || error);
}
return event.result();
}
if (!this.#allEventListeners[type]) {
this.#allEventListeners[type] = new Map();
}
const id = this.#addEventListenerApi(this.#target, type, eventCallback);
this.#allEventListeners[type].set(callback, id);
return id;
}
removeEventListener(type, callback) {
const map = this.#eventListeners[type];
const id = map.delete(callback);
if (id) {
this.#removeEventListenerApi(this.#target, type, id);
}
}
}
export class SystemTray {
#eventRegistry;
#menuUserCallback;
tray;
constructor() {
this.tray = SystemTray_create("Test");
this.#eventRegistry = new EventRegistry(this.tray, SystemTray_bind_event, SystemTray_remove_event_listener, this);
}
setTitle(title) {
SystemTray_set_title(this.tray, title);
}
setIcon(icon) {
SystemTray_set_icon(this.tray, icon);
}
setMenus(menus) {
const list = [];
const menuHandlers = new Map();
for (const m of menus) {
const {id, label, checked, enabled} = m;
const kind = m.kind || "standard";
if (m.handler) {
menuHandlers.set(m.id, m.handler);
}
list.push({id, label, kind, checked, enabled});
}
const menuHandler = (e) => {
const id = e.detail;
const handler = menuHandlers.get(id);
if (handler) {
handler();
}
if (this.#menuUserCallback) {
this.#menuUserCallback(e);
}
}
SystemTray_set_menus(this.tray, list);
this.#eventRegistry.bindEvent("menuclick", menuHandler)
}
bindActivate(callback) {
this.#eventRegistry.bindEvent("activate", callback);
}
bindMenuClick(callback) {
this.#menuUserCallback = callback;
}
}
export class View {
parent
el
#eventBinder;
constructor(el, context) {
const myContext = context || {};
CONTEXT2ELEMENT.set(myContext, this);
if (typeof el === "number") {
this.el = Element_create_by_type(el, myContext);
} else {
Element_set_js_context(el, myContext);
this.el = el;
}
if (!this.el) {
throw new Error("Failed to create view:" + el)
}
this.#eventBinder = new EventBinder(this.el, Element_add_js_event_listener, Element_remove_js_event_listener, this, (target) => {
const myContext = Element_get_js_context(target);
if (myContext) {
return CONTEXT2ELEMENT.get(myContext);
}
});
}
createEventBinder(target, addEventListenerApi, removeEventListenerApi) {
if (!removeEventListenerApi) {
removeEventListenerApi = (_t, listenerId) => {
Element_remove_js_event_listener(this.el, listenerId);
}
}
return new EventBinder(target, addEventListenerApi, removeEventListenerApi, this, (target) => {
const myContext = Element_get_js_context(target);
if (myContext) {
return CONTEXT2ELEMENT.get(myContext);
}
});
}
getId() {
return Element_get_id(this.el)
}
focus() {
Element_focus(this.el);
}
setStyle(style) {
Element_set_style(this.el, style);
}
setAnimation(animation) {
Element_set_animation(this.el, animation);
}
setHoverStyle(style) {
Element_set_hover_style(this.el, style);
}
setScrollTop(value) {
Element_set_scroll_top(this.el, value);
}
setScrollLeft(value) {
Element_set_scroll_left(this.el, value);
}
setDraggable(value) {
Element_set_draggable(this.el, value);
}
setCursor(value) {
Element_set_cursor(this.el, value);
}
getSize() {
return Element_get_size(this.el);
}
getContentSize() {
return Element_get_real_content_size(this.el);
}
getBoundingClientRect() {
return Element_get_bounding_client_rect(this.el);
}
getScrollTop() {
return Element_get_scroll_top(this.el);
}
getScrollLeft() {
return Element_get_scroll_left(this.el);
}
getScrollHeight() {
return Element_get_scroll_height(this.el);
}
getScrollWidth() {
return Element_scroll_width(this.el);
}
bindBoundsChange(callback) {
this.bindEvent("boundschange", callback);
}
bindFocus(callback) {
this.bindEvent("focus", callback);
}
bindBlur(callback) {
this.bindEvent("blur", callback);
}
bindClick(callback) {
this.#eventBinder.bindEvent("click", callback);
}
bindContextMenu(callback) {
this.#eventBinder.bindEvent("contextmenu", callback);
}
bindMouseDown(callback) {
this.#eventBinder.bindEvent("mousedown", callback);
}
bindMouseUp(callback) {
this.#eventBinder.bindEvent("mouseup", callback);
}
bindMouseMove(callback) {
this.#eventBinder.bindEvent("mousemove", callback);
}
bindMouseEnter(callback) {
this.#eventBinder.bindEvent("mouseenter", callback);
}
bindMouseLeave(callback) {
this.#eventBinder.bindEvent("mouseleave", callback);
}
bindKeyDown(callback) {
this.#eventBinder.bindEvent("keydown", callback);
}
bindKeyUp(callback) {
this.#eventBinder.bindEvent("keyup", callback);
}
bindSizeChanged(callback) {
this.#eventBinder.bindEvent("sizechange", callback);
}
bindScroll(callback) {
this.#eventBinder.bindEvent("scroll", callback);
}
bindMouseWheel(callback) {
this.#eventBinder.bindEvent("mousewheel", callback);
}
bindDragStart(callback) {
this.#eventBinder.bindEvent("dragstart", callback);
}
bindDragOver(callback) {
this.#eventBinder.bindEvent("dragover", callback);
}
bindDrop(callback) {
this.#eventBinder.bindEvent("drop", callback);
}
bindTouchStart(callback) {
this.#eventBinder.bindEvent("touchstart", callback);
}
bindTouchMove(callback) {
this.#eventBinder.bindEvent("touchmove", callback);
}
bindTouchEnd(callback) {
this.#eventBinder.bindEvent("touchend", callback);
}
bindTouchCancel(callback) {
this.#eventBinder.bindEvent("touchcancel", callback);
}
bindEvent(type, callback) {
this.#eventBinder.bindEvent(type, callback);
}
toString() {
return this.el + "@" + this.constructor.name
}
}
export class Audio {
context;
#eventRegistry;
id;
constructor(config) {
this.id = Audio_create(config || {})
this.#eventRegistry = new EventRegistry(this.id, Audio_add_event_listener, Audio_remove_event_listener, this);
}
play() {
Audio_play(this.id);
}
pause() {
Audio_pause(this.id);
}
stop() {
Audio_stop(this.id);
}
bindLoad(callback) {
this.#eventRegistry.bindEvent('load', callback);
}
bindTimeUpdate(callback) {
this.#eventRegistry.bindEvent("timeupdate", callback);
}
bindEnd(callback) {
this.#eventRegistry.bindEvent("end", callback);
}
bindPause(callback) {
this.#eventRegistry.bindEvent("pause", callback);
}
bindStop(callback) {
this.#eventRegistry.bindEvent("stop", callback);
}
bindCurrentChange(callback) {
this.#eventRegistry.bindEvent("currentchange", callback);
}
bindEvent(type, callback) {
this.#eventRegistry.bindEvent(type, callback);
}
}
export class LabelElement extends View {
constructor() {
super(VT_LABEL);
}
setTextWrap(wrap) {
Text_set_text_wrap(this.el, wrap);
}
setText(text) {
Text_set_text(this.el, text);
}
setAlign(align) {
Element_set_property(this.el, "align", align);
}
setSelection(selection) {
Text_set_selection(this.el, selection);
}
selectByCaretOffset(startCaretOffset, endCaretOffset) {
this.setSelection([startCaretOffset, endCaretOffset])
}
getLineBeginOffset(line) {
return Text_get_line_begin_offset(this.el, line);
}
insertLine(line, text) {
Text_insert_line(this.el, line, text);
}
updateLine(line, newText) {
Text_update_line(this.el, line, newText);
}
deleteLine(line) {
Text_delete_line(this.el, line);
}
getCaretOffsetByCursor(row, col) {
return Text_get_atom_offset_by_location(this.el, [row, col]);
}
}
export class ParagraphElement extends View {
#paragraph;
constructor() {
const p = Paragraph_new_element();
super(p, {});
this.#paragraph = p;
}
addLine(units) {
Paragraph_add_line(this.#paragraph, units);
}
insertLine(index, units) {
Paragraph_insert_line(this.#paragraph, index, units);
}
deleteLine(index) {
Paragraph_delete_line(this.#paragraph, index);
}
updateLine(index, units) {
Paragraph_update_line(this.#paragraph, index, units);
}
clear() {
Paragraph_clear(this.#paragraph);
}
measureLine(units) {
return Paragraph_measure_line(this.#paragraph, units);
}
getSelectionText() {
return Paragraph_get_selection_text(this.#paragraph);
}
}
export class ImageElement extends View {
constructor() {
super(VT_IMAGE);
}
setSrc(src) {
Image_set_src(this.el, src);
}
}
export class EntryElement extends View {
constructor() {
super(VT_ENTRY);
}
setAlign(align) {
Element_set_property(this.el, "align", align);
}
setText(text) {
Entry_set_text(this.el, text);
}
setSelectionByCharOffset(start, end) {
Entry_set_selection_by_char_offset(this.el, start, end)
}
setCaretByCharOffset(charOffset) {
Entry_set_caret_by_char_offset(this.el, charOffset);
}
setMultipleLine(multipleLine) {
Entry_set_multiple_line(this.el, multipleLine)
}
getText() {
return Entry_get_text(this.el);
}
setRows(rows) {
Entry_set_rows(this.el, rows);
}
bindTextChange(callback) {
this.bindEvent("textchange", callback);
}
}
export class TextEditElement extends View {
constructor() {
super(VT_TEXT_EDIT);
}
setAlign(align) {
Element_set_property(this.el, "align", align);
}
setText(text) {
Element_set_property(this.el, "text", text);
}
getText() {
return Element_get_property(this.el, "text");
}
setSelection(selection) {
Element_set_property(this.el, "selection", selection);
}
setCaret(caret) {
Element_set_property(this.el, "caret", caret);
}
scrollToTop(top) {
Element_set_property(this.el, "scroll_to_top", top);
}
bindTextChange(callback) {
this.bindEvent("textchange", callback);
}
bindCaretChange(callback) {
this.bindEvent("caretchange", callback);
}
}
class ContainerBasedElement extends View {
#children = [];
addChild(child, index= -1) {
if (child.parent === this) {
const oldIndex = this.#children.indexOf(child);
if (oldIndex === index) {
return;
}
index -= oldIndex < index ? 1 : 0;
this.removeChild(child);
this.addChild(child, index);
return;
}
if (child.parent) {
child.parent.removeChild(child);
}
child.parent = this;
if (typeof index === "number" && index >= 0 && index < this.#children.length) {
Element_add_child(this.el, child.el, index);
this.#children.splice(index, 0, child);
} else {
Element_add_child(this.el, child.el, -1);
this.#children.push(child);
}
}
addChildBefore(newNode, referenceNode) {
const index = this.#children.indexOf(referenceNode);
this.addChild(newNode, index);
}
addChildAfter(newNode, referenceNode) {
const index = this.#children.indexOf(referenceNode);
if (index >= 0) {
this.addChild(newNode, index + 1);
} else {
this.addChild(newNode);
}
}
removeChild(child) {
const index = this.#children.indexOf(child);
if (index >= 0) {
child.parent = null;
Element_remove_child(this.el, index);
this.#children.splice(index, 1);
} else {
console.log("remove child failed")
}
}
}
export class ButtonElement extends ContainerBasedElement {
constructor() {
super(VT_BUTTON);
}
}
export class ContainerElement extends ContainerBasedElement {
constructor() {
super(VT_CONTAINER);
}
}
export class ScrollElement extends ContainerBasedElement {
constructor() {
super(VT_SCROLL);
}
setScrollX(value) {
Scroll_set_scroll_x(this.el, value);
}
setScrollY(value) {
Scroll_set_scroll_y(this.el, value);
}
scrollBy(value) {
value.x = value.x || 0;
value.y = value.y || 0;
Element_scroll_by(this.el, value);
}
}
export class WebSocket {
client;
listeners;
onopen;
onclose;
onmessage;
onping;
onpong;
onerror;
#closed = false;
constructor(url) {
this.listeners = Object.create(null);
this.#connect(url);
}
addEventListener(name, callback) {
if (!this.listeners[name]) {
this.listeners[name] = [];
}
const listeners = this.listeners[name]
listeners.push(callback);
}
async send(data) {
try {
await WsConnection_send_str(this.client, data + "");
} catch (error) {
this.#emit('error', error);
}
}
close() {
if (!this.#closed) {
this.#closed = true;
this.#emit("close");
WsConnection_close(this.client);
}
}
async #connect(url) {
try {
this.client = await WsConnection_connect(url);
this.#emit("open");
this.#doRead();
} catch (error) {
this.#emit("error", error);
}
}
async #doRead() {
try {
loop:
for (;;) {
let [type, data] = await WsConnection_read(this.client);
switch (type) {
case "text":
this.#emit("message", data);
break;
case "binary":
this.#emit("message", ArrayBuffer.from(data));
break;
case "ping":
this.#emit("ping", data);
break;
case "pong":
this.#emit("pong", data);
break;
case "close":
break loop;
case "frame":
this.#emit("frame", data);
break;
}
}
this.close();
} catch (error) {
console.error(error);
this.#emit("error");
this.close();
}
}
#emit(name, data) {
let event = {
bubbles: false,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
eventPhase: 0,
isTrusted: true,
returnValue: false,
srcElement: null,
target: null,
timeStamp: new Date().getTime(),
type: name,
data,
};
const key = `on${name}`;
if (this[key]) {
try {
this[key](event)
} catch (error) {
console.error(error);
}
}
for (const listener of this.listeners[name] || []) {
try {
listener(event);
} catch (error) {
console.error(error);
}
}
}
}
export class Worker {
#worker
#eventBinder;
constructor(source) {
this.#worker = typeof source === "string" ? Worker_create(source) : Worker_bind(source);
this.#eventBinder = new EventBinder(
this.#worker,
Worker_bind_js_event_listener,
Worker_remove_js_event_listener,
this
);
}
postMessage(data) {
Worker_post_message(this.#worker, JSON.stringify(data));
}
bindMessage(callback) {
this.#eventBinder.bindEvent('message', e => {
e.data = JSON.parse(e.detail.data);
callback(e);
});
}
}
export class WorkerContext {
#workerContext;
#eventBinder;
constructor() {
this.#workerContext = WorkerContext_get();
this.#eventBinder = new EventBinder(
this.#workerContext,
WorkerContext_bind_js_event_listener,
WorkerContext_remove_js_event_listener,
this
)
}
postMessage(data) {
WorkerContext_post_message(this.#workerContext, JSON.stringify(data));
}
bindMessage(callback) {
this.#eventBinder.bindEvent('message', e => {
e.data = JSON.parse(e.detail.data);
callback(e);
});
}
static create() {
if (globalThis.WorkerContext_get) {
return new WorkerContext();
}
return null;
}
}
export class SqliteConn {
#conn;
constructor(conn) {
this.#conn = conn;
}
async execute(sql, params= []) {
return await SqliteConn_execute(this.#conn, sql, params);
}
async query(sql, params = []) {
const [columnNames, rows] = await SqliteConn_query(this.#conn, sql, params);
return rows.map(it => {
const map = {};
for (let i = 0; i < columnNames.length; i++) {
map[columnNames[i]] = it[i];
}
return map;
});
}
}
export class Sqlite {
static async open(path) {
const conn = await SqliteConn_open(path);
return new SqliteConn(conn);
}
}
function collectCircleRefInfo(value, visited, circleRefList, level) {
if (level >= 3) {
return;
}
if (value && typeof value === "object") {
if (visited.includes(value)) {
circleRefList.push(value);
return;
} else {
visited.push(value);
}
Object.entries(value).forEach(([k, v]) => {
collectCircleRefInfo(v, visited, circleRefList, level + 1);
})
}
}
function log(...values) {
values.forEach((value, index) => {
const visited = [];
const circleRefList = [];
collectCircleRefInfo(value, visited, circleRefList, 0);
printObj(value, "", circleRefList, [], 0);
if (index < values.length - 1) {
printObj(",")
}
})
Console_print("\n");
}
function printObj(value, padding, circleRefList, printedList, level) {
let type = typeof value;
if (value instanceof Error) {
console.log(`[Error(${value.name})]` + value.message);
if (value.stack) {
console.log(value.stack);
}
} else if (type === "object" && value != null) {
const refIdx = circleRefList.indexOf(value);
if (refIdx >= 0 && printedList.includes(value)) {
Console_print("[Circular *" + refIdx + "]");
} else {
const entries = Object.entries(value);
if (level >= 2) {
return "{...}"
}
if (!entries.length) {
Console_print("{}");
} else {
const prefix = refIdx >= 0 ? ("<ref *" + refIdx + ">") : "";
Console_print(prefix + "{\n");
printedList.push(value);
entries.forEach(([k, v], index) => {
Console_print(padding + " " + k + ":");
printObj(v, padding + " ", circleRefList, printedList, level + 1);
if (index < entries.length - 1) {
Console_print(",\n");
}
});
Console_print("\n" + padding + "}");
}
}
} else if (type === "symbol") {
console.log("[Symbol]")
} else if (type === "function") {
console.log("[Function]")
} else {
Console_print(value + "");
}
}
globalThis.console = {
trace: log,
debug: log,
log,
info: log,
warn: log,
error: log,
}
const localStorage = {
getItem(key) {
return localstorage_get(key)
},
setItem(key, value) {
localstorage_set(key, value);
}
}
export const workerContext = WorkerContext.create();
if (workerContext) {
globalThis.workerContext = workerContext;
}
globalThis.navigator = new Navigator();
globalThis.process = new Process();
globalThis.fileDialog = new FileDialog();
globalThis.Worker = Worker;
globalThis.WorkerContext = WorkerContext;
globalThis.Frame = Frame;
if (globalThis.SystemTray_create) {
globalThis.SystemTray = SystemTray;
}
globalThis.View = View;
globalThis.ContainerElement = ContainerElement;
globalThis.ScrollElement = ScrollElement;
globalThis.LabelElement = LabelElement;
globalThis.EntryElement = EntryElement;
globalThis.TextEditElement = TextEditElement;
globalThis.ButtonElement = ButtonElement;
globalThis.ImageElement = ImageElement;
globalThis.ParagraphElement = ParagraphElement;
globalThis.Audio = Audio;
globalThis.WebSocket = WebSocket;
globalThis.Sqlite = Sqlite;
globalThis.setTimeout = globalThis.timer_set_timeout;
globalThis.clearTimeout = globalThis.timer_clear_timeout;
globalThis.setInterval = globalThis.timer_set_interval;
globalThis.clearInterval = globalThis.timer_clear_interval;
globalThis.KEY_MOD_CTRL = 0x1;
globalThis.KEY_MOD_ALT = 0x1 << 1;
globalThis.KEY_MOD_META = 0x1 << 2;
globalThis.KEY_MOD_SHIFT = 0x1 << 3;
globalThis.localStorage = localStorage;