@atom-js-org/runtime 0.5.3-alpha.1 → 0.5.3-alpha.3
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 +3 -2
- package/src/bridge-server.cjs +54 -1
- package/src/windows-native-drag.cjs +126 -0
- package/src/windows-native-host.cjs +253 -13
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.3",
|
|
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",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"ws": "^8.21.1",
|
|
26
|
-
"@webviewjs/webview": "0.4.0"
|
|
26
|
+
"@webviewjs/webview": "0.4.0",
|
|
27
|
+
"koffi": "3.1.2"
|
|
27
28
|
},
|
|
28
29
|
"repository": {
|
|
29
30
|
"type": "git",
|
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 };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WM_NCLBUTTONDOWN = 0x00A1;
|
|
4
|
+
const HTCAPTION = 2;
|
|
5
|
+
const VK_LBUTTON = 0x01;
|
|
6
|
+
const SM_CXDOUBLECLK = 36;
|
|
7
|
+
const SM_CYDOUBLECLK = 37;
|
|
8
|
+
|
|
9
|
+
let singleton = null;
|
|
10
|
+
let warned = false;
|
|
11
|
+
|
|
12
|
+
class WindowsNativeDragApi {
|
|
13
|
+
constructor(koffi) {
|
|
14
|
+
if (!koffi || typeof koffi.load !== 'function') {
|
|
15
|
+
throw new TypeError('A Koffi module is required.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const user32 = koffi.load('user32.dll');
|
|
19
|
+
const pointType = koffi.struct('ATOMJS_WIN32_POINT', { x: 'long', y: 'long' });
|
|
20
|
+
this.releaseCapture = user32.func('__stdcall', 'ReleaseCapture', 'bool', []);
|
|
21
|
+
this.sendMessageW = user32.func(
|
|
22
|
+
'__stdcall',
|
|
23
|
+
'SendMessageW',
|
|
24
|
+
'intptr_t',
|
|
25
|
+
['void *', 'uint32_t', 'uintptr_t', 'intptr_t']
|
|
26
|
+
);
|
|
27
|
+
this.getAsyncKeyState = user32.func('__stdcall', 'GetAsyncKeyState', 'int16_t', ['int']);
|
|
28
|
+
this.getCursorPos = user32.func('__stdcall', 'GetCursorPos', 'bool', [koffi.out(koffi.pointer(pointType))]);
|
|
29
|
+
this.getDoubleClickTime = user32.func('__stdcall', 'GetDoubleClickTime', 'uint32_t', []);
|
|
30
|
+
this.getSystemMetrics = user32.func('__stdcall', 'GetSystemMetrics', 'int', ['int']);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
isLeftButtonDown() {
|
|
34
|
+
return (Number(this.getAsyncKeyState(VK_LBUTTON)) & 0x8000) !== 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
doubleClickSettings() {
|
|
38
|
+
return {
|
|
39
|
+
time: positiveInteger(this.getDoubleClickTime(), 500),
|
|
40
|
+
width: positiveInteger(this.getSystemMetrics(SM_CXDOUBLECLK), 4),
|
|
41
|
+
height: positiveInteger(this.getSystemMetrics(SM_CYDOUBLECLK), 4)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
startWindowDrag(nativeWindow) {
|
|
46
|
+
const handle = nativeWindowHandle(nativeWindow);
|
|
47
|
+
if (handle === 0n || !this.isLeftButtonDown()) return false;
|
|
48
|
+
|
|
49
|
+
// Hand control to DefWindowProc instead of moving the window from JavaScript.
|
|
50
|
+
// This enters Windows' normal move loop, including snapping, monitor/DPI
|
|
51
|
+
// transitions and maximized-window restore behavior.
|
|
52
|
+
const cursor = {};
|
|
53
|
+
const cursorPosition = this.getCursorPos(cursor) ? packScreenPoint(cursor.x, cursor.y) : 0n;
|
|
54
|
+
|
|
55
|
+
this.releaseCapture();
|
|
56
|
+
this.sendMessageW(handle, WM_NCLBUTTONDOWN, HTCAPTION, cursorPosition);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function packScreenPoint(x, y) {
|
|
63
|
+
const xWord = BigInt.asUintN(16, BigInt(Math.trunc(Number(x) || 0)));
|
|
64
|
+
const yWord = BigInt.asUintN(16, BigInt(Math.trunc(Number(y) || 0)));
|
|
65
|
+
return BigInt.asIntN(32, xWord | (yWord << 16n));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nativeWindowHandle(nativeWindow) {
|
|
69
|
+
if (!nativeWindow) return 0n;
|
|
70
|
+
|
|
71
|
+
let value = 0n;
|
|
72
|
+
try {
|
|
73
|
+
if (typeof nativeWindow.getNativeHandleAnyThread === 'function') {
|
|
74
|
+
value = nativeWindow.getNativeHandleAnyThread();
|
|
75
|
+
} else if (typeof nativeWindow.getNativeHandle === 'function') {
|
|
76
|
+
value = nativeWindow.getNativeHandle();
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
return 0n;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return BigInt(value || 0);
|
|
84
|
+
} catch {
|
|
85
|
+
return 0n;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function positiveInteger(value, fallback) {
|
|
90
|
+
const number = Number(value);
|
|
91
|
+
return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getWindowsNativeDragApi() {
|
|
95
|
+
if (process.platform !== 'win32') return null;
|
|
96
|
+
if (singleton) return singleton;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
singleton = new WindowsNativeDragApi(require('koffi'));
|
|
100
|
+
return singleton;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!warned) {
|
|
103
|
+
warned = true;
|
|
104
|
+
console.warn([
|
|
105
|
+
'[AtomJS] Native Windows window dragging could not be initialized.',
|
|
106
|
+
'Run npm install so the prebuilt koffi Windows package is present.',
|
|
107
|
+
error && error.message ? error.message : String(error)
|
|
108
|
+
].join('\n'));
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
WindowsNativeDragApi,
|
|
116
|
+
getWindowsNativeDragApi,
|
|
117
|
+
nativeWindowHandle,
|
|
118
|
+
packScreenPoint,
|
|
119
|
+
constants: {
|
|
120
|
+
WM_NCLBUTTONDOWN,
|
|
121
|
+
HTCAPTION,
|
|
122
|
+
VK_LBUTTON,
|
|
123
|
+
SM_CXDOUBLECLK,
|
|
124
|
+
SM_CYDOUBLECLK
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { getWindowsNativeDragApi } = require('./windows-native-drag.cjs');
|
|
7
|
+
|
|
3
8
|
let singleton = null;
|
|
4
9
|
|
|
5
10
|
class WindowsNativeHost {
|
|
6
11
|
constructor() {
|
|
7
12
|
this.binding = null;
|
|
8
13
|
this.application = null;
|
|
14
|
+
this.webContext = null;
|
|
15
|
+
this.webviewDataDirectory = null;
|
|
9
16
|
this.startPromise = null;
|
|
10
17
|
this.windows = new Map();
|
|
11
18
|
this.stopping = false;
|
|
@@ -42,6 +49,23 @@ class WindowsNativeHost {
|
|
|
42
49
|
this.binding = binding;
|
|
43
50
|
this.application = new binding.Application();
|
|
44
51
|
await this.application.whenReady({ interval: 16, ref: true });
|
|
52
|
+
|
|
53
|
+
this.webviewDataDirectory = resolveWritableWebViewDataDirectory();
|
|
54
|
+
try {
|
|
55
|
+
this.webContext = this.application.createWebContext({
|
|
56
|
+
dataDirectory: this.webviewDataDirectory,
|
|
57
|
+
allowsAutomation: false
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const wrapped = new Error([
|
|
61
|
+
'AtomJS could not create a writable WebView2 data directory.',
|
|
62
|
+
`Directory: ${this.webviewDataDirectory}`,
|
|
63
|
+
'Check that the current Windows user can write to LOCALAPPDATA.',
|
|
64
|
+
error && error.message ? error.message : String(error)
|
|
65
|
+
].join('\n'));
|
|
66
|
+
wrapped.cause = error;
|
|
67
|
+
throw wrapped;
|
|
68
|
+
}
|
|
45
69
|
}
|
|
46
70
|
|
|
47
71
|
async createWindow(atomWindow, config) {
|
|
@@ -52,6 +76,7 @@ class WindowsNativeHost {
|
|
|
52
76
|
title: String(config.title || 'AtomJS App'),
|
|
53
77
|
width: positive(config.width, 800),
|
|
54
78
|
height: positive(config.height, 600),
|
|
79
|
+
logical: true,
|
|
55
80
|
resizable: config.resizable !== false,
|
|
56
81
|
visible: false,
|
|
57
82
|
decorations: config.frame !== false,
|
|
@@ -85,10 +110,22 @@ class WindowsNativeHost {
|
|
|
85
110
|
url: String(config.url),
|
|
86
111
|
preload: String(config.bridgeScript || ''),
|
|
87
112
|
enableDevtools: Boolean(config.debug),
|
|
88
|
-
transparent: Boolean(config.transparent)
|
|
113
|
+
transparent: Boolean(config.transparent),
|
|
114
|
+
webContext: this.webContext
|
|
89
115
|
});
|
|
90
116
|
|
|
91
|
-
const record = {
|
|
117
|
+
const record = {
|
|
118
|
+
windowId: Number(config.windowId),
|
|
119
|
+
atomWindow,
|
|
120
|
+
nativeWindow,
|
|
121
|
+
webview,
|
|
122
|
+
dragRegions: [],
|
|
123
|
+
dragViewport: {
|
|
124
|
+
width: positive(config.width, 800),
|
|
125
|
+
height: positive(config.height, 600)
|
|
126
|
+
},
|
|
127
|
+
lastDragClick: null
|
|
128
|
+
};
|
|
92
129
|
this.windows.set(Number(config.windowId), record);
|
|
93
130
|
this._attachEvents(Number(config.windowId), record);
|
|
94
131
|
|
|
@@ -108,21 +145,57 @@ class WindowsNativeHost {
|
|
|
108
145
|
});
|
|
109
146
|
record.nativeWindow.on('focus', () => emit({ type: 'focus' }));
|
|
110
147
|
record.nativeWindow.on('blur', () => emit({ type: 'blur' }));
|
|
111
|
-
record.nativeWindow.on('move', (event) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
record.nativeWindow.on('move', (event) => {
|
|
149
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
150
|
+
emit({
|
|
151
|
+
type: 'bounds-changed',
|
|
152
|
+
reason: 'move',
|
|
153
|
+
bounds: { x: Number(event.x) / scale, y: Number(event.y) / scale }
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
record.nativeWindow.on('resize', (event) => {
|
|
157
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
158
|
+
emit({
|
|
159
|
+
type: 'bounds-changed',
|
|
160
|
+
reason: 'resize',
|
|
161
|
+
bounds: { width: Number(event.width) / scale, height: Number(event.height) / scale }
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
record.nativeWindow.on('mouse-down', (event) => {
|
|
165
|
+
if (Number(event.button) !== 0) return;
|
|
166
|
+
const point = physicalPoint(event);
|
|
167
|
+
if (!point || !isDraggablePoint(record, point)) return;
|
|
168
|
+
this._startNativeWindowDrag(record, point);
|
|
169
|
+
});
|
|
121
170
|
record.webview.on('page-load-started', (event) => emit({ type: 'did-start-loading', url: event.url || '' }));
|
|
122
171
|
record.webview.on('page-load-finished', (event) => emit({ type: 'did-finish-load', url: event.url || '' }));
|
|
123
172
|
record.webview.on('title-changed', (event) => emit({ type: 'page-title-updated', title: event.title || '' }));
|
|
124
173
|
}
|
|
125
174
|
|
|
175
|
+
_startNativeWindowDrag(record, point = null) {
|
|
176
|
+
const nativeDrag = getWindowsNativeDragApi();
|
|
177
|
+
if (!nativeDrag) return false;
|
|
178
|
+
|
|
179
|
+
if (point && isSystemDoubleClick(record, point, nativeDrag.doubleClickSettings())) {
|
|
180
|
+
record.lastDragClick = null;
|
|
181
|
+
try { record.nativeWindow.setMaximized(!record.nativeWindow.isMaximized()); } catch {}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const before = readPhysicalWindowPosition(record.nativeWindow);
|
|
186
|
+
const started = nativeDrag.startWindowDrag(record.nativeWindow);
|
|
187
|
+
if (!started) return false;
|
|
188
|
+
|
|
189
|
+
// SendMessageW returns when the native move loop ends. A real movement must
|
|
190
|
+
// not become the first half of a later title-bar double click.
|
|
191
|
+
const after = readPhysicalWindowPosition(record.nativeWindow);
|
|
192
|
+
if (!samePhysicalPosition(before, after)) {
|
|
193
|
+
record.lastDragClick = null;
|
|
194
|
+
emitFinalWindowBounds(record);
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
126
199
|
send(message) {
|
|
127
200
|
const record = this.windows.get(Number(message.windowId));
|
|
128
201
|
if (!record) return false;
|
|
@@ -154,6 +227,12 @@ class WindowsNativeHost {
|
|
|
154
227
|
win.setMaximized(false);
|
|
155
228
|
win.show();
|
|
156
229
|
return true;
|
|
230
|
+
case 'set-drag-regions':
|
|
231
|
+
record.dragRegions = normalizeDragRegions(message.regions);
|
|
232
|
+
record.dragViewport = normalizeViewport(message.viewport, win);
|
|
233
|
+
return true;
|
|
234
|
+
case 'start-drag':
|
|
235
|
+
return this._startNativeWindowDrag(record);
|
|
157
236
|
case 'set-bounds': {
|
|
158
237
|
const bounds = message.bounds || {};
|
|
159
238
|
if (Number.isFinite(Number(bounds.width)) && Number.isFinite(Number(bounds.height))) {
|
|
@@ -176,9 +255,14 @@ class WindowsNativeHost {
|
|
|
176
255
|
try { record.nativeWindow.dispose(); } catch {}
|
|
177
256
|
}
|
|
178
257
|
this.windows.clear();
|
|
258
|
+
if (this.webContext) {
|
|
259
|
+
try { this.webContext.dispose(); } catch {}
|
|
260
|
+
}
|
|
179
261
|
if (this.application) {
|
|
180
262
|
try { this.application.exit(); } catch {}
|
|
181
263
|
}
|
|
264
|
+
this.webContext = null;
|
|
265
|
+
this.webviewDataDirectory = null;
|
|
182
266
|
this.application = null;
|
|
183
267
|
this.startPromise = null;
|
|
184
268
|
this.stopping = false;
|
|
@@ -190,6 +274,152 @@ function positive(value, fallback) {
|
|
|
190
274
|
return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
|
|
191
275
|
}
|
|
192
276
|
|
|
277
|
+
function safeScaleFactor(win) {
|
|
278
|
+
try {
|
|
279
|
+
const scale = Number(win.scaleFactor());
|
|
280
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
281
|
+
} catch {
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function physicalPoint(event) {
|
|
287
|
+
const x = Number(event && event.x);
|
|
288
|
+
const y = Number(event && event.y);
|
|
289
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
290
|
+
return { x, y };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeDragRegions(regions) {
|
|
294
|
+
const normalized = [];
|
|
295
|
+
for (const region of Array.isArray(regions) ? regions.slice(0, 4096) : []) {
|
|
296
|
+
const x = Number(region && region.x);
|
|
297
|
+
const y = Number(region && region.y);
|
|
298
|
+
const width = Number(region && region.width);
|
|
299
|
+
const height = Number(region && region.height);
|
|
300
|
+
if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) continue;
|
|
301
|
+
normalized.push({ x, y, width, height, draggable: region.draggable === true });
|
|
302
|
+
}
|
|
303
|
+
return normalized;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeViewport(viewport, win) {
|
|
307
|
+
let fallback = { width: 1, height: 1 };
|
|
308
|
+
try {
|
|
309
|
+
const size = win.getInnerSize(true);
|
|
310
|
+
fallback = { width: positive(size.width, 1), height: positive(size.height, 1) };
|
|
311
|
+
} catch {}
|
|
312
|
+
const width = Number(viewport && viewport.width);
|
|
313
|
+
const height = Number(viewport && viewport.height);
|
|
314
|
+
return {
|
|
315
|
+
width: Number.isFinite(width) && width > 0 ? width : fallback.width,
|
|
316
|
+
height: Number.isFinite(height) && height > 0 ? height : fallback.height
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isDraggablePoint(record, physical) {
|
|
321
|
+
if (!record.dragRegions.length) return false;
|
|
322
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
323
|
+
const logical = { x: physical.x / scale, y: physical.y / scale };
|
|
324
|
+
let inner = { width: record.dragViewport.width, height: record.dragViewport.height };
|
|
325
|
+
try { inner = record.nativeWindow.getInnerSize(true); } catch {}
|
|
326
|
+
const scaleX = record.dragViewport.width > 0 ? Number(inner.width) / record.dragViewport.width : 1;
|
|
327
|
+
const scaleY = record.dragViewport.height > 0 ? Number(inner.height) / record.dragViewport.height : 1;
|
|
328
|
+
let draggable = false;
|
|
329
|
+
|
|
330
|
+
for (const region of record.dragRegions) {
|
|
331
|
+
const inside = logical.x >= region.x * scaleX &&
|
|
332
|
+
logical.x < (region.x + region.width) * scaleX &&
|
|
333
|
+
logical.y >= region.y * scaleY &&
|
|
334
|
+
logical.y < (region.y + region.height) * scaleY;
|
|
335
|
+
if (!inside) continue;
|
|
336
|
+
if (!region.draggable) return false;
|
|
337
|
+
draggable = true;
|
|
338
|
+
}
|
|
339
|
+
return draggable;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isSystemDoubleClick(record, point, settings) {
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
const previous = record.lastDragClick;
|
|
345
|
+
record.lastDragClick = { time: now, x: point.x, y: point.y };
|
|
346
|
+
if (!previous) return false;
|
|
347
|
+
|
|
348
|
+
const halfWidth = Math.max(1, Number(settings && settings.width) / 2);
|
|
349
|
+
const halfHeight = Math.max(1, Number(settings && settings.height) / 2);
|
|
350
|
+
return now - previous.time <= Number(settings && settings.time || 500) &&
|
|
351
|
+
Math.abs(Number(previous.x) - Number(point.x)) <= halfWidth &&
|
|
352
|
+
Math.abs(Number(previous.y) - Number(point.y)) <= halfHeight;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function readPhysicalWindowPosition(win) {
|
|
356
|
+
try {
|
|
357
|
+
const position = win.getPosition(false);
|
|
358
|
+
const x = Number(position && position.x);
|
|
359
|
+
const y = Number(position && position.y);
|
|
360
|
+
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
function emitFinalWindowBounds(record) {
|
|
368
|
+
try {
|
|
369
|
+
const win = record.nativeWindow;
|
|
370
|
+
const scale = safeScaleFactor(win);
|
|
371
|
+
const position = win.getPosition(false);
|
|
372
|
+
const size = win.getInnerSize(true);
|
|
373
|
+
record.atomWindow._handleHostEvent({
|
|
374
|
+
type: 'bounds-changed',
|
|
375
|
+
reason: 'move',
|
|
376
|
+
windowId: record.windowId,
|
|
377
|
+
bounds: {
|
|
378
|
+
x: Number(position.x) / scale,
|
|
379
|
+
y: Number(position.y) / scale,
|
|
380
|
+
width: Number(size.width),
|
|
381
|
+
height: Number(size.height)
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
} catch {}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function samePhysicalPosition(a, b) {
|
|
388
|
+
if (!a || !b) return true;
|
|
389
|
+
return Math.abs(a.x - b.x) <= 1 && Math.abs(a.y - b.y) <= 1;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function resolveWritableWebViewDataDirectory() {
|
|
393
|
+
const identity = sanitizePathSegment(
|
|
394
|
+
process.env.ATOM_APP_ID || process.env.ATOM_APP_NAME || 'AtomJS.App'
|
|
395
|
+
);
|
|
396
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
397
|
+
const candidates = [
|
|
398
|
+
path.join(localAppData, identity, 'AtomJS', 'WebView2'),
|
|
399
|
+
path.join(os.tmpdir(), identity, 'AtomJS', 'WebView2')
|
|
400
|
+
];
|
|
401
|
+
let lastError;
|
|
402
|
+
|
|
403
|
+
for (const candidate of candidates) {
|
|
404
|
+
try {
|
|
405
|
+
fs.mkdirSync(candidate, { recursive: true });
|
|
406
|
+
fs.accessSync(candidate, fs.constants.R_OK | fs.constants.W_OK);
|
|
407
|
+
return candidate;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
lastError = error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
throw new Error(`No writable WebView2 data directory was available: ${lastError ? lastError.message : 'unknown error'}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function sanitizePathSegment(value) {
|
|
417
|
+
return String(value || 'AtomJS.App')
|
|
418
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-')
|
|
419
|
+
.replace(/[. ]+$/g, '')
|
|
420
|
+
.slice(0, 120) || 'AtomJS.App';
|
|
421
|
+
}
|
|
422
|
+
|
|
193
423
|
function sanitizeWindowsClass(value) {
|
|
194
424
|
return String(value || 'AtomJS.App').replace(/[^A-Za-z0-9._-]+/g, '.').slice(0, 120) || 'AtomJS.App';
|
|
195
425
|
}
|
|
@@ -206,4 +436,14 @@ async function stopWindowsNativeHost() {
|
|
|
206
436
|
await current.stop();
|
|
207
437
|
}
|
|
208
438
|
|
|
209
|
-
module.exports = {
|
|
439
|
+
module.exports = {
|
|
440
|
+
WindowsNativeHost,
|
|
441
|
+
getWindowsNativeHost,
|
|
442
|
+
stopWindowsNativeHost,
|
|
443
|
+
isDraggablePoint,
|
|
444
|
+
normalizeDragRegions,
|
|
445
|
+
resolveWritableWebViewDataDirectory,
|
|
446
|
+
isSystemDoubleClick,
|
|
447
|
+
emitFinalWindowBounds,
|
|
448
|
+
samePhysicalPosition
|
|
449
|
+
};
|