@atom-js-org/runtime 0.5.3-alpha.2 → 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/windows-native-drag.cjs +126 -0
- package/src/windows-native-host.cjs +78 -55
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",
|
|
@@ -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
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { getWindowsNativeDragApi } = require('./windows-native-drag.cjs');
|
|
6
7
|
|
|
7
8
|
let singleton = null;
|
|
8
9
|
|
|
@@ -114,6 +115,7 @@ class WindowsNativeHost {
|
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
const record = {
|
|
118
|
+
windowId: Number(config.windowId),
|
|
117
119
|
atomWindow,
|
|
118
120
|
nativeWindow,
|
|
119
121
|
webview,
|
|
@@ -122,8 +124,6 @@ class WindowsNativeHost {
|
|
|
122
124
|
width: positive(config.width, 800),
|
|
123
125
|
height: positive(config.height, 600)
|
|
124
126
|
},
|
|
125
|
-
lastCursorPhysical: null,
|
|
126
|
-
dragState: null,
|
|
127
127
|
lastDragClick: null
|
|
128
128
|
};
|
|
129
129
|
this.windows.set(Number(config.windowId), record);
|
|
@@ -144,10 +144,7 @@ class WindowsNativeHost {
|
|
|
144
144
|
this.windows.delete(windowId);
|
|
145
145
|
});
|
|
146
146
|
record.nativeWindow.on('focus', () => emit({ type: 'focus' }));
|
|
147
|
-
record.nativeWindow.on('blur', () => {
|
|
148
|
-
record.dragState = null;
|
|
149
|
-
emit({ type: 'blur' });
|
|
150
|
-
});
|
|
147
|
+
record.nativeWindow.on('blur', () => emit({ type: 'blur' }));
|
|
151
148
|
record.nativeWindow.on('move', (event) => {
|
|
152
149
|
const scale = safeScaleFactor(record.nativeWindow);
|
|
153
150
|
emit({
|
|
@@ -164,60 +161,39 @@ class WindowsNativeHost {
|
|
|
164
161
|
bounds: { width: Number(event.width) / scale, height: Number(event.height) / scale }
|
|
165
162
|
});
|
|
166
163
|
});
|
|
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
164
|
record.nativeWindow.on('mouse-down', (event) => {
|
|
174
165
|
if (Number(event.button) !== 0) return;
|
|
175
166
|
const point = physicalPoint(event);
|
|
176
|
-
if (!point) return;
|
|
177
|
-
record
|
|
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;
|
|
167
|
+
if (!point || !isDraggablePoint(record, point)) return;
|
|
168
|
+
this._startNativeWindowDrag(record, point);
|
|
194
169
|
});
|
|
195
170
|
record.webview.on('page-load-started', (event) => emit({ type: 'did-start-loading', url: event.url || '' }));
|
|
196
171
|
record.webview.on('page-load-finished', (event) => emit({ type: 'did-finish-load', url: event.url || '' }));
|
|
197
172
|
record.webview.on('title-changed', (event) => emit({ type: 'page-title-updated', title: event.title || '' }));
|
|
198
173
|
}
|
|
199
174
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
175
|
+
_startNativeWindowDrag(record, point = null) {
|
|
176
|
+
const nativeDrag = getWindowsNativeDragApi();
|
|
177
|
+
if (!nativeDrag) return false;
|
|
205
178
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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);
|
|
220
195
|
}
|
|
196
|
+
return true;
|
|
221
197
|
}
|
|
222
198
|
|
|
223
199
|
send(message) {
|
|
@@ -256,7 +232,7 @@ class WindowsNativeHost {
|
|
|
256
232
|
record.dragViewport = normalizeViewport(message.viewport, win);
|
|
257
233
|
return true;
|
|
258
234
|
case 'start-drag':
|
|
259
|
-
return this.
|
|
235
|
+
return this._startNativeWindowDrag(record);
|
|
260
236
|
case 'set-bounds': {
|
|
261
237
|
const bounds = message.bounds || {};
|
|
262
238
|
if (Number.isFinite(Number(bounds.width)) && Number.isFinite(Number(bounds.height))) {
|
|
@@ -363,10 +339,54 @@ function isDraggablePoint(record, physical) {
|
|
|
363
339
|
return draggable;
|
|
364
340
|
}
|
|
365
341
|
|
|
366
|
-
function
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
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;
|
|
370
390
|
}
|
|
371
391
|
|
|
372
392
|
function resolveWritableWebViewDataDirectory() {
|
|
@@ -422,5 +442,8 @@ module.exports = {
|
|
|
422
442
|
stopWindowsNativeHost,
|
|
423
443
|
isDraggablePoint,
|
|
424
444
|
normalizeDragRegions,
|
|
425
|
-
resolveWritableWebViewDataDirectory
|
|
445
|
+
resolveWritableWebViewDataDirectory,
|
|
446
|
+
isSystemDoubleClick,
|
|
447
|
+
emitFinalWindowBounds,
|
|
448
|
+
samePhysicalPosition
|
|
426
449
|
};
|