@atom-js-org/runtime 0.5.2-alpha.0 → 0.5.3-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/runtime",
3
- "version": "0.5.2-alpha.0",
3
+ "version": "0.5.3-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",
@@ -57,7 +57,8 @@ function generateBridgeScript({ websocketUrl, preloadCode = '' }) {
57
57
  const inline = normalizeAppRegion(
58
58
  element.style && (
59
59
  element.style.getPropertyValue('-webkit-app-region') ||
60
- element.style.getPropertyValue('app-region')
60
+ element.style.getPropertyValue('app-region') ||
61
+ element.style.getPropertyValue('--atom-app-region')
61
62
  )
62
63
  );
63
64
  if (inline) return inline;
@@ -66,40 +67,223 @@ function generateBridgeScript({ websocketUrl, preloadCode = '' }) {
66
67
  const computed = getComputedStyle(element);
67
68
  return normalizeAppRegion(
68
69
  computed.getPropertyValue('-webkit-app-region') ||
69
- computed.getPropertyValue('app-region')
70
+ computed.getPropertyValue('app-region') ||
71
+ computed.getPropertyValue('--atom-app-region')
70
72
  );
71
73
  } catch {
72
74
  return '';
73
75
  }
74
76
  }
75
77
 
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
- ));
78
+ function collectStylesheetAppRegions() {
79
+ const regions = new Map();
80
+
81
+ function visitRules(rules) {
82
+ for (const rule of rules || []) {
83
+ if (rule && rule.cssRules) {
84
+ visitRules(rule.cssRules);
85
+ continue;
86
+ }
87
+ if (!rule || !rule.selectorText || !rule.style) continue;
88
+
89
+ const match = String(rule.cssText || '').match(
90
+ /(?:-webkit-app-region|app-region)\s*:\s*(drag|no-drag)/i
91
+ );
92
+ const region = normalizeAppRegion(
93
+ rule.style.getPropertyValue('-webkit-app-region') ||
94
+ rule.style.getPropertyValue('app-region') ||
95
+ rule.style.getPropertyValue('--atom-app-region') ||
96
+ (match && match[1])
97
+ );
98
+ if (!region) continue;
99
+
100
+ try {
101
+ for (const element of document.querySelectorAll(rule.selectorText)) {
102
+ regions.set(element, region);
103
+ }
104
+ } catch {}
105
+ }
106
+ }
107
+
108
+ for (const sheet of document.styleSheets || []) {
109
+ try {
110
+ visitRules(sheet.cssRules);
111
+ } catch {}
112
+ }
113
+
114
+ return regions;
80
115
  }
81
116
 
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;
117
+ function composedParent(element) {
118
+ if (!(element instanceof Element)) return null;
119
+ if (element.assignedSlot) return element.assignedSlot;
120
+ if (element.parentElement) return element.parentElement;
121
+ const root = element.getRootNode && element.getRootNode();
122
+ return root && root.host instanceof Element ? root.host : null;
123
+ }
124
+
125
+ function isInteractiveElement(element) {
126
+ return element instanceof Element && element.matches(
127
+ 'button, input, textarea, select, option, a[href], [contenteditable=""], [contenteditable="true"], [role="button"]'
128
+ );
129
+ }
86
130
 
87
- for (const entry of path) {
88
- if (!(entry instanceof Element)) continue;
89
- const region = appRegionForElement(entry);
131
+ function hasDraggableAncestor(element) {
132
+ let current = composedParent(element);
133
+ while (current) {
134
+ const region = appRegionForElement(current);
90
135
  if (region === 'no-drag') return false;
91
- if (region === 'drag') draggable = true;
136
+ if (region === 'drag') return true;
137
+ current = composedParent(current);
138
+ }
139
+ return false;
140
+ }
141
+
142
+ const observedDragRoots = new WeakSet();
143
+ let dragRegionObserver = null;
144
+ let dragRegionFrame = 0;
145
+ let lastDragRegionPayload = '';
146
+
147
+ function observeDragRoot(root) {
148
+ if (!dragRegionObserver || !root || observedDragRoots.has(root)) return;
149
+ observedDragRoots.add(root);
150
+ dragRegionObserver.observe(root, {
151
+ attributes: true,
152
+ childList: true,
153
+ subtree: true,
154
+ attributeFilter: [
155
+ 'class',
156
+ 'style',
157
+ 'hidden',
158
+ 'data-atom-drag-region',
159
+ 'data-atom-no-drag',
160
+ 'data-atom-app-region',
161
+ 'data-app-region'
162
+ ]
163
+ });
164
+ }
165
+
166
+ function walkRegionElements(root, visitor) {
167
+ const stack = [];
168
+ if (root && root.documentElement instanceof Element) {
169
+ stack.push(root.documentElement);
170
+ } else if (root && root.children) {
171
+ for (let index = root.children.length - 1; index >= 0; index -= 1) {
172
+ stack.push(root.children[index]);
173
+ }
174
+ }
175
+
176
+ while (stack.length > 0) {
177
+ const element = stack.pop();
178
+ if (!(element instanceof Element)) continue;
179
+ visitor(element);
180
+
181
+ if (element.shadowRoot) {
182
+ observeDragRoot(element.shadowRoot);
183
+ for (let index = element.shadowRoot.children.length - 1; index >= 0; index -= 1) {
184
+ stack.push(element.shadowRoot.children[index]);
185
+ }
186
+ }
187
+
188
+ for (let index = element.children.length - 1; index >= 0; index -= 1) {
189
+ stack.push(element.children[index]);
190
+ }
92
191
  }
192
+ }
193
+
194
+ function roundedRegionNumber(value) {
195
+ return Math.round(Number(value) * 4) / 4;
196
+ }
197
+
198
+ function collectWindowDragRegions() {
199
+ const regions = [];
200
+ const stylesheetRegions = collectStylesheetAppRegions();
201
+ const viewportWidth = Math.max(0, Number(window.innerWidth) || 0);
202
+ const viewportHeight = Math.max(0, Number(window.innerHeight) || 0);
203
+
204
+ walkRegionElements(document, (element) => {
205
+ let region = appRegionForElement(element) || stylesheetRegions.get(element) || '';
206
+ if (!region && isInteractiveElement(element) && hasDraggableAncestor(element)) {
207
+ region = 'no-drag';
208
+ }
209
+ if (!region) return;
210
+
211
+ let style;
212
+ try {
213
+ style = getComputedStyle(element);
214
+ } catch {
215
+ return;
216
+ }
217
+ if (style.display === 'none' || style.visibility === 'hidden') return;
218
+
219
+ const rect = element.getBoundingClientRect();
220
+ const left = Math.max(0, Math.min(viewportWidth, rect.left));
221
+ const top = Math.max(0, Math.min(viewportHeight, rect.top));
222
+ const right = Math.max(0, Math.min(viewportWidth, rect.right));
223
+ const bottom = Math.max(0, Math.min(viewportHeight, rect.bottom));
224
+ const width = right - left;
225
+ const height = bottom - top;
226
+ if (width <= 0 || height <= 0) return;
227
+
228
+ regions.push({
229
+ x: roundedRegionNumber(left),
230
+ y: roundedRegionNumber(top),
231
+ width: roundedRegionNumber(width),
232
+ height: roundedRegionNumber(height),
233
+ draggable: region === 'drag'
234
+ });
235
+ });
93
236
 
94
- if (!draggable || isInteractiveDragTarget(event.target)) return false;
95
- return true;
237
+ return {
238
+ type: 'system',
239
+ command: 'set-window-drag-regions',
240
+ viewport: {
241
+ width: roundedRegionNumber(viewportWidth),
242
+ height: roundedRegionNumber(viewportHeight)
243
+ },
244
+ deviceScaleFactor: Number(window.devicePixelRatio) || 1,
245
+ regions
246
+ };
247
+ }
248
+
249
+ function publishWindowDragRegions() {
250
+ dragRegionFrame = 0;
251
+ const payload = collectWindowDragRegions();
252
+ const serialized = JSON.stringify(payload);
253
+ if (serialized === lastDragRegionPayload) return;
254
+ lastDragRegionPayload = serialized;
255
+ sendNow(payload);
256
+ }
257
+
258
+ function scheduleWindowDragRegionUpdate() {
259
+ if (dragRegionFrame) return;
260
+ dragRegionFrame = requestAnimationFrame(publishWindowDragRegions);
261
+ }
262
+
263
+ function activateWindowDragRegions() {
264
+ observeDragRoot(document.documentElement);
265
+ scheduleWindowDragRegionUpdate();
266
+
267
+ if (typeof ResizeObserver === 'function') {
268
+ const resizeObserver = new ResizeObserver(scheduleWindowDragRegionUpdate);
269
+ resizeObserver.observe(document.documentElement);
270
+ if (document.body) resizeObserver.observe(document.body);
271
+ }
96
272
  }
97
273
 
98
- document.addEventListener('mousedown', (event) => {
99
- if (!shouldStartWindowDrag(event)) return;
100
- event.preventDefault();
101
- sendNow({ type: 'system', command: 'start-window-drag' });
102
- }, true);
274
+ dragRegionObserver = new MutationObserver(scheduleWindowDragRegionUpdate);
275
+ window.addEventListener('resize', scheduleWindowDragRegionUpdate, { passive: true });
276
+ window.addEventListener('scroll', scheduleWindowDragRegionUpdate, { capture: true, passive: true });
277
+ window.addEventListener('transitionend', scheduleWindowDragRegionUpdate, true);
278
+ window.addEventListener('animationend', scheduleWindowDragRegionUpdate, true);
279
+ window.addEventListener('load', scheduleWindowDragRegionUpdate, { once: true });
280
+ document.addEventListener('load', scheduleWindowDragRegionUpdate, true);
281
+
282
+ if (document.readyState === 'loading') {
283
+ document.addEventListener('DOMContentLoaded', activateWindowDragRegions, { once: true });
284
+ } else {
285
+ activateWindowDragRegions();
286
+ }
103
287
 
104
288
  function makeEvent(channel) {
105
289
  return Object.freeze({
@@ -164,7 +164,11 @@ class BridgeServer {
164
164
  }
165
165
 
166
166
  if (message.type === 'system') {
167
- if (message.command === 'start-window-drag') win._startNativeDrag();
167
+ if (message.command === 'start-window-drag') {
168
+ win._startNativeDrag();
169
+ } else if (message.command === 'set-window-drag-regions') {
170
+ win._setNativeDragRegions(message.regions, message.viewport);
171
+ }
168
172
  return;
169
173
  }
170
174
 
@@ -224,6 +224,39 @@ class BrowserWindow extends EventEmitter {
224
224
  return this._sendHostCommand({ command: 'start-drag' });
225
225
  }
226
226
 
227
+ _setNativeDragRegions(regions, viewport) {
228
+ if (this._destroyed) return false;
229
+
230
+ const normalizedRegions = [];
231
+ for (const region of Array.isArray(regions) ? regions.slice(0, 4096) : []) {
232
+ const x = Number(region && region.x);
233
+ const y = Number(region && region.y);
234
+ const width = Number(region && region.width);
235
+ const height = Number(region && region.height);
236
+ if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) continue;
237
+ normalizedRegions.push({
238
+ x,
239
+ y,
240
+ width,
241
+ height,
242
+ draggable: region.draggable === true
243
+ });
244
+ }
245
+
246
+ const viewportWidth = Number(viewport && viewport.width);
247
+ const viewportHeight = Number(viewport && viewport.height);
248
+ const normalizedViewport = {
249
+ width: Number.isFinite(viewportWidth) && viewportWidth > 0 ? viewportWidth : this._bounds.width,
250
+ height: Number.isFinite(viewportHeight) && viewportHeight > 0 ? viewportHeight : this._bounds.height
251
+ };
252
+
253
+ return this._sendHostCommand({
254
+ command: 'set-drag-regions',
255
+ regions: normalizedRegions,
256
+ viewport: normalizedViewport
257
+ });
258
+ }
259
+
227
260
  _markRendererReady(details) {
228
261
  this._notifyDidFinishLoad(details && details.href ? details.href : this._currentUrl, 'bridge');
229
262
  }
@@ -245,6 +278,16 @@ class BrowserWindow extends EventEmitter {
245
278
  this.emit('blur');
246
279
  return;
247
280
  }
281
+ if (event.type === 'bounds-changed') {
282
+ const bounds = event.bounds && typeof event.bounds === 'object' ? event.bounds : {};
283
+ for (const key of ['x', 'y', 'width', 'height']) {
284
+ const value = Number(bounds[key]);
285
+ if (Number.isFinite(value)) this._bounds[key] = value;
286
+ }
287
+ if (event.reason === 'move') this.emit('move');
288
+ if (event.reason === 'resize') this.emit('resize');
289
+ return;
290
+ }
248
291
  if (event.type === 'minimize') {
249
292
  this._minimized = true;
250
293
  this.emit('minimize');
@@ -70,6 +70,51 @@ static NSColor *AtomJSColor(NSString *hex) {
70
70
  return [NSColor colorWithSRGBRed:red green:green blue:blue alpha:alpha];
71
71
  }
72
72
 
73
+ static NSRect AtomJSVirtualDesktopFrame(void) {
74
+ NSArray<NSScreen *> *screens = [NSScreen screens];
75
+ if (screens.count == 0) {
76
+ NSScreen *mainScreen = [NSScreen mainScreen];
77
+ return mainScreen ? mainScreen.frame : NSZeroRect;
78
+ }
79
+
80
+ NSRect frame = screens.firstObject.frame;
81
+ for (NSUInteger index = 1; index < screens.count; index += 1) {
82
+ NSScreen *screen = screens[index];
83
+ frame = NSUnionRect(frame, screen.frame);
84
+ }
85
+ return frame;
86
+ }
87
+
88
+ static NSRect AtomJSCocoaFrameFromAtomBounds(NSDictionary *bounds, NSRect fallback) {
89
+ NSRect virtualDesktop = AtomJSVirtualDesktopFrame();
90
+ CGFloat width = MAX(1.0, [AtomJSNumber(bounds[@"width"], @(fallback.size.width)) doubleValue]);
91
+ CGFloat height = MAX(1.0, [AtomJSNumber(bounds[@"height"], @(fallback.size.height)) doubleValue]);
92
+ CGFloat defaultX = fallback.origin.x - NSMinX(virtualDesktop);
93
+ CGFloat defaultY = NSMaxY(virtualDesktop) - NSMaxY(fallback);
94
+ CGFloat atomX = [AtomJSNumber(bounds[@"x"], @(defaultX)) doubleValue];
95
+ CGFloat atomY = [AtomJSNumber(bounds[@"y"], @(defaultY)) doubleValue];
96
+
97
+ return NSMakeRect(
98
+ NSMinX(virtualDesktop) + atomX,
99
+ NSMaxY(virtualDesktop) - atomY - height,
100
+ width,
101
+ height
102
+ );
103
+ }
104
+
105
+ static NSDictionary *AtomJSAtomBoundsFromWindow(NSWindow *window) {
106
+ if (!window) return @{ @"x": @0, @"y": @0, @"width": @0, @"height": @0 };
107
+
108
+ NSRect virtualDesktop = AtomJSVirtualDesktopFrame();
109
+ NSRect frame = window.frame;
110
+ return @{
111
+ @"x": @(frame.origin.x - NSMinX(virtualDesktop)),
112
+ @"y": @(NSMaxY(virtualDesktop) - NSMaxY(frame)),
113
+ @"width": @(frame.size.width),
114
+ @"height": @(frame.size.height)
115
+ };
116
+ }
117
+
73
118
  static void AtomJSConfigureTransparentWebView(WKWebView *webView) {
74
119
  if (!webView) return;
75
120
 
@@ -132,15 +177,129 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
132
177
  application.applicationIconImage = icon ?: AtomJSDefaultApplicationIcon();
133
178
  }
134
179
 
180
+ @interface AtomJSDraggableContentView : NSView
181
+ @property(nonatomic, strong) WKWebView *webView;
182
+ @property(nonatomic, copy) NSArray<NSDictionary *> *dragRegions;
183
+ @property(nonatomic, assign) NSSize viewportSize;
184
+ - (instancetype)initWithFrame:(NSRect)frame webView:(WKWebView *)webView;
185
+ - (void)updateDragRegions:(NSArray *)regions viewport:(NSDictionary *)viewport;
186
+ @end
187
+
188
+ @implementation AtomJSDraggableContentView
189
+
190
+ - (instancetype)initWithFrame:(NSRect)frame webView:(WKWebView *)webView {
191
+ self = [super initWithFrame:frame];
192
+ if (!self) return nil;
193
+
194
+ _webView = webView;
195
+ _dragRegions = @[];
196
+ _viewportSize = frame.size;
197
+ self.autoresizesSubviews = YES;
198
+ webView.frame = self.bounds;
199
+ webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
200
+ [self addSubview:webView];
201
+ return self;
202
+ }
203
+
204
+ - (BOOL)isFlipped {
205
+ return YES;
206
+ }
207
+
208
+ - (void)layout {
209
+ [super layout];
210
+ self.webView.frame = self.bounds;
211
+ }
212
+
213
+ - (void)updateDragRegions:(NSArray *)regions viewport:(NSDictionary *)viewport {
214
+ NSMutableArray<NSDictionary *> *normalized = [NSMutableArray array];
215
+ for (id value in [regions isKindOfClass:[NSArray class]] ? regions : @[]) {
216
+ if (![value isKindOfClass:[NSDictionary class]]) continue;
217
+ NSDictionary *region = value;
218
+ NSNumber *x = AtomJSNumber(region[@"x"], nil);
219
+ NSNumber *y = AtomJSNumber(region[@"y"], nil);
220
+ NSNumber *width = AtomJSNumber(region[@"width"], nil);
221
+ NSNumber *height = AtomJSNumber(region[@"height"], nil);
222
+ if (!x || !y || !width || !height || width.doubleValue <= 0 || height.doubleValue <= 0) continue;
223
+
224
+ [normalized addObject:@{
225
+ @"x": x,
226
+ @"y": y,
227
+ @"width": width,
228
+ @"height": height,
229
+ @"draggable": @(AtomJSBoolean(region[@"draggable"], NO))
230
+ }];
231
+ if (normalized.count >= 4096) break;
232
+ }
233
+
234
+ CGFloat viewportWidth = [AtomJSNumber(viewport[@"width"], @(self.bounds.size.width)) doubleValue];
235
+ CGFloat viewportHeight = [AtomJSNumber(viewport[@"height"], @(self.bounds.size.height)) doubleValue];
236
+ self.viewportSize = NSMakeSize(
237
+ viewportWidth > 0 ? viewportWidth : self.bounds.size.width,
238
+ viewportHeight > 0 ? viewportHeight : self.bounds.size.height
239
+ );
240
+ self.dragRegions = normalized;
241
+ }
242
+
243
+ - (BOOL)isDraggablePoint:(NSPoint)point {
244
+ CGFloat scaleX = self.viewportSize.width > 0 ? self.bounds.size.width / self.viewportSize.width : 1.0;
245
+ CGFloat scaleY = self.viewportSize.height > 0 ? self.bounds.size.height / self.viewportSize.height : 1.0;
246
+ BOOL draggable = NO;
247
+
248
+ for (NSDictionary *region in self.dragRegions) {
249
+ NSRect rect = NSMakeRect(
250
+ [AtomJSNumber(region[@"x"], @0) doubleValue] * scaleX,
251
+ [AtomJSNumber(region[@"y"], @0) doubleValue] * scaleY,
252
+ [AtomJSNumber(region[@"width"], @0) doubleValue] * scaleX,
253
+ [AtomJSNumber(region[@"height"], @0) doubleValue] * scaleY
254
+ );
255
+ if (!NSPointInRect(point, rect)) continue;
256
+ if (!AtomJSBoolean(region[@"draggable"], NO)) return NO;
257
+ draggable = YES;
258
+ }
259
+
260
+ return draggable;
261
+ }
262
+
263
+ - (NSView *)hitTest:(NSPoint)point {
264
+ if ([self isDraggablePoint:point]) return self;
265
+ return [super hitTest:point];
266
+ }
267
+
268
+ - (BOOL)acceptsFirstMouse:(NSEvent *)event {
269
+ return YES;
270
+ }
271
+
272
+ - (void)mouseDown:(NSEvent *)event {
273
+ if (!self.window || event.buttonNumber != 0) {
274
+ [super mouseDown:event];
275
+ return;
276
+ }
277
+
278
+ if (event.clickCount == 2) {
279
+ NSButton *zoomButton = [self.window standardWindowButton:NSWindowZoomButton];
280
+ if (zoomButton.enabled) {
281
+ [self.window zoom:nil];
282
+ return;
283
+ }
284
+ }
285
+
286
+ [self.window performWindowDragWithEvent:event];
287
+ }
288
+
289
+ @end
290
+
135
291
  @interface AtomJSWindowController : NSObject <NSWindowDelegate, WKNavigationDelegate>
136
292
  @property(nonatomic, strong) NSNumber *windowId;
137
293
  @property(nonatomic, strong) NSWindow *window;
138
294
  @property(nonatomic, strong) WKWebView *webView;
295
+ @property(nonatomic, strong) AtomJSDraggableContentView *contentView;
139
296
  @property(nonatomic, weak) AtomJSWindowController *parentController;
140
297
  @property(nonatomic, assign) BOOL modal;
141
298
  - (instancetype)initWithWindowId:(NSNumber *)windowId config:(NSDictionary *)config;
142
299
  - (void)navigate:(NSString *)urlString;
143
300
  - (void)beginWindowDrag;
301
+ - (void)updateDragRegions:(NSArray *)regions viewport:(NSDictionary *)viewport;
302
+ - (void)emitBoundsChangedWithReason:(NSString *)reason;
144
303
  @end
145
304
 
146
305
  @implementation AtomJSWindowController
@@ -212,7 +371,13 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
212
371
  }
213
372
 
214
373
  if ([config[@"x"] isKindOfClass:[NSNumber class]] && [config[@"y"] isKindOfClass:[NSNumber class]]) {
215
- [_window setFrameOrigin:NSMakePoint([config[@"x"] doubleValue], [config[@"y"] doubleValue])];
374
+ NSDictionary *initialBounds = @{
375
+ @"x": config[@"x"],
376
+ @"y": config[@"y"],
377
+ @"width": @(_window.frame.size.width),
378
+ @"height": @(_window.frame.size.height)
379
+ };
380
+ [_window setFrame:AtomJSCocoaFrameFromAtomBounds(initialBounds, _window.frame) display:NO animate:NO];
216
381
  } else if (AtomJSBoolean(config[@"center"], YES)) {
217
382
  [_window center];
218
383
  }
@@ -234,7 +399,11 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
234
399
  _webView.navigationDelegate = self;
235
400
  _webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
236
401
  if (AtomJSBoolean(config[@"transparent"], NO)) AtomJSConfigureTransparentWebView(_webView);
237
- _window.contentView = _webView;
402
+
403
+ _contentView = [[AtomJSDraggableContentView alloc]
404
+ initWithFrame:NSMakeRect(0, 0, width, height)
405
+ webView:_webView];
406
+ _window.contentView = _contentView;
238
407
 
239
408
  NSNumber *parentWindowId = AtomJSNumber(config[@"parentWindowId"], nil);
240
409
  _parentController = parentWindowId ? atomWindows[parentWindowId] : nil;
@@ -274,22 +443,30 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
274
443
  }
275
444
 
276
445
  - (void)beginWindowDrag {
277
- if (!self.window || ([NSEvent pressedMouseButtons] & 1) == 0) return;
446
+ NSEvent *event = [NSApp currentEvent];
447
+ if (
448
+ !self.window ||
449
+ !event ||
450
+ event.type != NSEventTypeLeftMouseDown ||
451
+ event.window != self.window
452
+ ) {
453
+ return;
454
+ }
455
+
456
+ [self.window performWindowDragWithEvent:event];
457
+ }
278
458
 
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];
459
+ - (void)updateDragRegions:(NSArray *)regions viewport:(NSDictionary *)viewport {
460
+ [self.contentView updateDragRegions:regions viewport:viewport];
461
+ }
291
462
 
292
- if (mouseDown) [self.window performWindowDragWithEvent:mouseDown];
463
+ - (void)emitBoundsChangedWithReason:(NSString *)reason {
464
+ AtomJSEmit(@{
465
+ @"type": @"bounds-changed",
466
+ @"windowId": self.windowId,
467
+ @"reason": reason ?: @"unknown",
468
+ @"bounds": AtomJSAtomBoundsFromWindow(self.window)
469
+ });
293
470
  }
294
471
 
295
472
  - (void)navigate:(NSString *)urlString {
@@ -383,6 +560,14 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
383
560
  });
384
561
  }
385
562
 
563
+ - (void)windowDidMove:(NSNotification *)notification {
564
+ [self emitBoundsChangedWithReason:@"move"];
565
+ }
566
+
567
+ - (void)windowDidResize:(NSNotification *)notification {
568
+ [self emitBoundsChangedWithReason:@"resize"];
569
+ }
570
+
386
571
  @end
387
572
 
388
573
  static AtomJSWindowController *AtomJSController(NSDictionary *message) {
@@ -540,6 +725,7 @@ static void AtomJSHandleMessage(NSDictionary *message) {
540
725
  @"type": @"created",
541
726
  @"windowId": windowId
542
727
  });
728
+ [controller emitBoundsChangedWithReason:@"create"];
543
729
  return;
544
730
  }
545
731
 
@@ -585,6 +771,10 @@ static void AtomJSHandleMessage(NSDictionary *message) {
585
771
  [NSApp activateIgnoringOtherApps:YES];
586
772
  } else if ([command isEqualToString:@"start-drag"]) {
587
773
  [controller beginWindowDrag];
774
+ } else if ([command isEqualToString:@"set-drag-regions"]) {
775
+ NSArray *regions = [message[@"regions"] isKindOfClass:[NSArray class]] ? message[@"regions"] : @[];
776
+ NSDictionary *viewport = [message[@"viewport"] isKindOfClass:[NSDictionary class]] ? message[@"viewport"] : @{};
777
+ [controller updateDragRegions:regions viewport:viewport];
588
778
  } else if ([command isEqualToString:@"close"] || [command isEqualToString:@"destroy"]) {
589
779
  [controller.window close];
590
780
  } else if ([command isEqualToString:@"set-title"]) {
@@ -603,13 +793,12 @@ static void AtomJSHandleMessage(NSDictionary *message) {
603
793
  BOOL active = (controller.window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen;
604
794
  if (requested != active) [controller.window toggleFullScreen:nil];
605
795
  } else if ([command isEqualToString:@"set-bounds"]) {
606
- NSDictionary *bounds = [message[@"bounds"] isKindOfClass:[NSDictionary class]] ? message[@"bounds"] : @{};
607
- NSRect frame = controller.window.frame;
608
- if ([bounds[@"x"] isKindOfClass:[NSNumber class]]) frame.origin.x = [bounds[@"x"] doubleValue];
609
- if ([bounds[@"y"] isKindOfClass:[NSNumber class]]) frame.origin.y = [bounds[@"y"] doubleValue];
610
- if ([bounds[@"width"] isKindOfClass:[NSNumber class]]) frame.size.width = MAX(1.0, [bounds[@"width"] doubleValue]);
611
- if ([bounds[@"height"] isKindOfClass:[NSNumber class]]) frame.size.height = MAX(1.0, [bounds[@"height"] doubleValue]);
612
- [controller.window setFrame:frame display:YES animate:NO];
796
+ NSDictionary *requested = [message[@"bounds"] isKindOfClass:[NSDictionary class]] ? message[@"bounds"] : @{};
797
+ NSMutableDictionary *bounds = [AtomJSAtomBoundsFromWindow(controller.window) mutableCopy];
798
+ for (NSString *key in @[ @"x", @"y", @"width", @"height" ]) {
799
+ if ([requested[key] isKindOfClass:[NSNumber class]]) bounds[key] = requested[key];
800
+ }
801
+ [controller.window setFrame:AtomJSCocoaFrameFromAtomBounds(bounds, controller.window.frame) display:YES animate:NO];
613
802
  } else if ([command isEqualToString:@"set-always-on-top"]) {
614
803
  controller.window.level = AtomJSBoolean(message[@"value"], NO) ? NSFloatingWindowLevel : NSNormalWindowLevel;
615
804
  } else if ([command isEqualToString:@"set-opacity"]) {