@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/runtime",
3
- "version": "0.5.3-alpha.2",
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
- lastCursorPhysical: null,
126
- dragState: null,
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.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);
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.dragState = null;
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
- _beginWindowDrag(record, point) {
201
- if (!point) return false;
202
- record.dragState = { offsetX: point.x, offsetY: point.y };
203
- return true;
204
- }
183
+ _startNativeWindowDrag(record, point = null) {
184
+ const nativeDrag = getWindowsNativeDragApi();
185
+ if (!nativeDrag) return false;
205
186
 
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;
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._beginWindowDrag(record, record.lastCursorPhysical);
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 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);
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
  };