@atom-js-org/runtime 0.4.5-alpha.0 → 0.5.1-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/README.md CHANGED
@@ -1,17 +1,25 @@
1
1
  # @atom-js-org/runtime
2
2
 
3
- The lightweight AtomJS runtime. It uses Node.js for the main process and the operating system WebView for rendering.
3
+ The AtomJS runtime provides an Electron-style main-process API powered by operating-system WebViews.
4
4
 
5
- AtomJS does not install or execute the Electron runtime and does not bundle a private Chromium copy. On Windows, the system WebView is Microsoft Edge WebView2; on macOS it is WKWebView; on Linux it is WebKitGTK through the current native binding.
5
+ ```js
6
+ const { app, BrowserWindow } = require('electron');
7
+
8
+ app.whenReady().then(() => {
9
+ const main = new BrowserWindow({ width: 1000, height: 700 });
10
+ const auth = new BrowserWindow({ parent: main, modal: true, width: 520, height: 720 });
11
+ auth.loadURL('https://example.com/login');
12
+ });
13
+ ```
14
+
15
+ Supported window options include parent/modal relationships, frame and resize controls, always-on-top, opacity, transparency, min/max sizes, taskbar behavior, macOS title-bar styles and traffic-light positioning.
6
16
 
7
- Most applications should import the Electron-compatible alias instead:
17
+ AtomJS does not install or execute Electron and does not bundle a private Chromium copy. Windows uses WebView2, macOS uses WKWebView, and Linux uses WebKitGTK.
18
+
19
+ Most applications should install the Electron-compatible alias:
8
20
 
9
21
  ```bash
10
22
  npm install electron@npm:@atom-js-org/electron@alpha
11
23
  ```
12
24
 
13
- ```js
14
- const { app, BrowserWindow, ipcMain } = require('electron');
15
- ```
16
-
17
25
  Project: https://github.com/Atom-js-org/atom
package/index.d.ts CHANGED
@@ -5,11 +5,30 @@ import { EventEmitter } from 'node:events';
5
5
  export interface BrowserWindowConstructorOptions {
6
6
  width?: number;
7
7
  height?: number;
8
+ x?: number;
9
+ y?: number;
8
10
  title?: string;
9
11
  show?: boolean;
10
12
  resizable?: boolean;
11
13
  center?: boolean;
12
14
  frame?: boolean;
15
+ parent?: BrowserWindow;
16
+ modal?: boolean;
17
+ alwaysOnTop?: boolean;
18
+ focusable?: boolean;
19
+ closable?: boolean;
20
+ minimizable?: boolean;
21
+ maximizable?: boolean;
22
+ fullscreenable?: boolean;
23
+ skipTaskbar?: boolean;
24
+ transparent?: boolean;
25
+ opacity?: number;
26
+ titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
27
+ trafficLightPosition?: { x: number; y: number };
28
+ minWidth?: number;
29
+ minHeight?: number;
30
+ maxWidth?: number;
31
+ maxHeight?: number;
13
32
  backgroundColor?: string;
14
33
  webPreferences?: {
15
34
  preload?: string;
@@ -48,6 +67,8 @@ export class BrowserWindow extends EventEmitter {
48
67
  isDestroyed(): boolean;
49
68
  setTitle(title: string): void;
50
69
  getTitle(): string;
70
+ getParentWindow(): BrowserWindow | null;
71
+ getChildWindows(): BrowserWindow[];
51
72
  setMenu(menu: Menu | null): this;
52
73
  getMenu(): Menu | null;
53
74
  removeMenu(): void;
@@ -55,6 +76,13 @@ export class BrowserWindow extends EventEmitter {
55
76
  isMenuBarVisible(): boolean;
56
77
  setAutoHideMenuBar(hide: boolean): void;
57
78
  isFullScreen(): boolean;
79
+ setAlwaysOnTop(flag?: boolean): void;
80
+ isAlwaysOnTop(): boolean;
81
+ setOpacity(opacity: number): void;
82
+ getOpacity(): number;
83
+ setResizable(flag: boolean): void;
84
+ isResizable(): boolean;
85
+ isModal(): boolean;
58
86
  setFullScreen(flag: boolean): void;
59
87
  maximize(): void;
60
88
  unmaximize(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/runtime",
3
- "version": "0.4.5-alpha.0",
3
+ "version": "0.5.1-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",
@@ -15,6 +15,14 @@ class BrowserWindow extends EventEmitter {
15
15
  constructor(options = {}) {
16
16
  super();
17
17
  this.id = state.nextWindowId++;
18
+ const explicitParent = options.parent instanceof BrowserWindow && !options.parent.isDestroyed()
19
+ ? options.parent
20
+ : null;
21
+ const modalParent = !explicitParent && options.modal === true
22
+ ? BrowserWindow.getFocusedWindow()
23
+ : null;
24
+ this._parent = explicitParent || modalParent || null;
25
+ this._children = new Set();
18
26
  this.options = normalizeOptions(options);
19
27
  this.webContents = new WebContents(this);
20
28
  this._child = null;
@@ -32,12 +40,15 @@ class BrowserWindow extends EventEmitter {
32
40
  this._fullScreen = false;
33
41
  this._maximized = false;
34
42
  this._minimized = false;
43
+ this._alwaysOnTop = this.options.alwaysOnTop;
44
+ this._opacity = this.options.opacity;
35
45
  this._bounds = {
36
- x: 0,
37
- y: 0,
46
+ x: this.options.x == null ? 0 : this.options.x,
47
+ y: this.options.y == null ? 0 : this.options.y,
38
48
  width: this.options.width,
39
49
  height: this.options.height
40
50
  };
51
+ if (this._parent) this._parent._children.add(this);
41
52
  state.windows.set(this.id, this);
42
53
  }
43
54
 
@@ -82,12 +93,33 @@ class BrowserWindow extends EventEmitter {
82
93
  if (preloadPath) preloadCode = await fs.promises.readFile(path.resolve(preloadPath), 'utf8');
83
94
 
84
95
  const config = {
96
+ windowId: this.id,
85
97
  title: this.options.title || app.getName(),
86
98
  width: this.options.width,
87
99
  height: this.options.height,
100
+ x: this.options.x,
101
+ y: this.options.y,
88
102
  resizable: this.options.resizable,
89
103
  center: this.options.center,
90
104
  frame: this.options.frame,
105
+ parentWindowId: this._parent ? this._parent.id : null,
106
+ parentProcessId: this._parent && this._parent._child ? this._parent._child.pid : null,
107
+ modal: this.options.modal,
108
+ alwaysOnTop: this.options.alwaysOnTop,
109
+ focusable: this.options.focusable,
110
+ closable: this.options.closable,
111
+ minimizable: this.options.minimizable,
112
+ maximizable: this.options.maximizable,
113
+ fullscreenable: this.options.fullscreenable,
114
+ skipTaskbar: this.options.skipTaskbar,
115
+ transparent: this.options.transparent,
116
+ opacity: this.options.opacity,
117
+ titleBarStyle: this.options.titleBarStyle,
118
+ trafficLightPosition: this.options.trafficLightPosition,
119
+ minWidth: this.options.minWidth,
120
+ minHeight: this.options.minHeight,
121
+ maxWidth: this.options.maxWidth,
122
+ maxHeight: this.options.maxHeight,
91
123
  show: this.options.show,
92
124
  backgroundColor: this.options.backgroundColor,
93
125
  debug: Boolean(this.options.webPreferences.devTools || process.env.ATOM_DEV === '1'),
@@ -124,7 +156,8 @@ class BrowserWindow extends EventEmitter {
124
156
  this._child = spawn(nodeExecutable, hostArgs, {
125
157
  cwd: state.projectRoot,
126
158
  env: process.env,
127
- stdio: ['ignore', 'pipe', 'inherit']
159
+ stdio: ['ignore', 'pipe', 'inherit'],
160
+ windowsHide: true
128
161
  });
129
162
  this._hostAttached = true;
130
163
  attachHostOutput(this, this._child);
@@ -198,10 +231,12 @@ class BrowserWindow extends EventEmitter {
198
231
  return;
199
232
  }
200
233
  if (event.type === 'focus') {
234
+ state.focusedWindowId = this.id;
201
235
  this.emit('focus');
202
236
  return;
203
237
  }
204
238
  if (event.type === 'blur') {
239
+ if (state.focusedWindowId === this.id) state.focusedWindowId = null;
205
240
  this.emit('blur');
206
241
  return;
207
242
  }
@@ -266,6 +301,10 @@ class BrowserWindow extends EventEmitter {
266
301
  if (this._destroyed) return;
267
302
  this._destroyed = true;
268
303
  this._hostAttached = false;
304
+ if (state.focusedWindowId === this.id) state.focusedWindowId = null;
305
+ if (this._parent) this._parent._children.delete(this);
306
+ for (const child of this._children) child._parent = null;
307
+ this._children.clear();
269
308
  state.windows.delete(this.id);
270
309
  this.emit('closed', details);
271
310
  if (state.windows.size === 0 && !state.isQuitting) app.emit('window-all-closed');
@@ -337,6 +376,14 @@ class BrowserWindow extends EventEmitter {
337
376
  return this.options.title || app.getName();
338
377
  }
339
378
 
379
+ getParentWindow() {
380
+ return this._parent && !this._parent.isDestroyed() ? this._parent : null;
381
+ }
382
+
383
+ getChildWindows() {
384
+ return [...this._children].filter((child) => !child.isDestroyed());
385
+ }
386
+
340
387
  setMenu(menu) {
341
388
  this._menu = menu || null;
342
389
  return this;
@@ -368,6 +415,49 @@ class BrowserWindow extends EventEmitter {
368
415
  return this._fullScreen;
369
416
  }
370
417
 
418
+ setAlwaysOnTop(value = true) {
419
+ this._alwaysOnTop = Boolean(value);
420
+ this.options.alwaysOnTop = this._alwaysOnTop;
421
+ if (!this._sendHostCommand({ command: 'set-always-on-top', value: this._alwaysOnTop })) {
422
+ console.warn('[AtomJS] Changing always-on-top after creation is not supported by the current platform host. Set alwaysOnTop in BrowserWindow options.');
423
+ }
424
+ }
425
+
426
+ isAlwaysOnTop() {
427
+ return this._alwaysOnTop;
428
+ }
429
+
430
+ setOpacity(value) {
431
+ const number = Number(value);
432
+ if (!Number.isFinite(number) || number < 0 || number > 1) {
433
+ throw new TypeError('BrowserWindow.setOpacity expects a number between 0 and 1.');
434
+ }
435
+ this._opacity = number;
436
+ this.options.opacity = number;
437
+ if (!this._sendHostCommand({ command: 'set-opacity', value: number })) {
438
+ console.warn('[AtomJS] Changing opacity after creation is not supported by the current platform host. Set opacity in BrowserWindow options.');
439
+ }
440
+ }
441
+
442
+ getOpacity() {
443
+ return this._opacity;
444
+ }
445
+
446
+ setResizable(value) {
447
+ this.options.resizable = Boolean(value);
448
+ if (!this._sendHostCommand({ command: 'set-resizable', value: this.options.resizable })) {
449
+ console.warn('[AtomJS] Changing resizable after creation is not supported by the current platform host.');
450
+ }
451
+ }
452
+
453
+ isResizable() {
454
+ return this.options.resizable;
455
+ }
456
+
457
+ isModal() {
458
+ return this.options.modal;
459
+ }
460
+
371
461
  setFullScreen(value = true) {
372
462
  this._fullScreen = Boolean(value);
373
463
  if (!this._sendHostCommand({ command: 'fullscreen', value: this._fullScreen })) {
@@ -443,6 +533,10 @@ class BrowserWindow extends EventEmitter {
443
533
  }
444
534
 
445
535
  static getFocusedWindow() {
536
+ if (state.focusedWindowId != null) {
537
+ const focused = BrowserWindow.fromId(state.focusedWindowId);
538
+ if (focused && !focused.isDestroyed()) return focused;
539
+ }
446
540
  const windows = BrowserWindow.getAllWindows();
447
541
  return windows.length ? windows[windows.length - 1] : null;
448
542
  }
@@ -483,9 +577,27 @@ function normalizeOptions(options) {
483
577
  return {
484
578
  width: finiteOr(options.width, 800),
485
579
  height: finiteOr(options.height, 600),
580
+ x: finiteOrNull(options.x),
581
+ y: finiteOrNull(options.y),
486
582
  title: options.title ? String(options.title) : '',
487
583
  show: options.show !== false,
488
584
  resizable: options.resizable !== false,
585
+ modal: options.modal === true,
586
+ alwaysOnTop: options.alwaysOnTop === true,
587
+ focusable: options.focusable !== false,
588
+ closable: options.closable !== false,
589
+ minimizable: options.minimizable !== false,
590
+ maximizable: options.maximizable !== false,
591
+ fullscreenable: options.fullscreenable !== false,
592
+ skipTaskbar: options.skipTaskbar === true,
593
+ transparent: options.transparent === true,
594
+ opacity: clampOpacity(options.opacity),
595
+ titleBarStyle: normalizeTitleBarStyle(options.titleBarStyle),
596
+ trafficLightPosition: normalizePoint(options.trafficLightPosition),
597
+ minWidth: finiteOrZero(options.minWidth),
598
+ minHeight: finiteOrZero(options.minHeight),
599
+ maxWidth: finiteOrZero(options.maxWidth),
600
+ maxHeight: finiteOrZero(options.maxHeight),
489
601
  center: options.center !== false,
490
602
  frame: options.frame !== false,
491
603
  backgroundColor: options.backgroundColor || '#ffffff',
@@ -500,6 +612,38 @@ function normalizeOptions(options) {
500
612
  };
501
613
  }
502
614
 
615
+ function clampOpacity(value) {
616
+ if (value == null) return 1;
617
+ const number = Number(value);
618
+ if (!Number.isFinite(number)) return 1;
619
+ return Math.min(1, Math.max(0, number));
620
+ }
621
+
622
+ function finiteOrZero(value) {
623
+ const number = Number(value);
624
+ return Number.isFinite(number) && number > 0 ? Math.round(number) : 0;
625
+ }
626
+
627
+ function finiteOrNull(value) {
628
+ if (value == null) return null;
629
+ const number = Number(value);
630
+ return Number.isFinite(number) ? Math.round(number) : null;
631
+ }
632
+
633
+ function normalizePoint(value) {
634
+ if (!value || typeof value !== 'object') return null;
635
+ const x = Number(value.x);
636
+ const y = Number(value.y);
637
+ return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
638
+ }
639
+
640
+ function normalizeTitleBarStyle(value) {
641
+ const normalized = String(value || 'default');
642
+ return ['default', 'hidden', 'hiddenInset', 'customButtonsOnHover'].includes(normalized)
643
+ ? normalized
644
+ : 'default';
645
+ }
646
+
503
647
  function finiteOr(value, fallback) {
504
648
  const number = Number(value);
505
649
  return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
@@ -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];
@@ -114,6 +136,8 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
114
136
  @property(nonatomic, strong) NSNumber *windowId;
115
137
  @property(nonatomic, strong) NSWindow *window;
116
138
  @property(nonatomic, strong) WKWebView *webView;
139
+ @property(nonatomic, weak) AtomJSWindowController *parentController;
140
+ @property(nonatomic, assign) BOOL modal;
117
141
  - (instancetype)initWithWindowId:(NSNumber *)windowId config:(NSDictionary *)config;
118
142
  - (void)navigate:(NSString *)urlString;
119
143
  @end
@@ -130,10 +154,20 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
130
154
  CGFloat height = MAX(240.0, [AtomJSNumber(config[@"height"], @600) doubleValue]);
131
155
  NSRect frame = NSMakeRect(0, 0, width, height);
132
156
 
133
- NSWindowStyleMask style = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
157
+ NSWindowStyleMask style = 0;
158
+ if (AtomJSBoolean(config[@"closable"], YES)) style |= NSWindowStyleMaskClosable;
159
+ if (AtomJSBoolean(config[@"minimizable"], YES)) style |= NSWindowStyleMaskMiniaturizable;
134
160
  if (AtomJSBoolean(config[@"frame"], YES)) style |= NSWindowStyleMaskTitled;
135
161
  if (AtomJSBoolean(config[@"resizable"], YES)) style |= NSWindowStyleMaskResizable;
136
162
 
163
+ NSString *titleBarStyle = AtomJSString(config[@"titleBarStyle"], @"default");
164
+ BOOL usesHiddenTitleBar = [titleBarStyle isEqualToString:@"hidden"]
165
+ || [titleBarStyle isEqualToString:@"hiddenInset"]
166
+ || [titleBarStyle isEqualToString:@"customButtonsOnHover"];
167
+ if (usesHiddenTitleBar) {
168
+ style |= NSWindowStyleMaskFullSizeContentView;
169
+ }
170
+
137
171
  _window = [[NSWindow alloc]
138
172
  initWithContentRect:frame
139
173
  styleMask:style
@@ -144,8 +178,41 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
144
178
  _window.title = AtomJSString(config[@"title"], atomAppName);
145
179
  _window.backgroundColor = AtomJSColor(config[@"backgroundColor"]);
146
180
  _window.tabbingMode = NSWindowTabbingModeDisallowed;
181
+ _window.alphaValue = MIN(1.0, MAX(0.0, [AtomJSNumber(config[@"opacity"], @1) doubleValue]));
182
+ _window.opaque = !AtomJSBoolean(config[@"transparent"], NO);
183
+ if (!_window.opaque) {
184
+ _window.backgroundColor = [NSColor clearColor];
185
+ }
186
+
187
+ if (usesHiddenTitleBar) {
188
+ _window.titleVisibility = NSWindowTitleHidden;
189
+ _window.titlebarAppearsTransparent = YES;
190
+ }
147
191
 
148
- if (AtomJSBoolean(config[@"center"], YES)) [_window center];
192
+ if (!AtomJSBoolean(config[@"fullscreenable"], YES)) {
193
+ _window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenNone;
194
+ }
195
+ [_window standardWindowButton:NSWindowZoomButton].enabled = AtomJSBoolean(config[@"maximizable"], YES);
196
+ if (AtomJSBoolean(config[@"alwaysOnTop"], NO)) {
197
+ _window.level = NSFloatingWindowLevel;
198
+ }
199
+
200
+ CGFloat minWidth = [AtomJSNumber(config[@"minWidth"], @0) doubleValue];
201
+ CGFloat minHeight = [AtomJSNumber(config[@"minHeight"], @0) doubleValue];
202
+ CGFloat maxWidth = [AtomJSNumber(config[@"maxWidth"], @0) doubleValue];
203
+ CGFloat maxHeight = [AtomJSNumber(config[@"maxHeight"], @0) doubleValue];
204
+ if (minWidth > 0 || minHeight > 0) {
205
+ _window.contentMinSize = NSMakeSize(MAX(1.0, minWidth), MAX(1.0, minHeight));
206
+ }
207
+ if (maxWidth > 0 || maxHeight > 0) {
208
+ _window.contentMaxSize = NSMakeSize(maxWidth > 0 ? maxWidth : CGFLOAT_MAX, maxHeight > 0 ? maxHeight : CGFLOAT_MAX);
209
+ }
210
+
211
+ if ([config[@"x"] isKindOfClass:[NSNumber class]] && [config[@"y"] isKindOfClass:[NSNumber class]]) {
212
+ [_window setFrameOrigin:NSMakePoint([config[@"x"] doubleValue], [config[@"y"] doubleValue])];
213
+ } else if (AtomJSBoolean(config[@"center"], YES)) {
214
+ [_window center];
215
+ }
149
216
 
150
217
  WKUserContentController *contentController = [[WKUserContentController alloc] init];
151
218
  NSString *bridgeScript = AtomJSString(config[@"bridgeScript"], @"");
@@ -163,14 +230,40 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
163
230
  _webView = [[WKWebView alloc] initWithFrame:frame configuration:webConfiguration];
164
231
  _webView.navigationDelegate = self;
165
232
  _webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
233
+ if (AtomJSBoolean(config[@"transparent"], NO)) AtomJSConfigureTransparentWebView(_webView);
166
234
  _window.contentView = _webView;
167
235
 
236
+ NSNumber *parentWindowId = AtomJSNumber(config[@"parentWindowId"], nil);
237
+ _parentController = parentWindowId ? atomWindows[parentWindowId] : nil;
238
+ _modal = AtomJSBoolean(config[@"modal"], NO) && _parentController != nil;
239
+
240
+ NSDictionary *trafficLightPosition = [config[@"trafficLightPosition"] isKindOfClass:[NSDictionary class]]
241
+ ? config[@"trafficLightPosition"]
242
+ : nil;
243
+ if (trafficLightPosition) {
244
+ CGFloat x = [AtomJSNumber(trafficLightPosition[@"x"], @0) doubleValue];
245
+ CGFloat y = [AtomJSNumber(trafficLightPosition[@"y"], @0) doubleValue];
246
+ NSButton *closeButton = [_window standardWindowButton:NSWindowCloseButton];
247
+ NSView *container = closeButton.superview;
248
+ if (container) {
249
+ NSPoint origin = container.frame.origin;
250
+ origin.x = x;
251
+ origin.y = MAX(0.0, _window.frame.size.height - container.frame.size.height - y);
252
+ [container setFrameOrigin:origin];
253
+ }
254
+ }
255
+
168
256
  [self navigate:AtomJSString(config[@"url"], @"about:blank")];
169
257
 
170
258
  if (AtomJSBoolean(config[@"show"], YES)) {
171
259
  [NSApp unhide:nil];
172
- [_window makeKeyAndOrderFront:nil];
173
- [_window orderFrontRegardless];
260
+ if (_modal) {
261
+ [_parentController.window beginSheet:_window completionHandler:nil];
262
+ } else {
263
+ if (_parentController) [_parentController.window addChildWindow:_window ordered:NSWindowAbove];
264
+ [_window makeKeyAndOrderFront:nil];
265
+ [_window orderFrontRegardless];
266
+ }
174
267
  [NSApp activateIgnoringOtherApps:YES];
175
268
  }
176
269
 
@@ -226,6 +319,13 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
226
319
  }
227
320
 
228
321
  - (void)windowWillClose:(NSNotification *)notification {
322
+ if (self.parentController) {
323
+ if (self.modal && self.window.sheetParent) {
324
+ [self.window.sheetParent endSheet:self.window];
325
+ } else {
326
+ [self.parentController.window removeChildWindow:self.window];
327
+ }
328
+ }
229
329
  [atomWindows removeObjectForKey:self.windowId];
230
330
  AtomJSEmit(@{
231
331
  @"type": @"closed",
@@ -486,6 +586,14 @@ static void AtomJSHandleMessage(NSDictionary *message) {
486
586
  if ([bounds[@"width"] isKindOfClass:[NSNumber class]]) frame.size.width = MAX(1.0, [bounds[@"width"] doubleValue]);
487
587
  if ([bounds[@"height"] isKindOfClass:[NSNumber class]]) frame.size.height = MAX(1.0, [bounds[@"height"] doubleValue]);
488
588
  [controller.window setFrame:frame display:YES animate:NO];
589
+ } else if ([command isEqualToString:@"set-always-on-top"]) {
590
+ controller.window.level = AtomJSBoolean(message[@"value"], NO) ? NSFloatingWindowLevel : NSNormalWindowLevel;
591
+ } else if ([command isEqualToString:@"set-opacity"]) {
592
+ controller.window.alphaValue = MIN(1.0, MAX(0.0, [AtomJSNumber(message[@"value"], @1) doubleValue]));
593
+ } else if ([command isEqualToString:@"set-resizable"]) {
594
+ BOOL requested = AtomJSBoolean(message[@"value"], YES);
595
+ if (requested) controller.window.styleMask |= NSWindowStyleMaskResizable;
596
+ else controller.window.styleMask &= ~NSWindowStyleMaskResizable;
489
597
  } else {
490
598
  AtomJSRespond(requestId, NO, nil, [NSString stringWithFormat:@"Unsupported AtomJS native command: %@", command]);
491
599
  return;
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
2
3
 
4
+ const EVENT_PREFIX = '__ATOMJS_EVENT__';
3
5
  const configPath = process.argv[2];
4
6
  if (!configPath) {
5
7
  console.error('AtomJS window host expected a configuration file');
@@ -33,18 +35,387 @@ try {
33
35
  process.exit(1);
34
36
  }
35
37
 
36
- try {
37
- const view = new Webview(Boolean(config.debug));
38
- view.title(String(config.title || 'AtomJS App'));
39
- view.size(
40
- Number(config.width || 800),
41
- Number(config.height || 600),
42
- config.resizable === false ? SizeHint.Fixed : SizeHint.None
43
- );
44
- view.init(String(config.bridgeScript || ''));
45
- view.navigate(String(config.url));
46
- view.show();
47
- } catch (error) {
48
- console.error('AtomJS native window failed:', error && error.stack ? error.stack : error);
49
- process.exit(1);
38
+ function emit(event) {
39
+ process.stdout.write(`${EVENT_PREFIX}${JSON.stringify(event)}\n`);
40
+ }
41
+
42
+ function configureWindowsWindow(options) {
43
+ const parentProcessId = Number(options.parentProcessId || 0);
44
+ const modal = Boolean(options.modal && parentProcessId > 0);
45
+ const env = {
46
+ ...process.env,
47
+ ATOMJS_CHILD_PID: String(process.pid),
48
+ ATOMJS_PARENT_PID: String(parentProcessId || 0),
49
+ ATOMJS_MODAL: modal ? '1' : '0',
50
+ ATOMJS_FRAME: options.frame === false ? '0' : '1',
51
+ ATOMJS_RESIZABLE: options.resizable === false ? '0' : '1',
52
+ ATOMJS_MINIMIZABLE: options.minimizable === false ? '0' : '1',
53
+ ATOMJS_MAXIMIZABLE: options.maximizable === false ? '0' : '1',
54
+ ATOMJS_CLOSABLE: options.closable === false ? '0' : '1',
55
+ ATOMJS_ALWAYS_ON_TOP: options.alwaysOnTop ? '1' : '0',
56
+ ATOMJS_FOCUSABLE: options.focusable === false ? '0' : '1',
57
+ ATOMJS_SKIP_TASKBAR: options.skipTaskbar ? '1' : '0',
58
+ ATOMJS_TRANSPARENT: options.transparent ? '1' : '0',
59
+ ATOMJS_OPACITY: String(normalizeOpacity(options.opacity)),
60
+ ATOMJS_CENTER: options.center === false ? '0' : '1',
61
+ ATOMJS_SHOW: options.show === false ? '0' : '1',
62
+ ATOMJS_X: Number.isFinite(Number(options.x)) ? String(Math.round(Number(options.x))) : '',
63
+ ATOMJS_Y: Number.isFinite(Number(options.y)) ? String(Math.round(Number(options.y))) : ''
64
+ };
65
+
66
+ const result = spawnSync('powershell.exe', [
67
+ '-NoLogo',
68
+ '-NoProfile',
69
+ '-NonInteractive',
70
+ '-ExecutionPolicy', 'Bypass',
71
+ '-Command', WINDOWS_WINDOW_SETUP
72
+ ], {
73
+ env,
74
+ encoding: 'utf8',
75
+ windowsHide: true,
76
+ stdio: ['ignore', 'pipe', 'pipe']
77
+ });
78
+
79
+ if (result.error) {
80
+ console.warn(`[AtomJS] Could not configure the native Windows window: ${result.error.message}`);
81
+ } else if (result.status !== 0) {
82
+ const detail = String(result.stderr || result.stdout || '').trim();
83
+ if (detail) console.warn(`[AtomJS] Could not configure every native Windows option: ${detail}`);
84
+ }
85
+
86
+ return modal ? { parentProcessId } : null;
87
+ }
88
+
89
+ function releaseWindowsModal(ownership) {
90
+ if (!ownership || !ownership.parentProcessId) return;
91
+ const result = spawnSync('powershell.exe', [
92
+ '-NoLogo',
93
+ '-NoProfile',
94
+ '-NonInteractive',
95
+ '-ExecutionPolicy', 'Bypass',
96
+ '-Command', WINDOWS_MODAL_RELEASE
97
+ ], {
98
+ env: {
99
+ ...process.env,
100
+ ATOMJS_PARENT_PID: String(ownership.parentProcessId)
101
+ },
102
+ encoding: 'utf8',
103
+ windowsHide: true,
104
+ stdio: ['ignore', 'pipe', 'pipe']
105
+ });
106
+ if (result.error) {
107
+ console.warn(`[AtomJS] Could not restore the parent Windows window: ${result.error.message}`);
108
+ }
50
109
  }
110
+
111
+ function normalizeOpacity(value) {
112
+ const number = Number(value);
113
+ if (!Number.isFinite(number)) return 1;
114
+ return Math.min(1, Math.max(0, number));
115
+ }
116
+
117
+ const WINDOWS_INTEROP = String.raw`
118
+ Add-Type -TypeDefinition @'
119
+ using System;
120
+ using System.Runtime.InteropServices;
121
+
122
+ public static class AtomJSNativeWindow {
123
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
124
+
125
+ [DllImport("user32.dll")]
126
+ public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
127
+
128
+ [DllImport("user32.dll")]
129
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
130
+
131
+ [DllImport("user32.dll")]
132
+ public static extern IntPtr GetParent(IntPtr hWnd);
133
+
134
+ [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
135
+ private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int index);
136
+
137
+ [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
138
+ private static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int index);
139
+
140
+ [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
141
+ private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int index, IntPtr value);
142
+
143
+ [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
144
+ private static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int index, IntPtr value);
145
+
146
+ [DllImport("user32.dll")]
147
+ public static extern bool SetWindowPos(IntPtr hWnd, IntPtr insertAfter, int x, int y, int cx, int cy, uint flags);
148
+
149
+ [DllImport("user32.dll")]
150
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
151
+
152
+ [DllImport("user32.dll")]
153
+ public static extern IntPtr GetForegroundWindow();
154
+
155
+ [DllImport("user32.dll")]
156
+ public static extern bool AttachThreadInput(uint attachThreadId, uint attachToThreadId, bool attach);
157
+
158
+ [DllImport("kernel32.dll")]
159
+ public static extern uint GetCurrentThreadId();
160
+
161
+ [DllImport("user32.dll")]
162
+ public static extern IntPtr SetActiveWindow(IntPtr hWnd);
163
+
164
+ [DllImport("user32.dll")]
165
+ public static extern IntPtr SetFocus(IntPtr hWnd);
166
+
167
+ [DllImport("user32.dll")]
168
+ public static extern bool BringWindowToTop(IntPtr hWnd);
169
+
170
+ [DllImport("user32.dll")]
171
+ public static extern bool ShowWindow(IntPtr hWnd, int command);
172
+
173
+ [DllImport("user32.dll")]
174
+ public static extern bool EnableWindow(IntPtr hWnd, bool enabled);
175
+
176
+ [DllImport("user32.dll")]
177
+ public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
178
+
179
+ [DllImport("user32.dll")]
180
+ public static extern int GetSystemMetrics(int index);
181
+
182
+ [DllImport("user32.dll")]
183
+ public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool revert);
184
+
185
+ [DllImport("user32.dll")]
186
+ public static extern bool DeleteMenu(IntPtr menu, uint position, uint flags);
187
+
188
+ [DllImport("user32.dll")]
189
+ public static extern bool DrawMenuBar(IntPtr hWnd);
190
+
191
+ [DllImport("user32.dll")]
192
+ public static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint colorKey, byte alpha, uint flags);
193
+
194
+ [StructLayout(LayoutKind.Sequential)]
195
+ public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
196
+
197
+ public static IntPtr GetWindowLongPtr(IntPtr hWnd, int index) {
198
+ return IntPtr.Size == 8 ? GetWindowLongPtr64(hWnd, index) : GetWindowLongPtr32(hWnd, index);
199
+ }
200
+
201
+ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int index, IntPtr value) {
202
+ return IntPtr.Size == 8 ? SetWindowLongPtr64(hWnd, index, value) : SetWindowLongPtr32(hWnd, index, value);
203
+ }
204
+
205
+ public static IntPtr FindWindowForProcess(uint expectedProcessId) {
206
+ IntPtr result = IntPtr.Zero;
207
+ EnumWindows((window, state) => {
208
+ uint processId;
209
+ GetWindowThreadProcessId(window, out processId);
210
+ if (processId == expectedProcessId && GetParent(window) == IntPtr.Zero) {
211
+ result = window;
212
+ return false;
213
+ }
214
+ return true;
215
+ }, IntPtr.Zero);
216
+ return result;
217
+ }
218
+ }
219
+ '@
220
+
221
+ function Find-AtomWindow([uint32] $ProcessId) {
222
+ for ($attempt = 0; $attempt -lt 50; $attempt += 1) {
223
+ $window = [AtomJSNativeWindow]::FindWindowForProcess($ProcessId)
224
+ if ($window -ne [IntPtr]::Zero) { return $window }
225
+ Start-Sleep -Milliseconds 40
226
+ }
227
+ return [IntPtr]::Zero
228
+ }
229
+ `;
230
+
231
+ const WINDOWS_WINDOW_SETUP = WINDOWS_INTEROP + String.raw`
232
+ $child = Find-AtomWindow ([uint32]$env:ATOMJS_CHILD_PID)
233
+ if ($child -eq [IntPtr]::Zero) { throw 'The AtomJS WebView window handle was not found.' }
234
+
235
+ $GWL_STYLE = -16
236
+ $GWL_EXSTYLE = -20
237
+ $GWLP_HWNDPARENT = -8
238
+ $WS_CAPTION = 0x00C00000L
239
+ $WS_THICKFRAME = 0x00040000L
240
+ $WS_MINIMIZEBOX = 0x00020000L
241
+ $WS_MAXIMIZEBOX = 0x00010000L
242
+ $WS_EX_TOOLWINDOW = 0x00000080L
243
+ $WS_EX_APPWINDOW = 0x00040000L
244
+ $WS_EX_LAYERED = 0x00080000L
245
+ $WS_EX_NOACTIVATE = 0x08000000L
246
+ $SWP_NOSIZE = 0x0001
247
+ $SWP_NOMOVE = 0x0002
248
+ $SWP_NOACTIVATE = 0x0010
249
+ $SWP_FRAMECHANGED = 0x0020
250
+ $SWP_SHOWWINDOW = 0x0040
251
+ $SC_CLOSE = 0xF060
252
+ $MF_BYCOMMAND = 0x0000
253
+ $LWA_ALPHA = 0x00000002
254
+
255
+ $style = [AtomJSNativeWindow]::GetWindowLongPtr($child, $GWL_STYLE).ToInt64()
256
+ if ($env:ATOMJS_FRAME -eq '0') { $style = $style -band (-bnot ($WS_CAPTION -bor $WS_THICKFRAME)) }
257
+ if ($env:ATOMJS_RESIZABLE -eq '0') { $style = $style -band (-bnot $WS_THICKFRAME) }
258
+ if ($env:ATOMJS_MINIMIZABLE -eq '0') { $style = $style -band (-bnot $WS_MINIMIZEBOX) }
259
+ if ($env:ATOMJS_MAXIMIZABLE -eq '0') { $style = $style -band (-bnot $WS_MAXIMIZEBOX) }
260
+ [void][AtomJSNativeWindow]::SetWindowLongPtr($child, $GWL_STYLE, [IntPtr]$style)
261
+
262
+ $extendedStyle = [AtomJSNativeWindow]::GetWindowLongPtr($child, $GWL_EXSTYLE).ToInt64()
263
+ if ($env:ATOMJS_SKIP_TASKBAR -eq '1') {
264
+ $extendedStyle = ($extendedStyle -bor $WS_EX_TOOLWINDOW) -band (-bnot $WS_EX_APPWINDOW)
265
+ }
266
+ if ($env:ATOMJS_FOCUSABLE -eq '0') {
267
+ $extendedStyle = $extendedStyle -bor $WS_EX_NOACTIVATE
268
+ } else {
269
+ $extendedStyle = $extendedStyle -band (-bnot $WS_EX_NOACTIVATE)
270
+ }
271
+ $opacity = [Math]::Max(0.0, [Math]::Min(1.0, [double]$env:ATOMJS_OPACITY))
272
+ if ($env:ATOMJS_TRANSPARENT -eq '1' -or $opacity -lt 1.0) {
273
+ $extendedStyle = $extendedStyle -bor $WS_EX_LAYERED
274
+ }
275
+ [void][AtomJSNativeWindow]::SetWindowLongPtr($child, $GWL_EXSTYLE, [IntPtr]$extendedStyle)
276
+
277
+ if ($env:ATOMJS_CLOSABLE -eq '0') {
278
+ $menu = [AtomJSNativeWindow]::GetSystemMenu($child, $false)
279
+ if ($menu -ne [IntPtr]::Zero) {
280
+ [void][AtomJSNativeWindow]::DeleteMenu($menu, $SC_CLOSE, $MF_BYCOMMAND)
281
+ [void][AtomJSNativeWindow]::DrawMenuBar($child)
282
+ }
283
+ }
284
+
285
+ $alpha = [byte][Math]::Round($opacity * 255.0)
286
+ if ($env:ATOMJS_TRANSPARENT -eq '1' -or $opacity -lt 1.0) {
287
+ [void][AtomJSNativeWindow]::SetLayeredWindowAttributes($child, 0, $alpha, $LWA_ALPHA)
288
+ }
289
+
290
+ $parent = [IntPtr]::Zero
291
+ if ([uint32]$env:ATOMJS_PARENT_PID -gt 0) {
292
+ $parent = Find-AtomWindow ([uint32]$env:ATOMJS_PARENT_PID)
293
+ if ($parent -ne [IntPtr]::Zero) {
294
+ [void][AtomJSNativeWindow]::SetWindowLongPtr($child, $GWLP_HWNDPARENT, $parent)
295
+ if ($env:ATOMJS_MODAL -eq '1') { [void][AtomJSNativeWindow]::EnableWindow($parent, $false) }
296
+ }
297
+ }
298
+
299
+ $rect = New-Object AtomJSNativeWindow+RECT
300
+ [void][AtomJSNativeWindow]::GetWindowRect($child, [ref]$rect)
301
+ $width = $rect.Right - $rect.Left
302
+ $height = $rect.Bottom - $rect.Top
303
+ $x = $rect.Left
304
+ $y = $rect.Top
305
+ if ($env:ATOMJS_CENTER -eq '1' -and $parent -ne [IntPtr]::Zero) {
306
+ $parentRect = New-Object AtomJSNativeWindow+RECT
307
+ [void][AtomJSNativeWindow]::GetWindowRect($parent, [ref]$parentRect)
308
+ $x = $parentRect.Left + [Math]::Max(0, (($parentRect.Right - $parentRect.Left) - $width) / 2)
309
+ $y = $parentRect.Top + [Math]::Max(0, (($parentRect.Bottom - $parentRect.Top) - $height) / 2)
310
+ } elseif ($env:ATOMJS_CENTER -eq '1') {
311
+ $x = [Math]::Max(0, ([AtomJSNativeWindow]::GetSystemMetrics(0) - $width) / 2)
312
+ $y = [Math]::Max(0, ([AtomJSNativeWindow]::GetSystemMetrics(1) - $height) / 2)
313
+ }
314
+ if ($env:ATOMJS_X -ne '') { $x = [int]$env:ATOMJS_X }
315
+ if ($env:ATOMJS_Y -ne '') { $y = [int]$env:ATOMJS_Y }
316
+
317
+ $insertAfter = if ($env:ATOMJS_ALWAYS_ON_TOP -eq '1') { [IntPtr](-1) } else { [IntPtr]::Zero }
318
+ $positionFlags = $SWP_FRAMECHANGED
319
+ if ($env:ATOMJS_SHOW -ne '0') { $positionFlags = $positionFlags -bor $SWP_SHOWWINDOW }
320
+ [void][AtomJSNativeWindow]::SetWindowPos($child, $insertAfter, [int]$x, [int]$y, $width, $height, $positionFlags)
321
+ if ($env:ATOMJS_SHOW -eq '0') {
322
+ [void][AtomJSNativeWindow]::ShowWindow($child, 0)
323
+ } else {
324
+ [void][AtomJSNativeWindow]::ShowWindow($child, 5)
325
+ [void][AtomJSNativeWindow]::BringWindowToTop($child)
326
+ }
327
+ if ($env:ATOMJS_SHOW -ne '0' -and $env:ATOMJS_FOCUSABLE -ne '0') {
328
+ # WebView windows live in helper processes. Temporarily attach the setup
329
+ # thread to the foreground and WebView UI threads so Windows accepts the
330
+ # activation request instead of leaving OAuth/login windows behind the app.
331
+ $foreground = [AtomJSNativeWindow]::GetForegroundWindow()
332
+ [uint32]$foregroundProcess = 0
333
+ [uint32]$childProcess = 0
334
+ $foregroundThread = if ($foreground -ne [IntPtr]::Zero) {
335
+ [AtomJSNativeWindow]::GetWindowThreadProcessId($foreground, [ref]$foregroundProcess)
336
+ } else { 0 }
337
+ $childThread = [AtomJSNativeWindow]::GetWindowThreadProcessId($child, [ref]$childProcess)
338
+ $currentThread = [AtomJSNativeWindow]::GetCurrentThreadId()
339
+ $attachedForeground = $false
340
+ $attachedChild = $false
341
+
342
+ try {
343
+ if ($foregroundThread -ne 0 -and $foregroundThread -ne $currentThread) {
344
+ $attachedForeground = [AtomJSNativeWindow]::AttachThreadInput($currentThread, $foregroundThread, $true)
345
+ }
346
+ if ($childThread -ne 0 -and $childThread -ne $currentThread) {
347
+ $attachedChild = [AtomJSNativeWindow]::AttachThreadInput($currentThread, $childThread, $true)
348
+ }
349
+ [void][AtomJSNativeWindow]::BringWindowToTop($child)
350
+ [void][AtomJSNativeWindow]::SetActiveWindow($child)
351
+ [void][AtomJSNativeWindow]::SetForegroundWindow($child)
352
+ [void][AtomJSNativeWindow]::SetFocus($child)
353
+ } finally {
354
+ if ($attachedChild) {
355
+ [void][AtomJSNativeWindow]::AttachThreadInput($currentThread, $childThread, $false)
356
+ }
357
+ if ($attachedForeground) {
358
+ [void][AtomJSNativeWindow]::AttachThreadInput($currentThread, $foregroundThread, $false)
359
+ }
360
+ }
361
+ }
362
+ `;
363
+
364
+ const WINDOWS_MODAL_RELEASE = WINDOWS_INTEROP + String.raw`
365
+ $parent = Find-AtomWindow ([uint32]$env:ATOMJS_PARENT_PID)
366
+ if ($parent -ne [IntPtr]::Zero) {
367
+ [void][AtomJSNativeWindow]::EnableWindow($parent, $true)
368
+ [void][AtomJSNativeWindow]::ShowWindow($parent, 5)
369
+ [void][AtomJSNativeWindow]::BringWindowToTop($parent)
370
+ [void][AtomJSNativeWindow]::SetForegroundWindow($parent)
371
+ }
372
+ `;
373
+
374
+ async function main() {
375
+ let view = null;
376
+ let windowsOwnership = null;
377
+ try {
378
+ view = new Webview(Boolean(config.debug));
379
+ view.title(String(config.title || 'AtomJS App'));
380
+
381
+ const width = Number(config.width || 800);
382
+ const height = Number(config.height || 600);
383
+ view.size(
384
+ width,
385
+ height,
386
+ config.resizable === false ? SizeHint.Fixed : SizeHint.None
387
+ );
388
+
389
+ if (Number(config.minWidth) > 0 || Number(config.minHeight) > 0) {
390
+ view.size(
391
+ Number(config.minWidth) > 0 ? Number(config.minWidth) : width,
392
+ Number(config.minHeight) > 0 ? Number(config.minHeight) : height,
393
+ SizeHint.Min
394
+ );
395
+ }
396
+ if (Number(config.maxWidth) > 0 || Number(config.maxHeight) > 0) {
397
+ view.size(
398
+ Number(config.maxWidth) > 0 ? Number(config.maxWidth) : width,
399
+ Number(config.maxHeight) > 0 ? Number(config.maxHeight) : height,
400
+ SizeHint.Max
401
+ );
402
+ }
403
+
404
+ view.init(String(config.bridgeScript || ''));
405
+ view.navigate(String(config.url));
406
+
407
+ if (process.platform === 'win32') {
408
+ windowsOwnership = configureWindowsWindow(config);
409
+ }
410
+
411
+ emit({ type: 'created', windowId: Number(config.windowId || 0), pid: process.pid });
412
+ view.show();
413
+ } catch (error) {
414
+ console.error('AtomJS native window failed:', error && error.stack ? error.stack : error);
415
+ process.exitCode = 1;
416
+ } finally {
417
+ if (process.platform === 'win32') releaseWindowsModal(windowsOwnership);
418
+ }
419
+ }
420
+
421
+ await main();
package/src/state.cjs CHANGED
@@ -6,6 +6,7 @@ module.exports = {
6
6
  windows,
7
7
  bridgeServer: null,
8
8
  nextWindowId: 1,
9
+ focusedWindowId: null,
9
10
  projectRoot: process.env.ATOM_PROJECT_ROOT || process.cwd(),
10
11
  isQuitting: false
11
12
  };