@atom-js-org/runtime 0.5.3-alpha.0 → 0.5.3-alpha.2
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/package.json +4 -3
- package/src/app.cjs +2 -0
- package/src/bridge-server.cjs +54 -1
- package/src/browser-window.cjs +6 -2
- package/src/runtime/window-host.mjs +1 -1
- package/src/windows-native-host.cjs +426 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atom-js-org/runtime",
|
|
3
|
-
"version": "0.5.3-alpha.
|
|
3
|
+
"version": "0.5.3-alpha.2",
|
|
4
4
|
"description": "Electron-like desktop runtime powered by the operating system WebView.",
|
|
5
5
|
"main": "src/index.cjs",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
22
|
+
"node": ">=24"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"ws": "^8.21.1"
|
|
25
|
+
"ws": "^8.21.1",
|
|
26
|
+
"@webviewjs/webview": "0.4.0"
|
|
26
27
|
},
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
package/src/app.cjs
CHANGED
|
@@ -7,6 +7,7 @@ const fs = require('node:fs');
|
|
|
7
7
|
const state = require('./state.cjs');
|
|
8
8
|
const { BridgeServer } = require('./bridge-server.cjs');
|
|
9
9
|
const { stopNativeHost } = require('./native-host.cjs');
|
|
10
|
+
const { stopWindowsNativeHost } = require('./windows-native-host.cjs');
|
|
10
11
|
|
|
11
12
|
class App extends EventEmitter {
|
|
12
13
|
constructor() {
|
|
@@ -43,6 +44,7 @@ class App extends EventEmitter {
|
|
|
43
44
|
|
|
44
45
|
for (const win of [...state.windows.values()]) win.destroy();
|
|
45
46
|
await stopNativeHost();
|
|
47
|
+
await stopWindowsNativeHost();
|
|
46
48
|
if (state.bridgeServer) await state.bridgeServer.stop();
|
|
47
49
|
this.emit('will-quit');
|
|
48
50
|
this.emit('quit', {}, 0);
|
package/src/bridge-server.cjs
CHANGED
|
@@ -220,6 +220,7 @@ class BridgeServer {
|
|
|
220
220
|
|
|
221
221
|
const match = url.pathname.match(/^\/__atom\/window\/(\d+)\/(.*)$/);
|
|
222
222
|
if (!match) {
|
|
223
|
+
if (await redirectRootRelativeApplicationAsset(request, response, url)) return;
|
|
223
224
|
response.statusCode = 404;
|
|
224
225
|
response.end('Not found');
|
|
225
226
|
return;
|
|
@@ -271,6 +272,58 @@ class BridgeServer {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
async function redirectRootRelativeApplicationAsset(request, response, url) {
|
|
276
|
+
if (!['GET', 'HEAD'].includes(String(request.method || 'GET').toUpperCase())) return false;
|
|
277
|
+
if (url.pathname.startsWith('/__atom/')) return false;
|
|
278
|
+
|
|
279
|
+
const referer = request.headers && request.headers.referer;
|
|
280
|
+
if (!referer) return false;
|
|
281
|
+
|
|
282
|
+
let refererUrl;
|
|
283
|
+
try {
|
|
284
|
+
refererUrl = new URL(referer, 'http://127.0.0.1');
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const refererMatch = refererUrl.pathname.match(/^\/__atom\/window\/(\d+)\//);
|
|
290
|
+
if (!refererMatch) return false;
|
|
291
|
+
|
|
292
|
+
const windowId = Number(refererMatch[1]);
|
|
293
|
+
const win = state.windows.get(windowId);
|
|
294
|
+
if (!win || !win._contentRoot) return false;
|
|
295
|
+
|
|
296
|
+
let requested;
|
|
297
|
+
try {
|
|
298
|
+
requested = decodeURIComponent(url.pathname.replace(/^\/+/, ''));
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (!requested) return false;
|
|
303
|
+
|
|
304
|
+
const root = path.resolve(win._contentRoot);
|
|
305
|
+
const absolute = path.resolve(root, requested);
|
|
306
|
+
if (absolute !== root && !absolute.startsWith(root + path.sep)) return false;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const stat = await fs.promises.stat(absolute);
|
|
310
|
+
if (!stat.isFile() && !stat.isDirectory()) return false;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const encodedPath = requested
|
|
316
|
+
.split(/[\\/]+/)
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.map((part) => encodeURIComponent(part))
|
|
319
|
+
.join('/');
|
|
320
|
+
response.statusCode = 307;
|
|
321
|
+
response.setHeader('location', `/__atom/window/${windowId}/${encodedPath}${url.search}`);
|
|
322
|
+
response.setHeader('cache-control', 'no-store');
|
|
323
|
+
response.end();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
274
327
|
function createIpcEvent(win) {
|
|
275
328
|
return Object.freeze({
|
|
276
329
|
sender: win.webContents,
|
|
@@ -289,4 +342,4 @@ function serializeError(error) {
|
|
|
289
342
|
};
|
|
290
343
|
}
|
|
291
344
|
|
|
292
|
-
module.exports = { BridgeServer };
|
|
345
|
+
module.exports = { BridgeServer, redirectRootRelativeApplicationAsset };
|
package/src/browser-window.cjs
CHANGED
|
@@ -10,6 +10,7 @@ const app = require('./app.cjs');
|
|
|
10
10
|
const { WebContents } = require('./web-contents.cjs');
|
|
11
11
|
const { generateBridgeScript } = require('./bridge-script.cjs');
|
|
12
12
|
const { getNativeHost } = require('./native-host.cjs');
|
|
13
|
+
const { getWindowsNativeHost } = require('./windows-native-host.cjs');
|
|
13
14
|
|
|
14
15
|
class BrowserWindow extends EventEmitter {
|
|
15
16
|
constructor(options = {}) {
|
|
@@ -136,6 +137,10 @@ class BrowserWindow extends EventEmitter {
|
|
|
136
137
|
this._nativeHost = getNativeHost(app.getName());
|
|
137
138
|
this._hostAttached = true;
|
|
138
139
|
await this._nativeHost.createWindow(this, config);
|
|
140
|
+
} else if (process.platform === 'win32') {
|
|
141
|
+
this._nativeHost = getWindowsNativeHost();
|
|
142
|
+
this._hostAttached = true;
|
|
143
|
+
await this._nativeHost.createWindow(this, config);
|
|
139
144
|
} else {
|
|
140
145
|
await this._startLegacyHost(config);
|
|
141
146
|
}
|
|
@@ -213,8 +218,7 @@ class BrowserWindow extends EventEmitter {
|
|
|
213
218
|
|
|
214
219
|
_sendHostCommand(command) {
|
|
215
220
|
if (this._nativeHost) {
|
|
216
|
-
this._nativeHost.send({ ...command, windowId: this.id });
|
|
217
|
-
return true;
|
|
221
|
+
return this._nativeHost.send({ ...command, windowId: this.id }) !== false;
|
|
218
222
|
}
|
|
219
223
|
return false;
|
|
220
224
|
}
|
|
@@ -29,7 +29,7 @@ try {
|
|
|
29
29
|
({ Webview, SizeHint } = await import('webview-nodejs'));
|
|
30
30
|
} catch (error) {
|
|
31
31
|
console.error('\nAtomJS could not load the system WebView binding.');
|
|
32
|
-
console.error('
|
|
32
|
+
console.error('Linux currently requires the webview-nodejs package.');
|
|
33
33
|
console.error('Run `atom doctor`, install the platform prerequisites, then install dependencies again.');
|
|
34
34
|
console.error(error && error.stack ? error.stack : error);
|
|
35
35
|
process.exit(1);
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
let singleton = null;
|
|
8
|
+
|
|
9
|
+
class WindowsNativeHost {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.binding = null;
|
|
12
|
+
this.application = null;
|
|
13
|
+
this.webContext = null;
|
|
14
|
+
this.webviewDataDirectory = null;
|
|
15
|
+
this.startPromise = null;
|
|
16
|
+
this.windows = new Map();
|
|
17
|
+
this.stopping = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async ensureStarted() {
|
|
21
|
+
if (process.platform !== 'win32') {
|
|
22
|
+
throw new Error('The in-process Windows native host can only run on Windows.');
|
|
23
|
+
}
|
|
24
|
+
if (!this.startPromise) {
|
|
25
|
+
this.startPromise = this._start().catch((error) => {
|
|
26
|
+
this.startPromise = null;
|
|
27
|
+
throw error;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
await this.startPromise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async _start() {
|
|
34
|
+
let binding;
|
|
35
|
+
try {
|
|
36
|
+
binding = require('@webviewjs/webview');
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const wrapped = new Error([
|
|
39
|
+
'AtomJS could not load the prebuilt Windows WebView binding.',
|
|
40
|
+
'Delete node_modules and package-lock.json, then run npm install again.',
|
|
41
|
+
'CMake and Visual Studio Build Tools are not required.',
|
|
42
|
+
error && error.message ? error.message : String(error)
|
|
43
|
+
].join('\n'));
|
|
44
|
+
wrapped.cause = error;
|
|
45
|
+
throw wrapped;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.binding = binding;
|
|
49
|
+
this.application = new binding.Application();
|
|
50
|
+
await this.application.whenReady({ interval: 16, ref: true });
|
|
51
|
+
|
|
52
|
+
this.webviewDataDirectory = resolveWritableWebViewDataDirectory();
|
|
53
|
+
try {
|
|
54
|
+
this.webContext = this.application.createWebContext({
|
|
55
|
+
dataDirectory: this.webviewDataDirectory,
|
|
56
|
+
allowsAutomation: false
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const wrapped = new Error([
|
|
60
|
+
'AtomJS could not create a writable WebView2 data directory.',
|
|
61
|
+
`Directory: ${this.webviewDataDirectory}`,
|
|
62
|
+
'Check that the current Windows user can write to LOCALAPPDATA.',
|
|
63
|
+
error && error.message ? error.message : String(error)
|
|
64
|
+
].join('\n'));
|
|
65
|
+
wrapped.cause = error;
|
|
66
|
+
throw wrapped;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createWindow(atomWindow, config) {
|
|
71
|
+
await this.ensureStarted();
|
|
72
|
+
|
|
73
|
+
const parent = config.parentWindowId ? this.windows.get(Number(config.parentWindowId)) : null;
|
|
74
|
+
const nativeOptions = {
|
|
75
|
+
title: String(config.title || 'AtomJS App'),
|
|
76
|
+
width: positive(config.width, 800),
|
|
77
|
+
height: positive(config.height, 600),
|
|
78
|
+
logical: true,
|
|
79
|
+
resizable: config.resizable !== false,
|
|
80
|
+
visible: false,
|
|
81
|
+
decorations: config.frame !== false,
|
|
82
|
+
alwaysOnTop: Boolean(config.alwaysOnTop),
|
|
83
|
+
maximizable: config.maximizable !== false,
|
|
84
|
+
minimizable: config.minimizable !== false,
|
|
85
|
+
focused: config.focusable !== false,
|
|
86
|
+
transparent: Boolean(config.transparent),
|
|
87
|
+
windowsSkipTaskbar: Boolean(config.skipTaskbar),
|
|
88
|
+
windowsClassName: sanitizeWindowsClass(process.env.ATOM_APP_ID || process.env.ATOM_APP_NAME || config.title)
|
|
89
|
+
};
|
|
90
|
+
if (Number.isFinite(Number(config.x))) nativeOptions.x = Math.round(Number(config.x));
|
|
91
|
+
if (Number.isFinite(Number(config.y))) nativeOptions.y = Math.round(Number(config.y));
|
|
92
|
+
if (parent && parent.nativeWindow && typeof parent.nativeWindow.getNativeHandle === 'function') {
|
|
93
|
+
nativeOptions.windowsOwnerWindow = parent.nativeWindow.getNativeHandle();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const nativeWindow = this.application.createBrowserWindow(nativeOptions);
|
|
97
|
+
nativeWindow.setClosable(config.closable !== false);
|
|
98
|
+
if (Number(config.minWidth) > 0 || Number(config.minHeight) > 0) {
|
|
99
|
+
nativeWindow.setMinSize(positive(config.minWidth, 1), positive(config.minHeight, 1), true);
|
|
100
|
+
}
|
|
101
|
+
if (Number(config.maxWidth) > 0 || Number(config.maxHeight) > 0) {
|
|
102
|
+
nativeWindow.setMaxSize(positive(config.maxWidth, 100000), positive(config.maxHeight, 100000), true);
|
|
103
|
+
}
|
|
104
|
+
if (config.center !== false && !Number.isFinite(Number(config.x)) && !Number.isFinite(Number(config.y))) {
|
|
105
|
+
nativeWindow.center();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const webview = nativeWindow.createWebview({
|
|
109
|
+
url: String(config.url),
|
|
110
|
+
preload: String(config.bridgeScript || ''),
|
|
111
|
+
enableDevtools: Boolean(config.debug),
|
|
112
|
+
transparent: Boolean(config.transparent),
|
|
113
|
+
webContext: this.webContext
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const record = {
|
|
117
|
+
atomWindow,
|
|
118
|
+
nativeWindow,
|
|
119
|
+
webview,
|
|
120
|
+
dragRegions: [],
|
|
121
|
+
dragViewport: {
|
|
122
|
+
width: positive(config.width, 800),
|
|
123
|
+
height: positive(config.height, 600)
|
|
124
|
+
},
|
|
125
|
+
lastCursorPhysical: null,
|
|
126
|
+
dragState: null,
|
|
127
|
+
lastDragClick: null
|
|
128
|
+
};
|
|
129
|
+
this.windows.set(Number(config.windowId), record);
|
|
130
|
+
this._attachEvents(Number(config.windowId), record);
|
|
131
|
+
|
|
132
|
+
if (config.show === false) nativeWindow.hide();
|
|
133
|
+
else nativeWindow.show();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_attachEvents(windowId, record) {
|
|
137
|
+
const emit = (event) => {
|
|
138
|
+
if (!this.windows.has(windowId)) return;
|
|
139
|
+
try { record.atomWindow._handleHostEvent({ ...event, windowId }); } catch {}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
record.nativeWindow.on('close', () => {
|
|
143
|
+
try { record.atomWindow._handleHostEvent({ type: 'closed', windowId }); } catch {}
|
|
144
|
+
this.windows.delete(windowId);
|
|
145
|
+
});
|
|
146
|
+
record.nativeWindow.on('focus', () => emit({ type: 'focus' }));
|
|
147
|
+
record.nativeWindow.on('blur', () => {
|
|
148
|
+
record.dragState = null;
|
|
149
|
+
emit({ type: 'blur' });
|
|
150
|
+
});
|
|
151
|
+
record.nativeWindow.on('move', (event) => {
|
|
152
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
153
|
+
emit({
|
|
154
|
+
type: 'bounds-changed',
|
|
155
|
+
reason: 'move',
|
|
156
|
+
bounds: { x: Number(event.x) / scale, y: Number(event.y) / scale }
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
record.nativeWindow.on('resize', (event) => {
|
|
160
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
161
|
+
emit({
|
|
162
|
+
type: 'bounds-changed',
|
|
163
|
+
reason: 'resize',
|
|
164
|
+
bounds: { width: Number(event.width) / scale, height: Number(event.height) / scale }
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
record.nativeWindow.on('mouse-move', (event) => {
|
|
168
|
+
const point = physicalPoint(event);
|
|
169
|
+
if (!point) return;
|
|
170
|
+
record.lastCursorPhysical = point;
|
|
171
|
+
if (record.dragState) this._continueWindowDrag(record, point);
|
|
172
|
+
});
|
|
173
|
+
record.nativeWindow.on('mouse-down', (event) => {
|
|
174
|
+
if (Number(event.button) !== 0) return;
|
|
175
|
+
const point = physicalPoint(event);
|
|
176
|
+
if (!point) return;
|
|
177
|
+
record.lastCursorPhysical = point;
|
|
178
|
+
if (!isDraggablePoint(record, point)) return;
|
|
179
|
+
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const previous = record.lastDragClick;
|
|
182
|
+
record.lastDragClick = { time: now, x: point.x, y: point.y };
|
|
183
|
+
if (previous && now - previous.time <= 450 && distanceSquared(previous, point) <= 64) {
|
|
184
|
+
record.dragState = null;
|
|
185
|
+
record.lastDragClick = null;
|
|
186
|
+
try { record.nativeWindow.setMaximized(!record.nativeWindow.isMaximized()); } catch {}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this._beginWindowDrag(record, point);
|
|
191
|
+
});
|
|
192
|
+
record.nativeWindow.on('mouse-up', (event) => {
|
|
193
|
+
if (Number(event.button) === 0) record.dragState = null;
|
|
194
|
+
});
|
|
195
|
+
record.webview.on('page-load-started', (event) => emit({ type: 'did-start-loading', url: event.url || '' }));
|
|
196
|
+
record.webview.on('page-load-finished', (event) => emit({ type: 'did-finish-load', url: event.url || '' }));
|
|
197
|
+
record.webview.on('title-changed', (event) => emit({ type: 'page-title-updated', title: event.title || '' }));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_beginWindowDrag(record, point) {
|
|
201
|
+
if (!point) return false;
|
|
202
|
+
record.dragState = { offsetX: point.x, offsetY: point.y };
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_continueWindowDrag(record, point) {
|
|
207
|
+
const state = record.dragState;
|
|
208
|
+
if (!state) return;
|
|
209
|
+
try {
|
|
210
|
+
const current = record.nativeWindow.getPosition(false);
|
|
211
|
+
const globalX = Number(current.x) + point.x;
|
|
212
|
+
const globalY = Number(current.y) + point.y;
|
|
213
|
+
record.nativeWindow.setPosition(
|
|
214
|
+
Math.round(globalX - state.offsetX),
|
|
215
|
+
Math.round(globalY - state.offsetY),
|
|
216
|
+
false
|
|
217
|
+
);
|
|
218
|
+
} catch {
|
|
219
|
+
record.dragState = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
send(message) {
|
|
224
|
+
const record = this.windows.get(Number(message.windowId));
|
|
225
|
+
if (!record) return false;
|
|
226
|
+
const win = record.nativeWindow;
|
|
227
|
+
const view = record.webview;
|
|
228
|
+
|
|
229
|
+
switch (message.command) {
|
|
230
|
+
case 'navigate': view.loadUrl(String(message.url)); return true;
|
|
231
|
+
case 'show': win.show(); return true;
|
|
232
|
+
case 'hide': win.hide(); return true;
|
|
233
|
+
case 'focus': win.focus(); return true;
|
|
234
|
+
case 'close': win.close(); return true;
|
|
235
|
+
case 'destroy':
|
|
236
|
+
this.windows.delete(Number(message.windowId));
|
|
237
|
+
try { view.dispose(); } catch {}
|
|
238
|
+
try { win.dispose(); } catch {}
|
|
239
|
+
return true;
|
|
240
|
+
case 'set-title': win.setTitle(String(message.title || '')); return true;
|
|
241
|
+
case 'set-always-on-top': win.setAlwaysOnTop(Boolean(message.value)); return true;
|
|
242
|
+
case 'set-resizable': win.setResizable(Boolean(message.value)); return true;
|
|
243
|
+
case 'fullscreen':
|
|
244
|
+
win.setFullscreen(message.value ? this.binding.FullscreenType.Borderless : null);
|
|
245
|
+
return true;
|
|
246
|
+
case 'maximize': win.setMaximized(true); return true;
|
|
247
|
+
case 'unmaximize': win.setMaximized(false); return true;
|
|
248
|
+
case 'minimize': win.setMinimized(true); return true;
|
|
249
|
+
case 'restore':
|
|
250
|
+
win.setMinimized(false);
|
|
251
|
+
win.setMaximized(false);
|
|
252
|
+
win.show();
|
|
253
|
+
return true;
|
|
254
|
+
case 'set-drag-regions':
|
|
255
|
+
record.dragRegions = normalizeDragRegions(message.regions);
|
|
256
|
+
record.dragViewport = normalizeViewport(message.viewport, win);
|
|
257
|
+
return true;
|
|
258
|
+
case 'start-drag':
|
|
259
|
+
return this._beginWindowDrag(record, record.lastCursorPhysical);
|
|
260
|
+
case 'set-bounds': {
|
|
261
|
+
const bounds = message.bounds || {};
|
|
262
|
+
if (Number.isFinite(Number(bounds.width)) && Number.isFinite(Number(bounds.height))) {
|
|
263
|
+
win.setSize(Math.round(Number(bounds.width)), Math.round(Number(bounds.height)), true);
|
|
264
|
+
}
|
|
265
|
+
if (Number.isFinite(Number(bounds.x)) && Number.isFinite(Number(bounds.y))) {
|
|
266
|
+
win.setPosition(Math.round(Number(bounds.x)), Math.round(Number(bounds.y)), true);
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
default: return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async stop() {
|
|
275
|
+
if (this.stopping) return;
|
|
276
|
+
this.stopping = true;
|
|
277
|
+
for (const record of this.windows.values()) {
|
|
278
|
+
try { record.webview.dispose(); } catch {}
|
|
279
|
+
try { record.nativeWindow.dispose(); } catch {}
|
|
280
|
+
}
|
|
281
|
+
this.windows.clear();
|
|
282
|
+
if (this.webContext) {
|
|
283
|
+
try { this.webContext.dispose(); } catch {}
|
|
284
|
+
}
|
|
285
|
+
if (this.application) {
|
|
286
|
+
try { this.application.exit(); } catch {}
|
|
287
|
+
}
|
|
288
|
+
this.webContext = null;
|
|
289
|
+
this.webviewDataDirectory = null;
|
|
290
|
+
this.application = null;
|
|
291
|
+
this.startPromise = null;
|
|
292
|
+
this.stopping = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function positive(value, fallback) {
|
|
297
|
+
const number = Number(value);
|
|
298
|
+
return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function safeScaleFactor(win) {
|
|
302
|
+
try {
|
|
303
|
+
const scale = Number(win.scaleFactor());
|
|
304
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
305
|
+
} catch {
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function physicalPoint(event) {
|
|
311
|
+
const x = Number(event && event.x);
|
|
312
|
+
const y = Number(event && event.y);
|
|
313
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
314
|
+
return { x, y };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeDragRegions(regions) {
|
|
318
|
+
const normalized = [];
|
|
319
|
+
for (const region of Array.isArray(regions) ? regions.slice(0, 4096) : []) {
|
|
320
|
+
const x = Number(region && region.x);
|
|
321
|
+
const y = Number(region && region.y);
|
|
322
|
+
const width = Number(region && region.width);
|
|
323
|
+
const height = Number(region && region.height);
|
|
324
|
+
if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) continue;
|
|
325
|
+
normalized.push({ x, y, width, height, draggable: region.draggable === true });
|
|
326
|
+
}
|
|
327
|
+
return normalized;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeViewport(viewport, win) {
|
|
331
|
+
let fallback = { width: 1, height: 1 };
|
|
332
|
+
try {
|
|
333
|
+
const size = win.getInnerSize(true);
|
|
334
|
+
fallback = { width: positive(size.width, 1), height: positive(size.height, 1) };
|
|
335
|
+
} catch {}
|
|
336
|
+
const width = Number(viewport && viewport.width);
|
|
337
|
+
const height = Number(viewport && viewport.height);
|
|
338
|
+
return {
|
|
339
|
+
width: Number.isFinite(width) && width > 0 ? width : fallback.width,
|
|
340
|
+
height: Number.isFinite(height) && height > 0 ? height : fallback.height
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isDraggablePoint(record, physical) {
|
|
345
|
+
if (!record.dragRegions.length) return false;
|
|
346
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
347
|
+
const logical = { x: physical.x / scale, y: physical.y / scale };
|
|
348
|
+
let inner = { width: record.dragViewport.width, height: record.dragViewport.height };
|
|
349
|
+
try { inner = record.nativeWindow.getInnerSize(true); } catch {}
|
|
350
|
+
const scaleX = record.dragViewport.width > 0 ? Number(inner.width) / record.dragViewport.width : 1;
|
|
351
|
+
const scaleY = record.dragViewport.height > 0 ? Number(inner.height) / record.dragViewport.height : 1;
|
|
352
|
+
let draggable = false;
|
|
353
|
+
|
|
354
|
+
for (const region of record.dragRegions) {
|
|
355
|
+
const inside = logical.x >= region.x * scaleX &&
|
|
356
|
+
logical.x < (region.x + region.width) * scaleX &&
|
|
357
|
+
logical.y >= region.y * scaleY &&
|
|
358
|
+
logical.y < (region.y + region.height) * scaleY;
|
|
359
|
+
if (!inside) continue;
|
|
360
|
+
if (!region.draggable) return false;
|
|
361
|
+
draggable = true;
|
|
362
|
+
}
|
|
363
|
+
return draggable;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function distanceSquared(a, b) {
|
|
367
|
+
const dx = Number(a.x) - Number(b.x);
|
|
368
|
+
const dy = Number(a.y) - Number(b.y);
|
|
369
|
+
return (dx * dx) + (dy * dy);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveWritableWebViewDataDirectory() {
|
|
373
|
+
const identity = sanitizePathSegment(
|
|
374
|
+
process.env.ATOM_APP_ID || process.env.ATOM_APP_NAME || 'AtomJS.App'
|
|
375
|
+
);
|
|
376
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
377
|
+
const candidates = [
|
|
378
|
+
path.join(localAppData, identity, 'AtomJS', 'WebView2'),
|
|
379
|
+
path.join(os.tmpdir(), identity, 'AtomJS', 'WebView2')
|
|
380
|
+
];
|
|
381
|
+
let lastError;
|
|
382
|
+
|
|
383
|
+
for (const candidate of candidates) {
|
|
384
|
+
try {
|
|
385
|
+
fs.mkdirSync(candidate, { recursive: true });
|
|
386
|
+
fs.accessSync(candidate, fs.constants.R_OK | fs.constants.W_OK);
|
|
387
|
+
return candidate;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
lastError = error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
throw new Error(`No writable WebView2 data directory was available: ${lastError ? lastError.message : 'unknown error'}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function sanitizePathSegment(value) {
|
|
397
|
+
return String(value || 'AtomJS.App')
|
|
398
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-')
|
|
399
|
+
.replace(/[. ]+$/g, '')
|
|
400
|
+
.slice(0, 120) || 'AtomJS.App';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function sanitizeWindowsClass(value) {
|
|
404
|
+
return String(value || 'AtomJS.App').replace(/[^A-Za-z0-9._-]+/g, '.').slice(0, 120) || 'AtomJS.App';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getWindowsNativeHost() {
|
|
408
|
+
if (!singleton) singleton = new WindowsNativeHost();
|
|
409
|
+
return singleton;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function stopWindowsNativeHost() {
|
|
413
|
+
if (!singleton) return;
|
|
414
|
+
const current = singleton;
|
|
415
|
+
singleton = null;
|
|
416
|
+
await current.stop();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
WindowsNativeHost,
|
|
421
|
+
getWindowsNativeHost,
|
|
422
|
+
stopWindowsNativeHost,
|
|
423
|
+
isDraggablePoint,
|
|
424
|
+
normalizeDragRegions,
|
|
425
|
+
resolveWritableWebViewDataDirectory
|
|
426
|
+
};
|