@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 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.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.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;
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
- _beginWindowDrag(record, point) {
201
- if (!point) return false;
202
- record.dragState = { offsetX: point.x, offsetY: point.y };
203
- return true;
204
- }
175
+ _startNativeWindowDrag(record, point = null) {
176
+ const nativeDrag = getWindowsNativeDragApi();
177
+ if (!nativeDrag) return false;
205
178
 
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;
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._beginWindowDrag(record, record.lastCursorPhysical);
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 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);
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
  };