@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 +1 -1
- package/src/bridge-script.cjs +205 -21
- package/src/bridge-server.cjs +5 -1
- package/src/browser-window.cjs +43 -0
- package/src/runtime/macos-native-host.m +212 -23
package/package.json
CHANGED
package/src/bridge-script.cjs
CHANGED
|
@@ -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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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')
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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({
|
package/src/bridge-server.cjs
CHANGED
|
@@ -164,7 +164,11 @@ class BridgeServer {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
if (message.type === 'system') {
|
|
167
|
-
if (message.command === 'start-window-drag')
|
|
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
|
|
package/src/browser-window.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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 *
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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"]) {
|