@atom-js-org/runtime 0.5.3-alpha.2 → 0.5.3-alpha.4
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 +42 -54
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.4",
|
|
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.postMessageW = user32.func(
|
|
22
|
+
'__stdcall',
|
|
23
|
+
'PostMessageW',
|
|
24
|
+
'bool',
|
|
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
|
+
// Queue the standard non-client title-bar press and return immediately.
|
|
50
|
+
// PostMessageW is important here: SendMessageW blocks Node while Windows runs
|
|
51
|
+
// its modal move loop, which causes frozen rendering and erratic input. Tao's
|
|
52
|
+
// own Window::drag_window() follows the same asynchronous Win32 path.
|
|
53
|
+
const cursor = {};
|
|
54
|
+
const cursorPosition = this.getCursorPos(cursor) ? packScreenPoint(cursor.x, cursor.y) : 0n;
|
|
55
|
+
|
|
56
|
+
this.releaseCapture();
|
|
57
|
+
return Boolean(this.postMessageW(handle, WM_NCLBUTTONDOWN, HTCAPTION, cursorPosition));
|
|
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,9 +124,8 @@ class WindowsNativeHost {
|
|
|
122
124
|
width: positive(config.width, 800),
|
|
123
125
|
height: positive(config.height, 600)
|
|
124
126
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
lastDragClick: null
|
|
127
|
+
lastDragClick: null,
|
|
128
|
+
nativeDragPending: false
|
|
128
129
|
};
|
|
129
130
|
this.windows.set(Number(config.windowId), record);
|
|
130
131
|
this._attachEvents(Number(config.windowId), record);
|
|
@@ -144,11 +145,12 @@ class WindowsNativeHost {
|
|
|
144
145
|
this.windows.delete(windowId);
|
|
145
146
|
});
|
|
146
147
|
record.nativeWindow.on('focus', () => emit({ type: 'focus' }));
|
|
147
|
-
record.nativeWindow.on('blur', () => {
|
|
148
|
-
record.dragState = null;
|
|
149
|
-
emit({ type: 'blur' });
|
|
150
|
-
});
|
|
148
|
+
record.nativeWindow.on('blur', () => emit({ type: 'blur' }));
|
|
151
149
|
record.nativeWindow.on('move', (event) => {
|
|
150
|
+
if (record.nativeDragPending) {
|
|
151
|
+
record.nativeDragPending = false;
|
|
152
|
+
record.lastDragClick = null;
|
|
153
|
+
}
|
|
152
154
|
const scale = safeScaleFactor(record.nativeWindow);
|
|
153
155
|
emit({
|
|
154
156
|
type: 'bounds-changed',
|
|
@@ -164,60 +166,37 @@ class WindowsNativeHost {
|
|
|
164
166
|
bounds: { width: Number(event.width) / scale, height: Number(event.height) / scale }
|
|
165
167
|
});
|
|
166
168
|
});
|
|
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
169
|
record.nativeWindow.on('mouse-down', (event) => {
|
|
174
170
|
if (Number(event.button) !== 0) return;
|
|
175
171
|
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);
|
|
172
|
+
if (!point || !isDraggablePoint(record, point)) return;
|
|
173
|
+
this._startNativeWindowDrag(record, point);
|
|
191
174
|
});
|
|
192
175
|
record.nativeWindow.on('mouse-up', (event) => {
|
|
193
|
-
if (Number(event.button) === 0) record.
|
|
176
|
+
if (Number(event.button) === 0) record.nativeDragPending = false;
|
|
194
177
|
});
|
|
195
178
|
record.webview.on('page-load-started', (event) => emit({ type: 'did-start-loading', url: event.url || '' }));
|
|
196
179
|
record.webview.on('page-load-finished', (event) => emit({ type: 'did-finish-load', url: event.url || '' }));
|
|
197
180
|
record.webview.on('title-changed', (event) => emit({ type: 'page-title-updated', title: event.title || '' }));
|
|
198
181
|
}
|
|
199
182
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
183
|
+
_startNativeWindowDrag(record, point = null) {
|
|
184
|
+
const nativeDrag = getWindowsNativeDragApi();
|
|
185
|
+
if (!nativeDrag) return false;
|
|
205
186
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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;
|
|
187
|
+
if (point && isSystemDoubleClick(record, point, nativeDrag.doubleClickSettings())) {
|
|
188
|
+
record.lastDragClick = null;
|
|
189
|
+
try { record.nativeWindow.setMaximized(!record.nativeWindow.isMaximized()); } catch {}
|
|
190
|
+
return true;
|
|
220
191
|
}
|
|
192
|
+
|
|
193
|
+
const started = nativeDrag.startWindowDrag(record.nativeWindow);
|
|
194
|
+
if (!started) return false;
|
|
195
|
+
|
|
196
|
+
// The Win32 move loop is queued asynchronously. Native move events keep the
|
|
197
|
+
// BrowserWindow bounds synchronized while Windows owns the pointer.
|
|
198
|
+
record.nativeDragPending = true;
|
|
199
|
+
return true;
|
|
221
200
|
}
|
|
222
201
|
|
|
223
202
|
send(message) {
|
|
@@ -256,7 +235,7 @@ class WindowsNativeHost {
|
|
|
256
235
|
record.dragViewport = normalizeViewport(message.viewport, win);
|
|
257
236
|
return true;
|
|
258
237
|
case 'start-drag':
|
|
259
|
-
return this.
|
|
238
|
+
return this._startNativeWindowDrag(record);
|
|
260
239
|
case 'set-bounds': {
|
|
261
240
|
const bounds = message.bounds || {};
|
|
262
241
|
if (Number.isFinite(Number(bounds.width)) && Number.isFinite(Number(bounds.height))) {
|
|
@@ -363,12 +342,20 @@ function isDraggablePoint(record, physical) {
|
|
|
363
342
|
return draggable;
|
|
364
343
|
}
|
|
365
344
|
|
|
366
|
-
function
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
345
|
+
function isSystemDoubleClick(record, point, settings) {
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
const previous = record.lastDragClick;
|
|
348
|
+
record.lastDragClick = { time: now, x: point.x, y: point.y };
|
|
349
|
+
if (!previous) return false;
|
|
350
|
+
|
|
351
|
+
const halfWidth = Math.max(1, Number(settings && settings.width) / 2);
|
|
352
|
+
const halfHeight = Math.max(1, Number(settings && settings.height) / 2);
|
|
353
|
+
return now - previous.time <= Number(settings && settings.time || 500) &&
|
|
354
|
+
Math.abs(Number(previous.x) - Number(point.x)) <= halfWidth &&
|
|
355
|
+
Math.abs(Number(previous.y) - Number(point.y)) <= halfHeight;
|
|
370
356
|
}
|
|
371
357
|
|
|
358
|
+
|
|
372
359
|
function resolveWritableWebViewDataDirectory() {
|
|
373
360
|
const identity = sanitizePathSegment(
|
|
374
361
|
process.env.ATOM_APP_ID || process.env.ATOM_APP_NAME || 'AtomJS.App'
|
|
@@ -422,5 +409,6 @@ module.exports = {
|
|
|
422
409
|
stopWindowsNativeHost,
|
|
423
410
|
isDraggablePoint,
|
|
424
411
|
normalizeDragRegions,
|
|
425
|
-
resolveWritableWebViewDataDirectory
|
|
412
|
+
resolveWritableWebViewDataDirectory,
|
|
413
|
+
isSystemDoubleClick
|
|
426
414
|
};
|