@atom-js-org/runtime 0.5.1-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/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/bridge-script.cjs +246 -0
- package/src/bridge-server.cjs +9 -0
- package/src/browser-window.cjs +54 -0
- package/src/runtime/macos-native-host.m +222 -9
package/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/bridge-script.cjs
CHANGED
|
@@ -39,6 +39,252 @@ function generateBridgeScript({ websocketUrl, preloadCode = '' }) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function normalizeAppRegion(value) {
|
|
43
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
44
|
+
return normalized === 'drag' || normalized === 'no-drag' ? normalized : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appRegionForElement(element) {
|
|
48
|
+
if (!(element instanceof Element)) return '';
|
|
49
|
+
if (element.hasAttribute('data-atom-no-drag')) return 'no-drag';
|
|
50
|
+
if (element.hasAttribute('data-atom-drag-region')) return 'drag';
|
|
51
|
+
|
|
52
|
+
const attribute = normalizeAppRegion(
|
|
53
|
+
element.getAttribute('data-atom-app-region') || element.getAttribute('data-app-region')
|
|
54
|
+
);
|
|
55
|
+
if (attribute) return attribute;
|
|
56
|
+
|
|
57
|
+
const inline = normalizeAppRegion(
|
|
58
|
+
element.style && (
|
|
59
|
+
element.style.getPropertyValue('-webkit-app-region') ||
|
|
60
|
+
element.style.getPropertyValue('app-region') ||
|
|
61
|
+
element.style.getPropertyValue('--atom-app-region')
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
if (inline) return inline;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const computed = getComputedStyle(element);
|
|
68
|
+
return normalizeAppRegion(
|
|
69
|
+
computed.getPropertyValue('-webkit-app-region') ||
|
|
70
|
+
computed.getPropertyValue('app-region') ||
|
|
71
|
+
computed.getPropertyValue('--atom-app-region')
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
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;
|
|
115
|
+
}
|
|
116
|
+
|
|
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
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasDraggableAncestor(element) {
|
|
132
|
+
let current = composedParent(element);
|
|
133
|
+
while (current) {
|
|
134
|
+
const region = appRegionForElement(current);
|
|
135
|
+
if (region === 'no-drag') return false;
|
|
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
|
+
}
|
|
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
|
+
});
|
|
236
|
+
|
|
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
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
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
|
+
}
|
|
287
|
+
|
|
42
288
|
function makeEvent(channel) {
|
|
43
289
|
return Object.freeze({
|
|
44
290
|
channel,
|
package/src/bridge-server.cjs
CHANGED
|
@@ -163,6 +163,15 @@ class BridgeServer {
|
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
if (message.type === 'system') {
|
|
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
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
166
175
|
if (message.type === 'send') {
|
|
167
176
|
const event = createIpcEvent(win);
|
|
168
177
|
ipcMain.emit(message.channel, event, ...(Array.isArray(message.args) ? message.args : []));
|
package/src/browser-window.cjs
CHANGED
|
@@ -219,6 +219,44 @@ class BrowserWindow extends EventEmitter {
|
|
|
219
219
|
return false;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
_startNativeDrag() {
|
|
223
|
+
if (this._destroyed) return false;
|
|
224
|
+
return this._sendHostCommand({ command: 'start-drag' });
|
|
225
|
+
}
|
|
226
|
+
|
|
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
|
+
|
|
222
260
|
_markRendererReady(details) {
|
|
223
261
|
this._notifyDidFinishLoad(details && details.href ? details.href : this._currentUrl, 'bridge');
|
|
224
262
|
}
|
|
@@ -240,6 +278,16 @@ class BrowserWindow extends EventEmitter {
|
|
|
240
278
|
this.emit('blur');
|
|
241
279
|
return;
|
|
242
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
|
+
}
|
|
243
291
|
if (event.type === 'minimize') {
|
|
244
292
|
this._minimized = true;
|
|
245
293
|
this.emit('minimize');
|
|
@@ -334,6 +382,12 @@ class BrowserWindow extends EventEmitter {
|
|
|
334
382
|
}
|
|
335
383
|
}
|
|
336
384
|
|
|
385
|
+
startDrag() {
|
|
386
|
+
if (!this._startNativeDrag()) {
|
|
387
|
+
console.warn('[AtomJS] Native window dragging is not supported by the current platform host.');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
337
391
|
blur() {}
|
|
338
392
|
|
|
339
393
|
close() {
|
|
@@ -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,14 +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;
|
|
300
|
+
- (void)beginWindowDrag;
|
|
301
|
+
- (void)updateDragRegions:(NSArray *)regions viewport:(NSDictionary *)viewport;
|
|
302
|
+
- (void)emitBoundsChangedWithReason:(NSString *)reason;
|
|
143
303
|
@end
|
|
144
304
|
|
|
145
305
|
@implementation AtomJSWindowController
|
|
@@ -175,6 +335,8 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
|
|
|
175
335
|
defer:NO];
|
|
176
336
|
_window.releasedWhenClosed = NO;
|
|
177
337
|
_window.delegate = self;
|
|
338
|
+
_window.movable = YES;
|
|
339
|
+
_window.movableByWindowBackground = NO;
|
|
178
340
|
_window.title = AtomJSString(config[@"title"], atomAppName);
|
|
179
341
|
_window.backgroundColor = AtomJSColor(config[@"backgroundColor"]);
|
|
180
342
|
_window.tabbingMode = NSWindowTabbingModeDisallowed;
|
|
@@ -209,7 +371,13 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
|
|
|
209
371
|
}
|
|
210
372
|
|
|
211
373
|
if ([config[@"x"] isKindOfClass:[NSNumber class]] && [config[@"y"] isKindOfClass:[NSNumber class]]) {
|
|
212
|
-
|
|
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];
|
|
213
381
|
} else if (AtomJSBoolean(config[@"center"], YES)) {
|
|
214
382
|
[_window center];
|
|
215
383
|
}
|
|
@@ -231,7 +399,11 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
|
|
|
231
399
|
_webView.navigationDelegate = self;
|
|
232
400
|
_webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
233
401
|
if (AtomJSBoolean(config[@"transparent"], NO)) AtomJSConfigureTransparentWebView(_webView);
|
|
234
|
-
|
|
402
|
+
|
|
403
|
+
_contentView = [[AtomJSDraggableContentView alloc]
|
|
404
|
+
initWithFrame:NSMakeRect(0, 0, width, height)
|
|
405
|
+
webView:_webView];
|
|
406
|
+
_window.contentView = _contentView;
|
|
235
407
|
|
|
236
408
|
NSNumber *parentWindowId = AtomJSNumber(config[@"parentWindowId"], nil);
|
|
237
409
|
_parentController = parentWindowId ? atomWindows[parentWindowId] : nil;
|
|
@@ -270,6 +442,33 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
|
|
|
270
442
|
return self;
|
|
271
443
|
}
|
|
272
444
|
|
|
445
|
+
- (void)beginWindowDrag {
|
|
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
|
+
}
|
|
458
|
+
|
|
459
|
+
- (void)updateDragRegions:(NSArray *)regions viewport:(NSDictionary *)viewport {
|
|
460
|
+
[self.contentView updateDragRegions:regions viewport:viewport];
|
|
461
|
+
}
|
|
462
|
+
|
|
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
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
273
472
|
- (void)navigate:(NSString *)urlString {
|
|
274
473
|
NSURL *url = [NSURL URLWithString:urlString ?: @"about:blank"];
|
|
275
474
|
if (!url) {
|
|
@@ -361,6 +560,14 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
|
|
|
361
560
|
});
|
|
362
561
|
}
|
|
363
562
|
|
|
563
|
+
- (void)windowDidMove:(NSNotification *)notification {
|
|
564
|
+
[self emitBoundsChangedWithReason:@"move"];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
- (void)windowDidResize:(NSNotification *)notification {
|
|
568
|
+
[self emitBoundsChangedWithReason:@"resize"];
|
|
569
|
+
}
|
|
570
|
+
|
|
364
571
|
@end
|
|
365
572
|
|
|
366
573
|
static AtomJSWindowController *AtomJSController(NSDictionary *message) {
|
|
@@ -518,6 +725,7 @@ static void AtomJSHandleMessage(NSDictionary *message) {
|
|
|
518
725
|
@"type": @"created",
|
|
519
726
|
@"windowId": windowId
|
|
520
727
|
});
|
|
728
|
+
[controller emitBoundsChangedWithReason:@"create"];
|
|
521
729
|
return;
|
|
522
730
|
}
|
|
523
731
|
|
|
@@ -561,6 +769,12 @@ static void AtomJSHandleMessage(NSDictionary *message) {
|
|
|
561
769
|
[controller.window makeKeyAndOrderFront:nil];
|
|
562
770
|
[controller.window orderFrontRegardless];
|
|
563
771
|
[NSApp activateIgnoringOtherApps:YES];
|
|
772
|
+
} else if ([command isEqualToString:@"start-drag"]) {
|
|
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];
|
|
564
778
|
} else if ([command isEqualToString:@"close"] || [command isEqualToString:@"destroy"]) {
|
|
565
779
|
[controller.window close];
|
|
566
780
|
} else if ([command isEqualToString:@"set-title"]) {
|
|
@@ -579,13 +793,12 @@ static void AtomJSHandleMessage(NSDictionary *message) {
|
|
|
579
793
|
BOOL active = (controller.window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen;
|
|
580
794
|
if (requested != active) [controller.window toggleFullScreen:nil];
|
|
581
795
|
} else if ([command isEqualToString:@"set-bounds"]) {
|
|
582
|
-
NSDictionary *
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
[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];
|
|
589
802
|
} else if ([command isEqualToString:@"set-always-on-top"]) {
|
|
590
803
|
controller.window.level = AtomJSBoolean(message[@"value"], NO) ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
|
591
804
|
} else if ([command isEqualToString:@"set-opacity"]) {
|