@atom-js-org/runtime 0.5.0-alpha.0 → 0.5.2-alpha.0

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/index.d.ts CHANGED
@@ -61,6 +61,7 @@ export class BrowserWindow extends EventEmitter {
61
61
  hide(): void;
62
62
  isVisible(): boolean;
63
63
  focus(): void;
64
+ startDrag(): void;
64
65
  blur(): void;
65
66
  close(): void;
66
67
  destroy(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/runtime",
3
- "version": "0.5.0-alpha.0",
3
+ "version": "0.5.2-alpha.0",
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",
@@ -39,6 +39,68 @@ function generateBridgeScript({ websocketUrl, preloadCode = '' }) {
39
39
  }
40
40
  }
41
41
 
42
+ function normalizeAppRegion(value) {
43
+ const normalized = String(value || '').trim().toLowerCase();
44
+ return normalized === 'drag' || normalized === 'no-drag' ? normalized : '';
45
+ }
46
+
47
+ function appRegionForElement(element) {
48
+ if (!(element instanceof Element)) return '';
49
+ if (element.hasAttribute('data-atom-no-drag')) return 'no-drag';
50
+ if (element.hasAttribute('data-atom-drag-region')) return 'drag';
51
+
52
+ const attribute = normalizeAppRegion(
53
+ element.getAttribute('data-atom-app-region') || element.getAttribute('data-app-region')
54
+ );
55
+ if (attribute) return attribute;
56
+
57
+ const inline = normalizeAppRegion(
58
+ element.style && (
59
+ element.style.getPropertyValue('-webkit-app-region') ||
60
+ element.style.getPropertyValue('app-region')
61
+ )
62
+ );
63
+ if (inline) return inline;
64
+
65
+ try {
66
+ const computed = getComputedStyle(element);
67
+ return normalizeAppRegion(
68
+ computed.getPropertyValue('-webkit-app-region') ||
69
+ computed.getPropertyValue('app-region')
70
+ );
71
+ } catch {
72
+ return '';
73
+ }
74
+ }
75
+
76
+ function isInteractiveDragTarget(element) {
77
+ return element instanceof Element && Boolean(element.closest(
78
+ 'button, input, textarea, select, option, a[href], [contenteditable=""], [contenteditable="true"], [role="button"]'
79
+ ));
80
+ }
81
+
82
+ function shouldStartWindowDrag(event) {
83
+ if (event.button !== 0 || event.defaultPrevented) return false;
84
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [event.target];
85
+ let draggable = false;
86
+
87
+ for (const entry of path) {
88
+ if (!(entry instanceof Element)) continue;
89
+ const region = appRegionForElement(entry);
90
+ if (region === 'no-drag') return false;
91
+ if (region === 'drag') draggable = true;
92
+ }
93
+
94
+ if (!draggable || isInteractiveDragTarget(event.target)) return false;
95
+ return true;
96
+ }
97
+
98
+ document.addEventListener('mousedown', (event) => {
99
+ if (!shouldStartWindowDrag(event)) return;
100
+ event.preventDefault();
101
+ sendNow({ type: 'system', command: 'start-window-drag' });
102
+ }, true);
103
+
42
104
  function makeEvent(channel) {
43
105
  return Object.freeze({
44
106
  channel,
@@ -163,6 +163,11 @@ class BridgeServer {
163
163
  return;
164
164
  }
165
165
 
166
+ if (message.type === 'system') {
167
+ if (message.command === 'start-window-drag') win._startNativeDrag();
168
+ return;
169
+ }
170
+
166
171
  if (message.type === 'send') {
167
172
  const event = createIpcEvent(win);
168
173
  ipcMain.emit(message.channel, event, ...(Array.isArray(message.args) ? message.args : []));
@@ -219,6 +219,11 @@ class BrowserWindow extends EventEmitter {
219
219
  return false;
220
220
  }
221
221
 
222
+ _startNativeDrag() {
223
+ if (this._destroyed) return false;
224
+ return this._sendHostCommand({ command: 'start-drag' });
225
+ }
226
+
222
227
  _markRendererReady(details) {
223
228
  this._notifyDidFinishLoad(details && details.href ? details.href : this._currentUrl, 'bridge');
224
229
  }
@@ -334,6 +339,12 @@ class BrowserWindow extends EventEmitter {
334
339
  }
335
340
  }
336
341
 
342
+ startDrag() {
343
+ if (!this._startNativeDrag()) {
344
+ console.warn('[AtomJS] Native window dragging is not supported by the current platform host.');
345
+ }
346
+ }
347
+
337
348
  blur() {}
338
349
 
339
350
  close() {
@@ -70,6 +70,28 @@ static NSColor *AtomJSColor(NSString *hex) {
70
70
  return [NSColor colorWithSRGBRed:red green:green blue:blue alpha:alpha];
71
71
  }
72
72
 
73
+ static void AtomJSConfigureTransparentWebView(WKWebView *webView) {
74
+ if (!webView) return;
75
+
76
+ // `drawsBackground` is not exposed as a public Objective-C property by every
77
+ // macOS SDK. Use guarded KVC so older SDKs still compile, while retaining the
78
+ // WebKit behavior needed for genuinely transparent pages.
79
+ @try {
80
+ [webView setValue:@NO forKey:@"drawsBackground"];
81
+ } @catch (__unused NSException *exception) {}
82
+
83
+ // Newer WebKit versions expose a public under-page color. Set it through the
84
+ // Objective-C runtime so the host also compiles against SDKs that predate it.
85
+ if ([webView respondsToSelector:NSSelectorFromString(@"setUnderPageBackgroundColor:")]) {
86
+ @try {
87
+ [webView setValue:[NSColor clearColor] forKey:@"underPageBackgroundColor"];
88
+ } @catch (__unused NSException *exception) {}
89
+ }
90
+
91
+ webView.wantsLayer = YES;
92
+ webView.layer.backgroundColor = [NSColor clearColor].CGColor;
93
+ }
94
+
73
95
  static NSImage *AtomJSDefaultApplicationIcon(void) {
74
96
  NSSize size = NSMakeSize(512.0, 512.0);
75
97
  NSImage *image = [[NSImage alloc] initWithSize:size];
@@ -118,6 +140,7 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
118
140
  @property(nonatomic, assign) BOOL modal;
119
141
  - (instancetype)initWithWindowId:(NSNumber *)windowId config:(NSDictionary *)config;
120
142
  - (void)navigate:(NSString *)urlString;
143
+ - (void)beginWindowDrag;
121
144
  @end
122
145
 
123
146
  @implementation AtomJSWindowController
@@ -153,6 +176,8 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
153
176
  defer:NO];
154
177
  _window.releasedWhenClosed = NO;
155
178
  _window.delegate = self;
179
+ _window.movable = YES;
180
+ _window.movableByWindowBackground = NO;
156
181
  _window.title = AtomJSString(config[@"title"], atomAppName);
157
182
  _window.backgroundColor = AtomJSColor(config[@"backgroundColor"]);
158
183
  _window.tabbingMode = NSWindowTabbingModeDisallowed;
@@ -208,7 +233,7 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
208
233
  _webView = [[WKWebView alloc] initWithFrame:frame configuration:webConfiguration];
209
234
  _webView.navigationDelegate = self;
210
235
  _webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
211
- if (AtomJSBoolean(config[@"transparent"], NO)) _webView.drawsBackground = NO;
236
+ if (AtomJSBoolean(config[@"transparent"], NO)) AtomJSConfigureTransparentWebView(_webView);
212
237
  _window.contentView = _webView;
213
238
 
214
239
  NSNumber *parentWindowId = AtomJSNumber(config[@"parentWindowId"], nil);
@@ -248,6 +273,25 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
248
273
  return self;
249
274
  }
250
275
 
276
+ - (void)beginWindowDrag {
277
+ if (!self.window || ([NSEvent pressedMouseButtons] & 1) == 0) return;
278
+
279
+ NSPoint screenLocation = [NSEvent mouseLocation];
280
+ NSPoint windowLocation = [self.window convertPointFromScreen:screenLocation];
281
+ NSEvent *mouseDown = [NSEvent
282
+ mouseEventWithType:NSEventTypeLeftMouseDown
283
+ location:windowLocation
284
+ modifierFlags:0
285
+ timestamp:[[NSProcessInfo processInfo] systemUptime]
286
+ windowNumber:self.window.windowNumber
287
+ context:nil
288
+ eventNumber:0
289
+ clickCount:1
290
+ pressure:1.0];
291
+
292
+ if (mouseDown) [self.window performWindowDragWithEvent:mouseDown];
293
+ }
294
+
251
295
  - (void)navigate:(NSString *)urlString {
252
296
  NSURL *url = [NSURL URLWithString:urlString ?: @"about:blank"];
253
297
  if (!url) {
@@ -539,6 +583,8 @@ static void AtomJSHandleMessage(NSDictionary *message) {
539
583
  [controller.window makeKeyAndOrderFront:nil];
540
584
  [controller.window orderFrontRegardless];
541
585
  [NSApp activateIgnoringOtherApps:YES];
586
+ } else if ([command isEqualToString:@"start-drag"]) {
587
+ [controller beginWindowDrag];
542
588
  } else if ([command isEqualToString:@"close"] || [command isEqualToString:@"destroy"]) {
543
589
  [controller.window close];
544
590
  } else if ([command isEqualToString:@"set-title"]) {