@driftengine/native-host 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +29 -0
- package/README.md +58 -0
- package/dist/activation.d.ts +26 -0
- package/dist/activation.js +38 -0
- package/dist/alphaCopy.d.ts +36 -0
- package/dist/alphaCopy.js +178 -0
- package/dist/audio.d.ts +55 -0
- package/dist/audio.js +193 -0
- package/dist/audioParams.d.ts +25 -0
- package/dist/audioParams.js +128 -0
- package/dist/bridge.d.ts +70 -0
- package/dist/bridge.js +170 -0
- package/dist/canvas.d.ts +97 -0
- package/dist/canvas.js +197 -0
- package/dist/canvasStyle.d.ts +23 -0
- package/dist/canvasStyle.js +79 -0
- package/dist/device.d.ts +34 -0
- package/dist/device.js +144 -0
- package/dist/dialogs.d.ts +23 -0
- package/dist/dialogs.js +85 -0
- package/dist/domEvents.d.ts +37 -0
- package/dist/domEvents.js +145 -0
- package/dist/domTree.d.ts +34 -0
- package/dist/domTree.js +194 -0
- package/dist/dualRumble.d.ts +19 -0
- package/dist/dualRumble.js +47 -0
- package/dist/eventClasses.d.ts +12 -0
- package/dist/eventClasses.js +35 -0
- package/dist/files.d.ts +16 -0
- package/dist/files.js +59 -0
- package/dist/fullscreen.d.ts +33 -0
- package/dist/fullscreen.js +65 -0
- package/dist/game.d.ts +25 -0
- package/dist/game.js +38 -0
- package/dist/gamepads.d.ts +65 -0
- package/dist/gamepads.js +178 -0
- package/dist/globals.d.ts +24 -0
- package/dist/globals.js +37 -0
- package/dist/gpu.d.ts +26 -0
- package/dist/gpu.js +47 -0
- package/dist/images.d.ts +50 -0
- package/dist/images.js +111 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +21 -0
- package/dist/keyCodes.d.ts +30 -0
- package/dist/keyCodes.js +205 -0
- package/dist/keyEvents.d.ts +54 -0
- package/dist/keyEvents.js +117 -0
- package/dist/mediaQueries.d.ts +62 -0
- package/dist/mediaQueries.js +152 -0
- package/dist/mouseEvents.d.ts +96 -0
- package/dist/mouseEvents.js +287 -0
- package/dist/page.d.ts +118 -0
- package/dist/page.js +302 -0
- package/dist/pointerCapture.d.ts +32 -0
- package/dist/pointerCapture.js +69 -0
- package/dist/present.d.ts +27 -0
- package/dist/present.js +83 -0
- package/dist/rawPad.d.ts +44 -0
- package/dist/rawPad.js +65 -0
- package/dist/readback.d.ts +18 -0
- package/dist/readback.js +46 -0
- package/dist/runtime.d.ts +69 -0
- package/dist/runtime.js +103 -0
- package/dist/standardPad.d.ts +49 -0
- package/dist/standardPad.js +80 -0
- package/dist/textHost.d.ts +37 -0
- package/dist/textHost.js +43 -0
- package/dist/threads.d.ts +27 -0
- package/dist/threads.js +59 -0
- package/dist/tileFiles.d.ts +16 -0
- package/dist/tileFiles.js +36 -0
- package/dist/touchEvents.d.ts +67 -0
- package/dist/touchEvents.js +210 -0
- package/dist/uiEvents.d.ts +157 -0
- package/dist/uiEvents.js +206 -0
- package/dist/uiTouch.d.ts +59 -0
- package/dist/uiTouch.js +75 -0
- package/dist/window.d.ts +71 -0
- package/dist/window.js +164 -0
- package/dist/windowMode.d.ts +19 -0
- package/dist/windowMode.js +15 -0
- package/dist/windowTitle.d.ts +18 -0
- package/dist/windowTitle.js +33 -0
- package/dist/workerScope.mjs +48 -0
- package/licenses/GPL-3.0.txt +674 -0
- package/package.json +83 -0
- package/src/activation.ts +42 -0
- package/src/alphaCopy.ts +201 -0
- package/src/audio.ts +246 -0
- package/src/audioParams.ts +150 -0
- package/src/bridge.ts +230 -0
- package/src/canvas.ts +227 -0
- package/src/canvasStyle.ts +89 -0
- package/src/device.ts +167 -0
- package/src/dialogs.ts +98 -0
- package/src/domEvents.ts +170 -0
- package/src/domTree.ts +215 -0
- package/src/drs.d.ts +9 -0
- package/src/dualRumble.ts +58 -0
- package/src/eventClasses.ts +46 -0
- package/src/files.ts +61 -0
- package/src/fixtures/grey.jpg +0 -0
- package/src/fixtures/rgb.png +0 -0
- package/src/fixtures/tone.opus +0 -0
- package/src/fullscreen.ts +71 -0
- package/src/game.ts +53 -0
- package/src/gamepads.ts +195 -0
- package/src/globals.ts +39 -0
- package/src/gpu.ts +59 -0
- package/src/images.ts +148 -0
- package/src/index.ts +24 -0
- package/src/keyCodes.ts +203 -0
- package/src/keyEvents.ts +135 -0
- package/src/mediaQueries.ts +173 -0
- package/src/mouseEvents.ts +328 -0
- package/src/page.ts +337 -0
- package/src/pointerCapture.ts +73 -0
- package/src/present.ts +92 -0
- package/src/rawPad.ts +85 -0
- package/src/readback.ts +61 -0
- package/src/runtime.ts +162 -0
- package/src/standardPad.ts +126 -0
- package/src/textHost.ts +66 -0
- package/src/threads.ts +66 -0
- package/src/tileFiles.ts +39 -0
- package/src/touchEvents.ts +238 -0
- package/src/uiEvents.ts +268 -0
- package/src/uiTouch.ts +97 -0
- package/src/window.ts +192 -0
- package/src/windowMode.ts +24 -0
- package/src/windowTitle.ts +36 -0
- package/src/workerScope.mjs +48 -0
package/dist/uiTouch.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Touches, as a browser's page reads them: `Touch`, the `TouchList` three of which a `TouchEvent`
|
|
3
|
+
* carries, and the event. The sibling of `uiEvents.ts`, split out only so each stays one screenful.
|
|
4
|
+
*
|
|
5
|
+
* A `TouchList` indexes, iterates and answers `item`, the three ways a page reads one; the engine's
|
|
6
|
+
* input reads `changedTouches[i]`.
|
|
7
|
+
*/
|
|
8
|
+
import { UIEvent } from './uiEvents.js';
|
|
9
|
+
export class Touch {
|
|
10
|
+
identifier;
|
|
11
|
+
target;
|
|
12
|
+
clientX;
|
|
13
|
+
clientY;
|
|
14
|
+
screenX;
|
|
15
|
+
screenY;
|
|
16
|
+
radiusX;
|
|
17
|
+
radiusY;
|
|
18
|
+
rotationAngle = 0;
|
|
19
|
+
force;
|
|
20
|
+
altitudeAngle = Math.PI / 2;
|
|
21
|
+
azimuthAngle = 0;
|
|
22
|
+
touchType = 'direct';
|
|
23
|
+
constructor(init) {
|
|
24
|
+
this.identifier = init.identifier;
|
|
25
|
+
this.target = init.target;
|
|
26
|
+
this.clientX = init.clientX ?? 0;
|
|
27
|
+
this.clientY = init.clientY ?? 0;
|
|
28
|
+
this.screenX = init.screenX ?? 0;
|
|
29
|
+
this.screenY = init.screenY ?? 0;
|
|
30
|
+
this.radiusX = init.radiusX ?? 0;
|
|
31
|
+
this.radiusY = init.radiusY ?? 0;
|
|
32
|
+
this.force = init.force ?? 0;
|
|
33
|
+
}
|
|
34
|
+
get pageX() {
|
|
35
|
+
return this.clientX;
|
|
36
|
+
}
|
|
37
|
+
get pageY() {
|
|
38
|
+
return this.clientY;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class TouchList {
|
|
42
|
+
length;
|
|
43
|
+
constructor(touches = []) {
|
|
44
|
+
this.length = touches.length;
|
|
45
|
+
touches.forEach((touch, at) => {
|
|
46
|
+
this[at] = touch;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
item(index) {
|
|
50
|
+
return this[index] ?? null;
|
|
51
|
+
}
|
|
52
|
+
*[Symbol.iterator]() {
|
|
53
|
+
for (let at = 0; at < this.length; at += 1)
|
|
54
|
+
yield this[at];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class TouchEvent extends UIEvent {
|
|
58
|
+
touches;
|
|
59
|
+
targetTouches;
|
|
60
|
+
changedTouches;
|
|
61
|
+
ctrlKey;
|
|
62
|
+
shiftKey;
|
|
63
|
+
altKey;
|
|
64
|
+
metaKey;
|
|
65
|
+
constructor(type, init = {}) {
|
|
66
|
+
super(type, init);
|
|
67
|
+
this.touches = new TouchList(init.touches);
|
|
68
|
+
this.targetTouches = new TouchList(init.targetTouches);
|
|
69
|
+
this.changedTouches = new TouchList(init.changedTouches);
|
|
70
|
+
this.ctrlKey = init.ctrlKey === true;
|
|
71
|
+
this.shiftKey = init.shiftKey === true;
|
|
72
|
+
this.altKey = init.altKey === true;
|
|
73
|
+
this.metaKey = init.metaKey === true;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/window.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A window, the canvas the engine draws into, and the frame put on the screen.
|
|
3
|
+
*
|
|
4
|
+
* **SDL makes the window and Dawn draws into it**, through `@kmamal/sdl` and `@kmamal/gpu`: the
|
|
5
|
+
* composition chosen for this host on 2026-09-18, because Dawn is the WebGPU implementation inside
|
|
6
|
+
* Chrome and the host's gate is that the engine's frames match the browser's. SDL is also what the
|
|
7
|
+
* later tasks read input, audio and the clipboard from.
|
|
8
|
+
*
|
|
9
|
+
* **The window renderer is made from the engine's device, when the engine configures the canvas.**
|
|
10
|
+
* The engine asks `navigator.gpu` for its own adapter and device, as it does in a browser, so the
|
|
11
|
+
* host does not have one until the engine hands it over — which it does, in `configure`.
|
|
12
|
+
*
|
|
13
|
+
* **Teardown waits for the device.** Destroying the device, the instance or the window with work
|
|
14
|
+
* still in flight trips an assertion inside Dawn and takes the process with it (measured
|
|
15
|
+
* 2026-09-18); after `onSubmittedWorkDone` every order is clean.
|
|
16
|
+
*/
|
|
17
|
+
import type { NativeBridge } from './bridge.ts';
|
|
18
|
+
import { NativeCanvas } from './canvas.ts';
|
|
19
|
+
import type { NativeTextHost } from './textHost.ts';
|
|
20
|
+
import { type WindowMode } from './windowMode.ts';
|
|
21
|
+
import type { HostPage } from './page.ts';
|
|
22
|
+
export interface HostWindowOptions {
|
|
23
|
+
readonly title: string;
|
|
24
|
+
readonly width: number;
|
|
25
|
+
readonly height: number;
|
|
26
|
+
/** A window that is never shown, for checks that should not flash on a desktop. */
|
|
27
|
+
readonly hidden?: boolean;
|
|
28
|
+
/** As a packaged game's manifest says; windowed and resizable when it says nothing. */
|
|
29
|
+
readonly mode?: WindowMode;
|
|
30
|
+
readonly resizable?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class HostWindow {
|
|
33
|
+
readonly canvas: NativeCanvas;
|
|
34
|
+
readonly gpu: GPU;
|
|
35
|
+
private readonly window;
|
|
36
|
+
private readonly instance;
|
|
37
|
+
private device;
|
|
38
|
+
private renderer;
|
|
39
|
+
private presenter;
|
|
40
|
+
private closed;
|
|
41
|
+
constructor(options: HostWindowOptions);
|
|
42
|
+
/**
|
|
43
|
+
* Put this window's canvas in `page`, and its keys, mouse, focus and size into `page`'s events.
|
|
44
|
+
* Hands back what disconnects them.
|
|
45
|
+
*/
|
|
46
|
+
connectPage(page: HostPage): () => void;
|
|
47
|
+
/**
|
|
48
|
+
* The shell's bridge over this window, with its store at `storePath` (see `bridge.ts`). `quit` is
|
|
49
|
+
* how the host hears that the game, or the person closing the window, has ended the session.
|
|
50
|
+
*/
|
|
51
|
+
createBridge(storePath: string, quit: () => void): NativeBridge;
|
|
52
|
+
/** The editor's text services over this window's typing and the desktop's clipboard. */
|
|
53
|
+
createTextHost(): NativeTextHost;
|
|
54
|
+
/** Whether the person closed it. */
|
|
55
|
+
/** A new title, made showable as the first one was (`windowTitle.ts`). */
|
|
56
|
+
setTitle(title: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Send `event` as SDL sends one, through the window's own emitter, for input a capture replays:
|
|
59
|
+
* it then takes the path a person's does, and counts as a gesture where theirs would.
|
|
60
|
+
*/
|
|
61
|
+
replay(type: string, event: object): void;
|
|
62
|
+
get isClosed(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Put the engine's last frame on the screen. Nothing happens before the engine has configured
|
|
65
|
+
* the canvas, or while the window is minimised to nothing.
|
|
66
|
+
*/
|
|
67
|
+
present(): void;
|
|
68
|
+
/** Wait for the device, then let everything go in an order Dawn accepts. */
|
|
69
|
+
close(): Promise<void>;
|
|
70
|
+
private adopt;
|
|
71
|
+
}
|
package/dist/window.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A window, the canvas the engine draws into, and the frame put on the screen.
|
|
3
|
+
*
|
|
4
|
+
* **SDL makes the window and Dawn draws into it**, through `@kmamal/sdl` and `@kmamal/gpu`: the
|
|
5
|
+
* composition chosen for this host on 2026-09-18, because Dawn is the WebGPU implementation inside
|
|
6
|
+
* Chrome and the host's gate is that the engine's frames match the browser's. SDL is also what the
|
|
7
|
+
* later tasks read input, audio and the clipboard from.
|
|
8
|
+
*
|
|
9
|
+
* **The window renderer is made from the engine's device, when the engine configures the canvas.**
|
|
10
|
+
* The engine asks `navigator.gpu` for its own adapter and device, as it does in a browser, so the
|
|
11
|
+
* host does not have one until the engine hands it over — which it does, in `configure`.
|
|
12
|
+
*
|
|
13
|
+
* **Teardown waits for the device.** Destroying the device, the instance or the window with work
|
|
14
|
+
* still in flight trips an assertion inside Dawn and takes the process with it (measured
|
|
15
|
+
* 2026-09-18); after `onSubmittedWorkDone` every order is clean.
|
|
16
|
+
*/
|
|
17
|
+
import gpuModule from '@kmamal/gpu';
|
|
18
|
+
import sdl from '@kmamal/sdl';
|
|
19
|
+
import { nativeBridge } from './bridge.js';
|
|
20
|
+
import { NativeCanvas } from './canvas.js';
|
|
21
|
+
import { desktopFiles } from './dialogs.js';
|
|
22
|
+
import { createNativeTextHost } from './textHost.js';
|
|
23
|
+
import { connectDomEvents } from './domEvents.js';
|
|
24
|
+
import { sdlWindowFlags } from './windowMode.js';
|
|
25
|
+
import { windowTitle } from './windowTitle.js';
|
|
26
|
+
import { nativeGpu } from './gpu.js';
|
|
27
|
+
import { Presenter } from './present.js';
|
|
28
|
+
export class HostWindow {
|
|
29
|
+
canvas;
|
|
30
|
+
gpu;
|
|
31
|
+
window;
|
|
32
|
+
instance;
|
|
33
|
+
device = null;
|
|
34
|
+
renderer = null;
|
|
35
|
+
presenter = null;
|
|
36
|
+
closed = false;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.window = sdl.video.createWindow({
|
|
39
|
+
title: windowTitle(options.title, sdl.info.drivers.video.current),
|
|
40
|
+
width: options.width,
|
|
41
|
+
height: options.height,
|
|
42
|
+
...sdlWindowFlags(options.mode, options.resizable),
|
|
43
|
+
visible: options.hidden !== true,
|
|
44
|
+
webgpu: true,
|
|
45
|
+
});
|
|
46
|
+
this.canvas = new NativeCanvas(this.window.pixelWidth, this.window.pixelHeight);
|
|
47
|
+
this.instance = gpuModule.create([]);
|
|
48
|
+
this.gpu = nativeGpu(this.instance);
|
|
49
|
+
this.window.on('resize', ({ pixelWidth, pixelHeight }) => {
|
|
50
|
+
this.canvas.resizeTo(pixelWidth, pixelHeight);
|
|
51
|
+
this.renderer?.resize();
|
|
52
|
+
});
|
|
53
|
+
this.window.on('close', () => {
|
|
54
|
+
this.closed = true;
|
|
55
|
+
});
|
|
56
|
+
/*
|
|
57
|
+
* **A window nobody focused receives no keys, and SDL says so nowhere.**
|
|
58
|
+
*
|
|
59
|
+
* `@kmamal/sdl` routes a mouse event by which window is *hovered* and a key event by which one
|
|
60
|
+
* is *focused*, and it learns the second only from a `focus` event it may deliver before
|
|
61
|
+
* anything is listening — a window created and given focus by the desktop in the same breath
|
|
62
|
+
* leaves `Globals.windows.focused` null. The window then draws, answers the mouse, and is deaf:
|
|
63
|
+
* a maintainer reported exactly that on 2026-09-20, with clicks landing and the keyboard doing
|
|
64
|
+
* nothing.
|
|
65
|
+
*
|
|
66
|
+
* Asking for focus outright sets it, which is what an application does when it opens a window
|
|
67
|
+
* anyway. **Only a visible one**: a hidden window is for checks that must not take a desktop's
|
|
68
|
+
* focus away from whoever is using it.
|
|
69
|
+
*/
|
|
70
|
+
if (options.hidden !== true)
|
|
71
|
+
this.window.focus();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Put this window's canvas in `page`, and its keys, mouse, focus and size into `page`'s events.
|
|
75
|
+
* Hands back what disconnects them.
|
|
76
|
+
*/
|
|
77
|
+
connectPage(page) {
|
|
78
|
+
page.attach(this.canvas);
|
|
79
|
+
return connectDomEvents(this.window, sdl.mouse, page, this.canvas);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The shell's bridge over this window, with its store at `storePath` (see `bridge.ts`). `quit` is
|
|
83
|
+
* how the host hears that the game, or the person closing the window, has ended the session.
|
|
84
|
+
*/
|
|
85
|
+
createBridge(storePath, quit) {
|
|
86
|
+
return nativeBridge({
|
|
87
|
+
window: this.window,
|
|
88
|
+
displays: () => sdl.video.displays,
|
|
89
|
+
storePath,
|
|
90
|
+
quit,
|
|
91
|
+
files: desktopFiles() ?? undefined,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** The editor's text services over this window's typing and the desktop's clipboard. */
|
|
95
|
+
createTextHost() {
|
|
96
|
+
return createNativeTextHost(this.window, sdl.clipboard);
|
|
97
|
+
}
|
|
98
|
+
/** Whether the person closed it. */
|
|
99
|
+
/** A new title, made showable as the first one was (`windowTitle.ts`). */
|
|
100
|
+
setTitle(title) {
|
|
101
|
+
this.window.setTitle(windowTitle(title, sdl.info.drivers.video.current));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Send `event` as SDL sends one, through the window's own emitter, for input a capture replays:
|
|
105
|
+
* it then takes the path a person's does, and counts as a gesture where theirs would.
|
|
106
|
+
*/
|
|
107
|
+
replay(type, event) {
|
|
108
|
+
this.window.emit(type, event);
|
|
109
|
+
}
|
|
110
|
+
get isClosed() {
|
|
111
|
+
return this.closed;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Put the engine's last frame on the screen. Nothing happens before the engine has configured
|
|
115
|
+
* the canvas, or while the window is minimised to nothing.
|
|
116
|
+
*/
|
|
117
|
+
present() {
|
|
118
|
+
if (this.closed || !this.canvas.visible)
|
|
119
|
+
return;
|
|
120
|
+
const context = this.canvas.getContext('webgpu');
|
|
121
|
+
const configuration = context.getConfiguration();
|
|
122
|
+
if (configuration === null)
|
|
123
|
+
return;
|
|
124
|
+
if (this.device !== configuration.device)
|
|
125
|
+
this.adopt(configuration.device);
|
|
126
|
+
const renderer = this.renderer;
|
|
127
|
+
const presenter = this.presenter;
|
|
128
|
+
if (renderer === null || presenter === null)
|
|
129
|
+
return;
|
|
130
|
+
presenter.present(context.getCurrentTexture(), renderer.getCurrentTextureView());
|
|
131
|
+
renderer.swap();
|
|
132
|
+
}
|
|
133
|
+
/** Wait for the device, then let everything go in an order Dawn accepts. */
|
|
134
|
+
async close() {
|
|
135
|
+
await this.device?.queue.onSubmittedWorkDone();
|
|
136
|
+
this.closed = true;
|
|
137
|
+
/*
|
|
138
|
+
* **A window the person already closed is destroyed, and destroying it again throws.**
|
|
139
|
+
*
|
|
140
|
+
* SDL destroys a window when its close button is pressed, so a host that then shuts down in
|
|
141
|
+
* good order — which is the ordinary path, and the only one after a person closes a window —
|
|
142
|
+
* met `Error: window is destroyed` out of `sdl.video.window.destroy` and ended on a stack
|
|
143
|
+
* trace instead of a clean exit. Reported from a maintainer's own machine, 2026-09-20.
|
|
144
|
+
*
|
|
145
|
+
* **What it gives up:** a destroy that fails for some other reason is swallowed too. The
|
|
146
|
+
* instance below is released either way, which is the part that holds the process open.
|
|
147
|
+
*/
|
|
148
|
+
try {
|
|
149
|
+
this.window.destroy();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
/* Already gone, which is what closing a window does. */
|
|
153
|
+
}
|
|
154
|
+
gpuModule.destroy(this.instance);
|
|
155
|
+
}
|
|
156
|
+
adopt(device) {
|
|
157
|
+
this.device = device;
|
|
158
|
+
this.renderer = gpuModule.renderGPUDeviceToWindow({
|
|
159
|
+
device: device,
|
|
160
|
+
window: this.window,
|
|
161
|
+
});
|
|
162
|
+
this.presenter = new Presenter(device, this.renderer.getCurrentTexture().format);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window a packaged game asked for, in SDL's flags: the manifest's `window.mode` and
|
|
3
|
+
* `window.resizable`, which the desktop shell hands to Electron's `BrowserWindow`.
|
|
4
|
+
*
|
|
5
|
+
* **Fullscreen is the desktop's**: `@kmamal/sdl` opens it as `SDL_WINDOW_FULLSCREEN_DESKTOP`,
|
|
6
|
+
* covering the screen at the screen's own resolution, which is what Electron's `fullscreen: true`
|
|
7
|
+
* is. No mode is switched. **Borderless** is a window with no frame, which is Electron's
|
|
8
|
+
* `frame: false`.
|
|
9
|
+
*
|
|
10
|
+
* What it gives up: nothing chooses the display. SDL opens the window on its first display, as
|
|
11
|
+
* Electron does.
|
|
12
|
+
*/
|
|
13
|
+
export type WindowMode = 'windowed' | 'borderless' | 'fullscreen';
|
|
14
|
+
export interface WindowFlags {
|
|
15
|
+
readonly fullscreen: boolean;
|
|
16
|
+
readonly borderless: boolean;
|
|
17
|
+
readonly resizable: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function sdlWindowFlags(mode?: WindowMode, resizable?: boolean): WindowFlags;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window a packaged game asked for, in SDL's flags: the manifest's `window.mode` and
|
|
3
|
+
* `window.resizable`, which the desktop shell hands to Electron's `BrowserWindow`.
|
|
4
|
+
*
|
|
5
|
+
* **Fullscreen is the desktop's**: `@kmamal/sdl` opens it as `SDL_WINDOW_FULLSCREEN_DESKTOP`,
|
|
6
|
+
* covering the screen at the screen's own resolution, which is what Electron's `fullscreen: true`
|
|
7
|
+
* is. No mode is switched. **Borderless** is a window with no frame, which is Electron's
|
|
8
|
+
* `frame: false`.
|
|
9
|
+
*
|
|
10
|
+
* What it gives up: nothing chooses the display. SDL opens the window on its first display, as
|
|
11
|
+
* Electron does.
|
|
12
|
+
*/
|
|
13
|
+
export function sdlWindowFlags(mode = 'windowed', resizable = true) {
|
|
14
|
+
return { fullscreen: mode === 'fullscreen', borderless: mode === 'borderless', resizable };
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A window's title as this SDL can show it.
|
|
3
|
+
*
|
|
4
|
+
* **Under X11 the title SDL writes is in the process's locale, and Node leaves that at C.** SDL
|
|
5
|
+
* 2.32's X11 driver sets `WM_NAME` through `XmbTextListToTextProperty`, so a UTF-8 title's bytes
|
|
6
|
+
* go in as Latin-1, and the `_NET_WM_NAME` it should add in UTF-8 — the one a window manager prefers —
|
|
7
|
+
* is not on the window at all. Read back with `xprop` on 2026-09-19: `WM_NAME(STRING) = "Showroom
|
|
8
|
+
* â\302\200\302\224 DriftEngine"` and no `_NET_WM_NAME`, which the desktop drew as `Showroom —
|
|
9
|
+
* DriftEngine`; reported by the maintainer. So under X11 a title is made ASCII before SDL sees it:
|
|
10
|
+
* accents dropped, dashes and quotes in their ASCII forms, and anything else one `?` rather than
|
|
11
|
+
* three characters of noise.
|
|
12
|
+
*
|
|
13
|
+
* What would make it wrong: an SDL that sets `_NET_WM_NAME`, which `xprop -id <window>
|
|
14
|
+
* _NET_WM_NAME` shows, and this then drops letters for nothing. Wayland's title is UTF-8 and is
|
|
15
|
+
* left alone.
|
|
16
|
+
*/
|
|
17
|
+
/** `title` as `driver` can show it: unchanged, or in ASCII under X11. */
|
|
18
|
+
export declare function windowTitle(title: string, driver: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A window's title as this SDL can show it.
|
|
3
|
+
*
|
|
4
|
+
* **Under X11 the title SDL writes is in the process's locale, and Node leaves that at C.** SDL
|
|
5
|
+
* 2.32's X11 driver sets `WM_NAME` through `XmbTextListToTextProperty`, so a UTF-8 title's bytes
|
|
6
|
+
* go in as Latin-1, and the `_NET_WM_NAME` it should add in UTF-8 — the one a window manager prefers —
|
|
7
|
+
* is not on the window at all. Read back with `xprop` on 2026-09-19: `WM_NAME(STRING) = "Showroom
|
|
8
|
+
* â\302\200\302\224 DriftEngine"` and no `_NET_WM_NAME`, which the desktop drew as `Showroom —
|
|
9
|
+
* DriftEngine`; reported by the maintainer. So under X11 a title is made ASCII before SDL sees it:
|
|
10
|
+
* accents dropped, dashes and quotes in their ASCII forms, and anything else one `?` rather than
|
|
11
|
+
* three characters of noise.
|
|
12
|
+
*
|
|
13
|
+
* What would make it wrong: an SDL that sets `_NET_WM_NAME`, which `xprop -id <window>
|
|
14
|
+
* _NET_WM_NAME` shows, and this then drops letters for nothing. Wayland's title is UTF-8 and is
|
|
15
|
+
* left alone.
|
|
16
|
+
*/
|
|
17
|
+
const PLAIN = new Map([
|
|
18
|
+
['–', '-'],
|
|
19
|
+
['—', '-'],
|
|
20
|
+
['‘', "'"],
|
|
21
|
+
['’', "'"],
|
|
22
|
+
['“', '"'],
|
|
23
|
+
['”', '"'],
|
|
24
|
+
['…', '...'],
|
|
25
|
+
]);
|
|
26
|
+
/** `title` as `driver` can show it: unchanged, or in ASCII under X11. */
|
|
27
|
+
export function windowTitle(title, driver) {
|
|
28
|
+
if (driver !== 'x11')
|
|
29
|
+
return title;
|
|
30
|
+
return [...title.normalize('NFKD').replace(/\p{M}/gu, '')]
|
|
31
|
+
.map((character) => character.charCodeAt(0) < 0x80 ? character : (PLAIN.get(character) ?? '?'))
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The start of every thread the native host makes: a browser worker's scope over `worker_threads`,
|
|
3
|
+
* and then the module the page asked for.
|
|
4
|
+
*
|
|
5
|
+
* **A worker module written for a page reads `self`, `onmessage`, `addEventListener` and a global
|
|
6
|
+
* `postMessage`**, none of which a Node thread has; they are made here over `parentPort`, and a
|
|
7
|
+
* message is handed on with its value as `data`, as a `MessageEvent` carries it.
|
|
8
|
+
*
|
|
9
|
+
* **Messages wait until the module has run**, as a browser holds them until a worker's script has
|
|
10
|
+
* been evaluated: a page usually posts its first message the moment it constructs the worker, and a
|
|
11
|
+
* module that sets `onmessage` after an `await` would otherwise never see it.
|
|
12
|
+
*
|
|
13
|
+
* Plain JavaScript, so that it loads before any loader a host registers has anything to do.
|
|
14
|
+
*/
|
|
15
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
16
|
+
|
|
17
|
+
const scope = globalThis;
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
let ready = false;
|
|
20
|
+
const held = [];
|
|
21
|
+
|
|
22
|
+
function deliver(data) {
|
|
23
|
+
const event = { type: 'message', data, target: scope, currentTarget: scope };
|
|
24
|
+
if (typeof scope.onmessage === 'function') scope.onmessage(event);
|
|
25
|
+
for (const listener of listeners) {
|
|
26
|
+
if (typeof listener === 'function') listener(event);
|
|
27
|
+
else listener.handleEvent(event);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
scope.self = scope;
|
|
32
|
+
scope.onmessage = null;
|
|
33
|
+
scope.postMessage = (value, transfer) => parentPort.postMessage(value, transfer);
|
|
34
|
+
scope.addEventListener = (type, listener) => {
|
|
35
|
+
if (type === 'message') listeners.add(listener);
|
|
36
|
+
};
|
|
37
|
+
scope.removeEventListener = (type, listener) => {
|
|
38
|
+
if (type === 'message') listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
scope.close = () => process.exit(0);
|
|
41
|
+
parentPort.on('message', (data) => {
|
|
42
|
+
if (ready) deliver(data);
|
|
43
|
+
else held.push(data);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await import(workerData.url);
|
|
47
|
+
ready = true;
|
|
48
|
+
for (const data of held.splice(0)) deliver(data);
|