@windowkit/appkit 0.1.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/src/addon.mm ADDED
@@ -0,0 +1,966 @@
1
+ // @windowkit/appkit: retained-mode CALayer / CoreText backend for Node.js
2
+ //
3
+ // Design: node's main thread IS the process main thread on macOS, so we can own
4
+ // NSApplication from JS. We never call [NSApp run]; instead JS drives an event
5
+ // pump (nextEventMatchingMask with distantPast) on a timer. Core Animation
6
+ // runs its animations in the render server (WindowServer), so animations stay
7
+ // smooth regardless of pump cadence.
8
+
9
+ #include <napi.h>
10
+ #import <Cocoa/Cocoa.h>
11
+ #import <IOSurface/IOSurface.h>
12
+ #import <QuartzCore/QuartzCore.h>
13
+ #import <CoreText/CoreText.h>
14
+ #import <ImageIO/ImageIO.h>
15
+
16
+ #include <cmath>
17
+ #include <string>
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ static NSString* ToNSString(Napi::Value v) {
24
+ std::string s = v.As<Napi::String>().Utf8Value();
25
+ return [NSString stringWithUTF8String:s.c_str()];
26
+ }
27
+
28
+ static double NumOr(Napi::Object o, const char* k, double d) {
29
+ if (!o.Has(k)) return d;
30
+ Napi::Value v = o.Get(k);
31
+ return v.IsNumber() ? v.As<Napi::Number>().DoubleValue() : d;
32
+ }
33
+
34
+ static bool BoolOr(Napi::Object o, const char* k, bool d) {
35
+ if (!o.Has(k)) return d;
36
+ Napi::Value v = o.Get(k);
37
+ return v.IsBoolean() ? v.As<Napi::Boolean>().Value() : d;
38
+ }
39
+
40
+ static NSString* StrOr(Napi::Object o, const char* k, NSString* d) {
41
+ if (!o.Has(k)) return d;
42
+ Napi::Value v = o.Get(k);
43
+ return v.IsString() ? ToNSString(v) : d;
44
+ }
45
+
46
+ // [r,g,b] or [r,g,b,a], components 0..1 — caller owns the returned color.
47
+ static CGColorRef MakeColor(Napi::Value v) {
48
+ Napi::Array a = v.As<Napi::Array>();
49
+ double r = a.Get(0u).As<Napi::Number>().DoubleValue();
50
+ double g = a.Get(1u).As<Napi::Number>().DoubleValue();
51
+ double b = a.Get(2u).As<Napi::Number>().DoubleValue();
52
+ double al = a.Length() > 3 ? a.Get(3u).As<Napi::Number>().DoubleValue() : 1.0;
53
+ return CGColorCreateGenericRGB(r, g, b, al);
54
+ }
55
+
56
+ static CGPoint PointFrom(Napi::Value v) {
57
+ Napi::Array a = v.As<Napi::Array>();
58
+ return CGPointMake(a.Get(0u).As<Napi::Number>().DoubleValue(),
59
+ a.Get(1u).As<Napi::Number>().DoubleValue());
60
+ }
61
+
62
+ static CGRect RectFrom(Napi::Value v) {
63
+ Napi::Array a = v.As<Napi::Array>();
64
+ return CGRectMake(a.Get(0u).As<Napi::Number>().DoubleValue(),
65
+ a.Get(1u).As<Napi::Number>().DoubleValue(),
66
+ a.Get(2u).As<Napi::Number>().DoubleValue(),
67
+ a.Get(3u).As<Napi::Number>().DoubleValue());
68
+ }
69
+
70
+ template <typename T>
71
+ static T Deref(Napi::Value v) {
72
+ return (__bridge T)(v.As<Napi::External<void>>().Data());
73
+ }
74
+
75
+ // Wrap an ObjC object as an External holding a +1 retain, released on GC.
76
+ static Napi::Value WrapRetained(Napi::Env env, id obj) {
77
+ void* p = (void*)CFBridgingRetain(obj);
78
+ return Napi::External<void>::New(env, p, [](Napi::Env, void* d) { CFRelease(d); });
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // app / window
83
+ // ---------------------------------------------------------------------------
84
+
85
+ @interface CALHostView : NSView
86
+ @end
87
+ @implementation CALHostView
88
+ - (BOOL)acceptsFirstResponder { return YES; }
89
+ // AppKit forces the hosted layer's geometryFlipped to match isFlipped, so this
90
+ // is what actually gives the layer tree a top-left origin.
91
+ - (BOOL)isFlipped { return YES; }
92
+ // Swallow keys so unhandled keyDown doesn't beep; JS observes keys in the pump.
93
+ - (void)keyDown:(NSEvent*)event { (void)event; }
94
+ @end
95
+
96
+ static bool gAppInited = false;
97
+
98
+ static void EnsureApp() {
99
+ if (gAppInited) return;
100
+ @autoreleasepool {
101
+ [NSApplication sharedApplication];
102
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
103
+ [NSApp finishLaunching];
104
+ }
105
+ gAppInited = true;
106
+ }
107
+
108
+ static Napi::Value InitApp(const Napi::CallbackInfo& info) {
109
+ EnsureApp();
110
+ return info.Env().Undefined();
111
+ }
112
+
113
+ static Napi::Value CreateWindowFn(const Napi::CallbackInfo& info) {
114
+ Napi::Env env = info.Env();
115
+ EnsureApp();
116
+ double w = info[0].As<Napi::Number>().DoubleValue();
117
+ double h = info[1].As<Napi::Number>().DoubleValue();
118
+ NSString* title = info.Length() > 2 && info[2].IsString() ? ToNSString(info[2]) : @"";
119
+
120
+ NSWindow* win;
121
+ @autoreleasepool {
122
+ NSRect rect = NSMakeRect(0, 0, w, h);
123
+ win = [[NSWindow alloc]
124
+ initWithContentRect:rect
125
+ styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
126
+ NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable)
127
+ backing:NSBackingStoreBuffered
128
+ defer:NO];
129
+ win.releasedWhenClosed = NO;
130
+ win.title = title;
131
+ win.acceptsMouseMovedEvents = YES;
132
+
133
+ // Layer-hosting view: we own the CALayer tree entirely.
134
+ CALHostView* view = [[CALHostView alloc] initWithFrame:rect];
135
+ CALayer* root = [CALayer layer];
136
+ root.geometryFlipped = YES; // top-left origin, like every UI toolkit
137
+ [view setLayer:root];
138
+ [view setWantsLayer:YES];
139
+ win.contentView = view;
140
+ root.contentsScale = win.backingScaleFactor;
141
+
142
+ [win center];
143
+ [win makeKeyAndOrderFront:nil];
144
+ [win makeFirstResponder:view];
145
+ [NSApp activateIgnoringOtherApps:YES];
146
+ }
147
+ return WrapRetained(env, win);
148
+ }
149
+
150
+ static Napi::Value WindowRootLayer(const Napi::CallbackInfo& info) {
151
+ NSWindow* win = Deref<NSWindow*>(info[0]);
152
+ return WrapRetained(info.Env(), win.contentView.layer);
153
+ }
154
+
155
+ static Napi::Value WindowScale(const Napi::CallbackInfo& info) {
156
+ NSWindow* win = Deref<NSWindow*>(info[0]);
157
+ return Napi::Number::New(info.Env(), win.backingScaleFactor);
158
+ }
159
+
160
+ static Napi::Value WindowContentSize(const Napi::CallbackInfo& info) {
161
+ NSWindow* win = Deref<NSWindow*>(info[0]);
162
+ NSSize s = win.contentView.bounds.size;
163
+ Napi::Array a = Napi::Array::New(info.Env(), 2);
164
+ a.Set(0u, s.width);
165
+ a.Set(1u, s.height);
166
+ return a;
167
+ }
168
+
169
+ static Napi::Value WindowIsVisible(const Napi::CallbackInfo& info) {
170
+ NSWindow* win = Deref<NSWindow*>(info[0]);
171
+ return Napi::Boolean::New(info.Env(), win.isVisible);
172
+ }
173
+
174
+ static Napi::Value WindowNumber(const Napi::CallbackInfo& info) {
175
+ NSWindow* win = Deref<NSWindow*>(info[0]);
176
+ return Napi::Number::New(info.Env(), (double)win.windowNumber);
177
+ }
178
+
179
+ static Napi::Value CloseWindow(const Napi::CallbackInfo& info) {
180
+ NSWindow* win = Deref<NSWindow*>(info[0]);
181
+ [win close];
182
+ return info.Env().Undefined();
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // event pump
187
+ // ---------------------------------------------------------------------------
188
+
189
+ static Napi::FunctionReference gEventCb;
190
+
191
+ static void DispatchEvent(Napi::Env env, NSEvent* e) {
192
+ if (gEventCb.IsEmpty()) return;
193
+ const char* type = nullptr;
194
+ bool mouse = false, key = false, wheel = false;
195
+ switch (e.type) {
196
+ case NSEventTypeLeftMouseDown: type = "mousedown"; mouse = true; break;
197
+ case NSEventTypeLeftMouseUp: type = "mouseup"; mouse = true; break;
198
+ case NSEventTypeRightMouseDown: type = "rightdown"; mouse = true; break;
199
+ case NSEventTypeRightMouseUp: type = "rightup"; mouse = true; break;
200
+ case NSEventTypeMouseMoved: type = "mousemove"; mouse = true; break;
201
+ case NSEventTypeLeftMouseDragged: type = "mousedrag"; mouse = true; break;
202
+ case NSEventTypeScrollWheel: type = "wheel"; mouse = true; wheel = true; break;
203
+ case NSEventTypeKeyDown: type = "keydown"; key = true; break;
204
+ case NSEventTypeKeyUp: type = "keyup"; key = true; break;
205
+ default: return;
206
+ }
207
+ if (mouse && !e.window) return; // e.g. moves outside any of our windows
208
+ Napi::HandleScope scope(env);
209
+ Napi::Object ev = Napi::Object::New(env);
210
+ ev.Set("type", type);
211
+ if (mouse && e.window) {
212
+ NSView* v = e.window.contentView;
213
+ NSPoint p = [v convertPoint:e.locationInWindow fromView:nil];
214
+ ev.Set("x", p.x);
215
+ ev.Set("y", v.isFlipped ? p.y : v.bounds.size.height - p.y); // top-left origin
216
+ }
217
+ if (wheel) {
218
+ ev.Set("dx", e.scrollingDeltaX);
219
+ ev.Set("dy", e.scrollingDeltaY);
220
+ }
221
+ if (key) {
222
+ ev.Set("keyCode", (double)e.keyCode);
223
+ NSString* ch = e.charactersIgnoringModifiers;
224
+ if (ch) ev.Set("chars", ch.UTF8String);
225
+ ev.Set("repeat", (bool)e.isARepeat);
226
+ }
227
+ gEventCb.Call({ev});
228
+ }
229
+
230
+ // postMouseEvent(win, 'down'|'up'|'move'|'drag', x, y) — synthesizes an event
231
+ // through the normal pump path (top-left coords). Handy for automated tests.
232
+ static Napi::Value PostMouseEvent(const Napi::CallbackInfo& info) {
233
+ NSWindow* win = Deref<NSWindow*>(info[0]);
234
+ std::string t = info[1].As<Napi::String>().Utf8Value();
235
+ double x = info[2].As<Napi::Number>().DoubleValue();
236
+ double y = info[3].As<Napi::Number>().DoubleValue();
237
+ NSEventType type;
238
+ if (t == "down") type = NSEventTypeLeftMouseDown;
239
+ else if (t == "up") type = NSEventTypeLeftMouseUp;
240
+ else if (t == "drag") type = NSEventTypeLeftMouseDragged;
241
+ else type = NSEventTypeMouseMoved;
242
+ NSView* v = win.contentView;
243
+ NSPoint wp = [v convertPoint:NSMakePoint(x, y) toView:nil]; // v is flipped
244
+ NSEvent* e = [NSEvent mouseEventWithType:type
245
+ location:wp
246
+ modifierFlags:0
247
+ timestamp:[[NSProcessInfo processInfo] systemUptime]
248
+ windowNumber:win.windowNumber
249
+ context:nil
250
+ eventNumber:0
251
+ clickCount:1
252
+ pressure:1];
253
+ [NSApp postEvent:e atStart:NO];
254
+ return info.Env().Undefined();
255
+ }
256
+
257
+ static Napi::Value SetEventCallback(const Napi::CallbackInfo& info) {
258
+ if (info[0].IsFunction()) {
259
+ gEventCb = Napi::Persistent(info[0].As<Napi::Function>());
260
+ } else {
261
+ gEventCb.Reset();
262
+ }
263
+ return info.Env().Undefined();
264
+ }
265
+
266
+ static Napi::Value Pump(const Napi::CallbackInfo& info) {
267
+ Napi::Env env = info.Env();
268
+ EnsureApp();
269
+ @autoreleasepool {
270
+ while (true) {
271
+ NSEvent* e = [NSApp nextEventMatchingMask:NSEventMaskAny
272
+ untilDate:[NSDate distantPast]
273
+ inMode:NSDefaultRunLoopMode
274
+ dequeue:YES];
275
+ if (!e) break;
276
+ DispatchEvent(env, e);
277
+ [NSApp sendEvent:e];
278
+ }
279
+ [CATransaction flush];
280
+ }
281
+ return env.Undefined();
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // layers
286
+ // ---------------------------------------------------------------------------
287
+
288
+ static Napi::Value CreateLayer(const Napi::CallbackInfo& info) {
289
+ return WrapRetained(info.Env(), [CALayer layer]);
290
+ }
291
+ static Napi::Value CreateTextLayer(const Napi::CallbackInfo& info) {
292
+ CATextLayer* t = [CATextLayer layer];
293
+ t.contentsScale = 2.0; // sane retina default; overridable via contentsScale
294
+ return WrapRetained(info.Env(), t);
295
+ }
296
+ static Napi::Value CreateGradientLayer(const Napi::CallbackInfo& info) {
297
+ return WrapRetained(info.Env(), [CAGradientLayer layer]);
298
+ }
299
+ static Napi::Value CreateShapeLayer(const Napi::CallbackInfo& info) {
300
+ return WrapRetained(info.Env(), [CAShapeLayer layer]);
301
+ }
302
+
303
+ static Napi::Value AddSublayer(const Napi::CallbackInfo& info) {
304
+ CALayer* parent = Deref<CALayer*>(info[0]);
305
+ CALayer* child = Deref<CALayer*>(info[1]);
306
+ [parent addSublayer:child];
307
+ return info.Env().Undefined();
308
+ }
309
+
310
+ static Napi::Value RemoveFromSuperlayer(const Napi::CallbackInfo& info) {
311
+ CALayer* l = Deref<CALayer*>(info[0]);
312
+ [l removeFromSuperlayer];
313
+ return info.Env().Undefined();
314
+ }
315
+
316
+ static void ApplyTransform(CALayer* L, Napi::Value v) {
317
+ if (v.IsNull() || v.IsUndefined()) {
318
+ L.transform = CATransform3DIdentity;
319
+ return;
320
+ }
321
+ Napi::Object t = v.As<Napi::Object>();
322
+ CATransform3D m = CATransform3DIdentity;
323
+ m = CATransform3DTranslate(m, NumOr(t, "translateX", 0), NumOr(t, "translateY", 0), 0);
324
+ double rot = NumOr(t, "rotate", 0); // radians
325
+ if (rot != 0) m = CATransform3DRotate(m, rot, 0, 0, 1);
326
+ double s = NumOr(t, "scale", 1);
327
+ double sx = NumOr(t, "scaleX", s), sy = NumOr(t, "scaleY", s);
328
+ if (sx != 1 || sy != 1) m = CATransform3DScale(m, sx, sy, 1);
329
+ L.transform = m;
330
+ }
331
+
332
+ static void SetColorProp(CALayer* L, Napi::Object o, const char* key,
333
+ void (^setter)(CGColorRef)) {
334
+ if (!o.Has(key)) return;
335
+ Napi::Value v = o.Get(key);
336
+ if (v.IsNull()) {
337
+ setter(NULL);
338
+ } else {
339
+ CGColorRef c = MakeColor(v);
340
+ setter(c);
341
+ CGColorRelease(c);
342
+ }
343
+ }
344
+
345
+ static void ApplyLayerProps(CALayer* L, Napi::Object o) {
346
+ if (o.Has("frame")) L.frame = RectFrom(o.Get("frame"));
347
+ if (o.Has("bounds")) {
348
+ // [w, h] or [x, y, w, h] — the four-element form carries a bounds
349
+ // ORIGIN, which is Core Animation's native scroll: the layer shows its
350
+ // sublayers shifted by (-x, -y) with nothing repainted.
351
+ Napi::Array a = o.Get("bounds").As<Napi::Array>();
352
+ if (a.Length() >= 4) {
353
+ L.bounds = CGRectMake(a.Get(0u).As<Napi::Number>().DoubleValue(),
354
+ a.Get(1u).As<Napi::Number>().DoubleValue(),
355
+ a.Get(2u).As<Napi::Number>().DoubleValue(),
356
+ a.Get(3u).As<Napi::Number>().DoubleValue());
357
+ } else {
358
+ L.bounds = CGRectMake(0, 0, a.Get(0u).As<Napi::Number>().DoubleValue(),
359
+ a.Get(1u).As<Napi::Number>().DoubleValue());
360
+ }
361
+ }
362
+ if (o.Has("position")) L.position = PointFrom(o.Get("position"));
363
+ if (o.Has("anchorPoint")) L.anchorPoint = PointFrom(o.Get("anchorPoint"));
364
+ if (o.Has("zPosition")) L.zPosition = NumOr(o, "zPosition", 0);
365
+ SetColorProp(L, o, "backgroundColor", ^(CGColorRef c) { L.backgroundColor = c; });
366
+ SetColorProp(L, o, "borderColor", ^(CGColorRef c) { L.borderColor = c; });
367
+ SetColorProp(L, o, "shadowColor", ^(CGColorRef c) { L.shadowColor = c; });
368
+ if (o.Has("cornerRadius")) L.cornerRadius = NumOr(o, "cornerRadius", 0);
369
+ if (o.Has("borderWidth")) L.borderWidth = NumOr(o, "borderWidth", 0);
370
+ if (o.Has("opacity")) L.opacity = (float)NumOr(o, "opacity", 1);
371
+ if (o.Has("hidden")) L.hidden = BoolOr(o, "hidden", false);
372
+ if (o.Has("masksToBounds")) L.masksToBounds = BoolOr(o, "masksToBounds", false);
373
+ if (o.Has("shadowOpacity")) L.shadowOpacity = (float)NumOr(o, "shadowOpacity", 0);
374
+ if (o.Has("shadowRadius")) L.shadowRadius = NumOr(o, "shadowRadius", 3);
375
+ if (o.Has("shadowOffset")) {
376
+ CGPoint p = PointFrom(o.Get("shadowOffset"));
377
+ L.shadowOffset = CGSizeMake(p.x, p.y);
378
+ }
379
+ if (o.Has("contentsScale")) L.contentsScale = NumOr(o, "contentsScale", 1);
380
+ if (o.Has("name")) L.name = ToNSString(o.Get("name"));
381
+ if (o.Has("mask")) {
382
+ Napi::Value v = o.Get("mask");
383
+ L.mask = (v.IsNull() || v.IsUndefined()) ? nil : Deref<CALayer*>(v);
384
+ }
385
+ if (o.Has("transform")) ApplyTransform(L, o.Get("transform"));
386
+ if (o.Has("contents")) {
387
+ Napi::Value v = o.Get("contents");
388
+ if (v.IsNull()) L.contents = nil;
389
+ }
390
+ }
391
+
392
+ static Napi::Value SetLayerProps(const Napi::CallbackInfo& info) {
393
+ CALayer* L = Deref<CALayer*>(info[0]);
394
+ ApplyLayerProps(L, info[1].As<Napi::Object>());
395
+ return info.Env().Undefined();
396
+ }
397
+
398
+ // ---------------------------------------------------------------------------
399
+ // CATextLayer
400
+ // ---------------------------------------------------------------------------
401
+
402
+ static Napi::Value SetTextProps(const Napi::CallbackInfo& info) {
403
+ CATextLayer* T = (CATextLayer*)Deref<CALayer*>(info[0]);
404
+ Napi::Object o = info[1].As<Napi::Object>();
405
+ if (o.Has("fontSize")) T.fontSize = NumOr(o, "fontSize", 14);
406
+ if (o.Has("fontName")) {
407
+ NSString* name = ToNSString(o.Get("fontName"));
408
+ CTFontRef f = CTFontCreateWithName((__bridge CFStringRef)name,
409
+ T.fontSize > 0 ? T.fontSize : 14, NULL);
410
+ T.font = f;
411
+ CFRelease(f);
412
+ }
413
+ if (o.Has("string")) T.string = ToNSString(o.Get("string"));
414
+ SetColorProp(T, o, "color", ^(CGColorRef c) { T.foregroundColor = c; });
415
+ if (o.Has("align")) {
416
+ NSString* a = ToNSString(o.Get("align"));
417
+ if ([a isEqualToString:@"center"]) T.alignmentMode = kCAAlignmentCenter;
418
+ else if ([a isEqualToString:@"right"]) T.alignmentMode = kCAAlignmentRight;
419
+ else if ([a isEqualToString:@"justified"]) T.alignmentMode = kCAAlignmentJustified;
420
+ else T.alignmentMode = kCAAlignmentLeft;
421
+ }
422
+ if (o.Has("wrapped")) T.wrapped = BoolOr(o, "wrapped", false);
423
+ if (o.Has("truncation")) {
424
+ NSString* t = ToNSString(o.Get("truncation"));
425
+ if ([t isEqualToString:@"start"]) T.truncationMode = kCATruncationStart;
426
+ else if ([t isEqualToString:@"end"]) T.truncationMode = kCATruncationEnd;
427
+ else if ([t isEqualToString:@"middle"]) T.truncationMode = kCATruncationMiddle;
428
+ else T.truncationMode = kCATruncationNone;
429
+ }
430
+ return info.Env().Undefined();
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // CAGradientLayer
435
+ // ---------------------------------------------------------------------------
436
+
437
+ static Napi::Value SetGradientProps(const Napi::CallbackInfo& info) {
438
+ CAGradientLayer* G = (CAGradientLayer*)Deref<CALayer*>(info[0]);
439
+ Napi::Object o = info[1].As<Napi::Object>();
440
+ if (o.Has("colors")) {
441
+ Napi::Array arr = o.Get("colors").As<Napi::Array>();
442
+ NSMutableArray* colors = [NSMutableArray arrayWithCapacity:arr.Length()];
443
+ for (uint32_t i = 0; i < arr.Length(); i++) {
444
+ [colors addObject:CFBridgingRelease(MakeColor(arr.Get(i)))];
445
+ }
446
+ G.colors = colors;
447
+ }
448
+ if (o.Has("locations")) {
449
+ Napi::Array arr = o.Get("locations").As<Napi::Array>();
450
+ NSMutableArray* locs = [NSMutableArray arrayWithCapacity:arr.Length()];
451
+ for (uint32_t i = 0; i < arr.Length(); i++) {
452
+ [locs addObject:@(arr.Get(i).As<Napi::Number>().DoubleValue())];
453
+ }
454
+ G.locations = locs;
455
+ }
456
+ if (o.Has("startPoint")) G.startPoint = PointFrom(o.Get("startPoint"));
457
+ if (o.Has("endPoint")) G.endPoint = PointFrom(o.Get("endPoint"));
458
+ if (o.Has("type")) {
459
+ NSString* t = ToNSString(o.Get("type"));
460
+ if ([t isEqualToString:@"radial"]) G.type = kCAGradientLayerRadial;
461
+ else if ([t isEqualToString:@"conic"]) G.type = kCAGradientLayerConic;
462
+ else G.type = kCAGradientLayerAxial;
463
+ }
464
+ return info.Env().Undefined();
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // CAShapeLayer
469
+ // ---------------------------------------------------------------------------
470
+
471
+ static CGPathRef BuildPath(Napi::Array ops) {
472
+ CGMutablePathRef p = CGPathCreateMutable();
473
+ for (uint32_t i = 0; i < ops.Length(); i++) {
474
+ Napi::Array op = ops.Get(i).As<Napi::Array>();
475
+ std::string cmd = op.Get(0u).As<Napi::String>().Utf8Value();
476
+ auto n = [&](uint32_t idx) { return op.Get(idx).As<Napi::Number>().DoubleValue(); };
477
+ if (cmd == "move") CGPathMoveToPoint(p, NULL, n(1), n(2));
478
+ else if (cmd == "line") CGPathAddLineToPoint(p, NULL, n(1), n(2));
479
+ else if (cmd == "curve") CGPathAddCurveToPoint(p, NULL, n(1), n(2), n(3), n(4), n(5), n(6));
480
+ else if (cmd == "quad") CGPathAddQuadCurveToPoint(p, NULL, n(1), n(2), n(3), n(4));
481
+ else if (cmd == "arc") CGPathAddArc(p, NULL, n(1), n(2), n(3), n(4), n(5),
482
+ op.Length() > 6 && op.Get(6u).As<Napi::Boolean>().Value());
483
+ else if (cmd == "rect") CGPathAddRect(p, NULL, CGRectMake(n(1), n(2), n(3), n(4)));
484
+ else if (cmd == "ellipse") CGPathAddEllipseInRect(p, NULL, CGRectMake(n(1), n(2), n(3), n(4)));
485
+ else if (cmd == "roundRect") CGPathAddRoundedRect(p, NULL, CGRectMake(n(1), n(2), n(3), n(4)), n(5), n(5));
486
+ else if (cmd == "close") CGPathCloseSubpath(p);
487
+ }
488
+ return p;
489
+ }
490
+
491
+ static Napi::Value SetShapeProps(const Napi::CallbackInfo& info) {
492
+ CAShapeLayer* S = (CAShapeLayer*)Deref<CALayer*>(info[0]);
493
+ Napi::Object o = info[1].As<Napi::Object>();
494
+ if (o.Has("path")) {
495
+ CGPathRef p = BuildPath(o.Get("path").As<Napi::Array>());
496
+ S.path = p;
497
+ CGPathRelease(p);
498
+ }
499
+ SetColorProp(S, o, "fillColor", ^(CGColorRef c) { S.fillColor = c; });
500
+ SetColorProp(S, o, "strokeColor", ^(CGColorRef c) { S.strokeColor = c; });
501
+ if (o.Has("lineWidth")) S.lineWidth = NumOr(o, "lineWidth", 1);
502
+ if (o.Has("strokeStart")) S.strokeStart = NumOr(o, "strokeStart", 0);
503
+ if (o.Has("strokeEnd")) S.strokeEnd = NumOr(o, "strokeEnd", 1);
504
+ if (o.Has("lineCap")) {
505
+ NSString* c = ToNSString(o.Get("lineCap"));
506
+ if ([c isEqualToString:@"round"]) S.lineCap = kCALineCapRound;
507
+ else if ([c isEqualToString:@"square"]) S.lineCap = kCALineCapSquare;
508
+ else S.lineCap = kCALineCapButt;
509
+ }
510
+ if (o.Has("lineDashPattern")) {
511
+ Napi::Array arr = o.Get("lineDashPattern").As<Napi::Array>();
512
+ NSMutableArray* d = [NSMutableArray arrayWithCapacity:arr.Length()];
513
+ for (uint32_t i = 0; i < arr.Length(); i++)
514
+ [d addObject:@(arr.Get(i).As<Napi::Number>().DoubleValue())];
515
+ S.lineDashPattern = d;
516
+ }
517
+ if (o.Has("fillRule")) {
518
+ S.fillRule = [ToNSString(o.Get("fillRule")) isEqualToString:@"evenodd"]
519
+ ? kCAFillRuleEvenOdd : kCAFillRuleNonZero;
520
+ }
521
+ return info.Env().Undefined();
522
+ }
523
+
524
+ // ---------------------------------------------------------------------------
525
+ // animations & transactions
526
+ // ---------------------------------------------------------------------------
527
+
528
+ static id AnimValue(Napi::Value v) {
529
+ if (v.IsNumber()) return @(v.As<Napi::Number>().DoubleValue());
530
+ if (v.IsArray()) {
531
+ Napi::Array a = v.As<Napi::Array>();
532
+ if (a.Length() == 2) {
533
+ return [NSValue valueWithPoint:NSMakePoint(a.Get(0u).As<Napi::Number>().DoubleValue(),
534
+ a.Get(1u).As<Napi::Number>().DoubleValue())];
535
+ }
536
+ if (a.Length() >= 3) return CFBridgingRelease(MakeColor(v)); // color
537
+ }
538
+ return nil;
539
+ }
540
+
541
+ static CAMediaTimingFunction* TimingFn(NSString* name) {
542
+ if ([name isEqualToString:@"linear"]) return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
543
+ if ([name isEqualToString:@"easeIn"]) return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
544
+ if ([name isEqualToString:@"easeOut"]) return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
545
+ if ([name isEqualToString:@"easeInEaseOut"]) return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
546
+ return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
547
+ }
548
+
549
+ // addAnimation(layer, keyPath, opts, key)
550
+ static Napi::Value AddAnimation(const Napi::CallbackInfo& info) {
551
+ CALayer* L = Deref<CALayer*>(info[0]);
552
+ NSString* keyPath = ToNSString(info[1]);
553
+ Napi::Object o = info[2].As<Napi::Object>();
554
+ NSString* key = info.Length() > 3 && info[3].IsString() ? ToNSString(info[3]) : keyPath;
555
+
556
+ CABasicAnimation* a = [CABasicAnimation animationWithKeyPath:keyPath];
557
+ if (o.Has("from")) a.fromValue = AnimValue(o.Get("from"));
558
+ if (o.Has("to")) a.toValue = AnimValue(o.Get("to"));
559
+ a.duration = NumOr(o, "duration", 0.25);
560
+ double rep = NumOr(o, "repeat", 0);
561
+ if (rep > 0) a.repeatCount = std::isinf(rep) ? HUGE_VALF : (float)rep;
562
+ a.autoreverses = BoolOr(o, "autoreverse", false);
563
+ if (o.Has("timing")) a.timingFunction = TimingFn(ToNSString(o.Get("timing")));
564
+ if (BoolOr(o, "hold", false)) {
565
+ a.removedOnCompletion = NO;
566
+ a.fillMode = kCAFillModeForwards;
567
+ }
568
+ [L addAnimation:a forKey:key];
569
+ return info.Env().Undefined();
570
+ }
571
+
572
+ static Napi::Value RemoveAnimation(const Napi::CallbackInfo& info) {
573
+ CALayer* L = Deref<CALayer*>(info[0]);
574
+ [L removeAnimationForKey:ToNSString(info[1])];
575
+ return info.Env().Undefined();
576
+ }
577
+
578
+ static Napi::Value RemoveAllAnimations(const Napi::CallbackInfo& info) {
579
+ CALayer* L = Deref<CALayer*>(info[0]);
580
+ [L removeAllAnimations];
581
+ return info.Env().Undefined();
582
+ }
583
+
584
+ static Napi::Value TxBegin(const Napi::CallbackInfo& info) {
585
+ [CATransaction begin];
586
+ if (info.Length() > 0 && info[0].IsObject()) {
587
+ Napi::Object o = info[0].As<Napi::Object>();
588
+ if (o.Has("duration")) [CATransaction setAnimationDuration:NumOr(o, "duration", 0.25)];
589
+ if (BoolOr(o, "disableActions", false)) [CATransaction setDisableActions:YES];
590
+ if (o.Has("timing")) [CATransaction setAnimationTimingFunction:TimingFn(ToNSString(o.Get("timing")))];
591
+ }
592
+ return info.Env().Undefined();
593
+ }
594
+
595
+ static Napi::Value TxCommit(const Napi::CallbackInfo& info) {
596
+ [CATransaction commit];
597
+ return info.Env().Undefined();
598
+ }
599
+
600
+ // ---------------------------------------------------------------------------
601
+ // hit testing
602
+ // ---------------------------------------------------------------------------
603
+
604
+ static Napi::Value HitTest(const Napi::CallbackInfo& info) {
605
+ Napi::Env env = info.Env();
606
+ CALayer* root = Deref<CALayer*>(info[0]);
607
+ double x = info[1].As<Napi::Number>().DoubleValue();
608
+ double y = info[2].As<Napi::Number>().DoubleValue();
609
+ // hitTest: takes the point in the receiver's superlayer space, which stays
610
+ // bottom-up even when geometryFlipped flips the sublayer layout.
611
+ if (root.geometryFlipped) y = CGRectGetHeight(root.bounds) - y;
612
+ CALayer* hit = [root hitTest:CGPointMake(x, y)];
613
+ if (hit && hit.name) return Napi::String::New(env, hit.name.UTF8String);
614
+ return env.Null();
615
+ }
616
+
617
+ // ---------------------------------------------------------------------------
618
+ // CoreText: measure + render to CGImage
619
+ // ---------------------------------------------------------------------------
620
+
621
+ static NSAttributedString* AttrString(Napi::Object o, CGColorRef* outColor) {
622
+ NSString* text = StrOr(o, "text", @"");
623
+ NSString* fontName = StrOr(o, "fontName", @"Helvetica");
624
+ double fontSize = NumOr(o, "fontSize", 14);
625
+ CGColorRef color = o.Has("color") ? MakeColor(o.Get("color"))
626
+ : CGColorCreateGenericRGB(0, 0, 0, 1);
627
+ CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)fontName, fontSize, NULL);
628
+ NSDictionary* attrs = @{
629
+ (__bridge id)kCTFontAttributeName : (__bridge id)font,
630
+ (__bridge id)kCTForegroundColorAttributeName : (__bridge id)color,
631
+ };
632
+ NSAttributedString* as = [[NSAttributedString alloc] initWithString:text attributes:attrs];
633
+ CFRelease(font);
634
+ *outColor = color; // caller releases
635
+ return as;
636
+ }
637
+
638
+ static Napi::Value MeasureText(const Napi::CallbackInfo& info) {
639
+ Napi::Env env = info.Env();
640
+ CGColorRef color;
641
+ NSAttributedString* as = AttrString(info[0].As<Napi::Object>(), &color);
642
+ CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)as);
643
+ CGFloat ascent, descent, leading;
644
+ double width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
645
+ CFRelease(line);
646
+ CGColorRelease(color);
647
+ Napi::Object r = Napi::Object::New(env);
648
+ r.Set("width", width);
649
+ r.Set("ascent", ascent);
650
+ r.Set("descent", descent);
651
+ r.Set("leading", leading);
652
+ return r;
653
+ }
654
+
655
+ // createTextImage({text, fontName, fontSize, color, maxWidth, scale})
656
+ // -> { image: External<CGImage>, width, height, scale } (width/height in points)
657
+ static Napi::Value CreateTextImage(const Napi::CallbackInfo& info) {
658
+ Napi::Env env = info.Env();
659
+ Napi::Object o = info[0].As<Napi::Object>();
660
+ double scale = NumOr(o, "scale", 2);
661
+ double maxWidth = NumOr(o, "maxWidth", 100000);
662
+
663
+ CGColorRef color;
664
+ NSAttributedString* as = AttrString(o, &color);
665
+ CTFramesetterRef fs = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)as);
666
+ CFRange fit;
667
+ CGSize sz = CTFramesetterSuggestFrameSizeWithConstraints(
668
+ fs, CFRangeMake(0, 0), NULL, CGSizeMake(maxWidth, CGFLOAT_MAX), &fit);
669
+ double wpt = ceil(sz.width) + 1, hpt = ceil(sz.height) + 1;
670
+ size_t pw = (size_t)ceil(wpt * scale), ph = (size_t)ceil(hpt * scale);
671
+
672
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
673
+ CGContextRef ctx = CGBitmapContextCreate(
674
+ NULL, pw, ph, 8, 0, cs,
675
+ kCGImageAlphaPremultipliedFirst | (CGBitmapInfo)kCGBitmapByteOrder32Host);
676
+ CGContextScaleCTM(ctx, scale, scale);
677
+
678
+ CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, wpt, hpt), NULL);
679
+ CTFrameRef frame = CTFramesetterCreateFrame(fs, CFRangeMake(0, 0), path, NULL);
680
+ CTFrameDraw(frame, ctx);
681
+ CGImageRef img = CGBitmapContextCreateImage(ctx);
682
+
683
+ CFRelease(frame);
684
+ CGPathRelease(path);
685
+ CGContextRelease(ctx);
686
+ CGColorSpaceRelease(cs);
687
+ CFRelease(fs);
688
+ CGColorRelease(color);
689
+
690
+ Napi::Object r = Napi::Object::New(env);
691
+ r.Set("image", Napi::External<void>::New(env, (void*)img, [](Napi::Env, void* d) {
692
+ CGImageRelease((CGImageRef)d);
693
+ }));
694
+ r.Set("width", wpt);
695
+ r.Set("height", hpt);
696
+ r.Set("scale", scale);
697
+ return r;
698
+ }
699
+
700
+ // setContentsImage(layer, imageExternal, contentsScale?)
701
+ static Napi::Value SetContentsImage(const Napi::CallbackInfo& info) {
702
+ CALayer* L = Deref<CALayer*>(info[0]);
703
+ CGImageRef img = (CGImageRef)info[1].As<Napi::External<void>>().Data();
704
+ L.contents = (__bridge id)img;
705
+ if (info.Length() > 2 && info[2].IsNumber())
706
+ L.contentsScale = info[2].As<Napi::Number>().DoubleValue();
707
+ return info.Env().Undefined();
708
+ }
709
+
710
+ // ---------------------------------------------------------------------------
711
+ // native controls: NSCell rendered offscreen (the WebKit/Firefox technique)
712
+ // ---------------------------------------------------------------------------
713
+
714
+ static NSView* DummyDrawView() {
715
+ // Cells only use the view for flippedness/appearance queries; it never needs
716
+ // to be in a window.
717
+ static CALHostView* v = nil;
718
+ if (!v) v = [[CALHostView alloc] initWithFrame:NSMakeRect(0, 0, 1000, 1000)];
719
+ return v;
720
+ }
721
+
722
+ // drawControl({kind, title, state, pressed, enabled, isDefault, value,
723
+ // controlSize, appearance, width, height, scale})
724
+ // kind: 'push' | 'checkbox' | 'radio' | 'popup' | 'slider'
725
+ // -> { image: External<CGImage>, width, height, scale } (points)
726
+ // width/height default to the cell's natural cellSize (slider must pass them).
727
+ static Napi::Value DrawControl(const Napi::CallbackInfo& info) {
728
+ Napi::Env env = info.Env();
729
+ EnsureApp();
730
+ Napi::Object o = info[0].As<Napi::Object>();
731
+ NSString* kind = StrOr(o, "kind", @"push");
732
+ NSString* title = StrOr(o, "title", @"");
733
+ double scale = NumOr(o, "scale", 2);
734
+ bool pressed = BoolOr(o, "pressed", false);
735
+ bool enabled = BoolOr(o, "enabled", true);
736
+ int state = (int)NumOr(o, "state", 0); // 0 off, 1 on
737
+
738
+ NSCell* cell = nil;
739
+ if ([kind isEqualToString:@"checkbox"] || [kind isEqualToString:@"radio"] ||
740
+ [kind isEqualToString:@"push"]) {
741
+ NSButtonCell* c = [[NSButtonCell alloc] initTextCell:title];
742
+ if ([kind isEqualToString:@"checkbox"]) {
743
+ c.buttonType = NSButtonTypeSwitch;
744
+ } else if ([kind isEqualToString:@"radio"]) {
745
+ c.buttonType = NSButtonTypeRadio;
746
+ } else {
747
+ c.buttonType = NSButtonTypeMomentaryPushIn;
748
+ c.bezelStyle = NSBezelStylePush;
749
+ if (BoolOr(o, "isDefault", false)) c.keyEquivalent = @"\r"; // accent fill
750
+ }
751
+ c.state = state == 1 ? NSControlStateValueOn : NSControlStateValueOff;
752
+ cell = c;
753
+ } else if ([kind isEqualToString:@"popup"]) {
754
+ NSPopUpButtonCell* c = [[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO];
755
+ [c addItemWithTitle:title];
756
+ cell = c;
757
+ }
758
+
759
+ // Controls whose cells no longer draw offscreen (NSSliderCell renders via
760
+ // the view's layer machinery) or that have no cell at all (NSSwitch): use a
761
+ // real offscreen NSControl and displayRectIgnoringOpacity:inContext:.
762
+ NSControl* viewControl = nil;
763
+ if ([kind isEqualToString:@"slider"]) {
764
+ NSSlider* s = [[NSSlider alloc] init];
765
+ s.minValue = 0;
766
+ s.maxValue = 1;
767
+ s.doubleValue = NumOr(o, "value", 0.5);
768
+ viewControl = s;
769
+ } else if ([kind isEqualToString:@"switch"]) {
770
+ NSSwitch* s = [[NSSwitch alloc] init];
771
+ s.state = state == 1 ? NSControlStateValueOn : NSControlStateValueOff;
772
+ viewControl = s;
773
+ }
774
+
775
+ if (!cell && !viewControl) {
776
+ Napi::Error::New(env, "unknown control kind").ThrowAsJavaScriptException();
777
+ return env.Undefined();
778
+ }
779
+
780
+ NSString* sz = StrOr(o, "controlSize", @"regular");
781
+ NSControlSize csize = NSControlSizeRegular;
782
+ if ([sz isEqualToString:@"small"]) csize = NSControlSizeSmall;
783
+ else if ([sz isEqualToString:@"mini"]) csize = NSControlSizeMini;
784
+ else if ([sz isEqualToString:@"large"]) csize = NSControlSizeLarge;
785
+
786
+ double w = NumOr(o, "width", 0), h = NumOr(o, "height", 0);
787
+ if (cell) {
788
+ cell.controlSize = csize;
789
+ cell.font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:csize]];
790
+ cell.enabled = enabled;
791
+ cell.highlighted = pressed;
792
+ NSSize natural = cell.cellSize;
793
+ if (w <= 0) w = ceil(natural.width);
794
+ if (h <= 0) h = ceil(natural.height);
795
+ } else {
796
+ viewControl.controlSize = csize;
797
+ viewControl.enabled = enabled;
798
+ NSSize natural = viewControl.intrinsicContentSize;
799
+ if (w <= 0) w = natural.width > 0 ? ceil(natural.width) : 100;
800
+ if (h <= 0) h = natural.height > 0 ? ceil(natural.height) : 22;
801
+ }
802
+
803
+ size_t pw = (size_t)ceil(w * scale), ph = (size_t)ceil(h * scale);
804
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
805
+ CGContextRef ctx = CGBitmapContextCreate(
806
+ NULL, pw, ph, 8, 0, cs,
807
+ kCGImageAlphaPremultipliedFirst | (CGBitmapInfo)kCGBitmapByteOrder32Host);
808
+ CGContextScaleCTM(ctx, scale, scale);
809
+ // NSGraphicsContext flipped:YES expects a CTM that already puts the origin
810
+ // at the top-left.
811
+ CGContextTranslateCTM(ctx, 0, h);
812
+ CGContextScaleCTM(ctx, 1, -1);
813
+
814
+ NSGraphicsContext* g = [NSGraphicsContext graphicsContextWithCGContext:ctx flipped:YES];
815
+ [NSGraphicsContext saveGraphicsState];
816
+ [NSGraphicsContext setCurrentContext:g];
817
+
818
+ NSString* apName = StrOr(o, "appearance", @"system");
819
+ NSAppearance* ap = NSApp.effectiveAppearance;
820
+ if ([apName isEqualToString:@"dark"]) ap = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
821
+ else if ([apName isEqualToString:@"light"]) ap = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
822
+
823
+ if (viewControl) {
824
+ viewControl.frame = NSMakeRect(0, 0, w, h);
825
+ viewControl.appearance = ap;
826
+ [viewControl layoutSubtreeIfNeeded];
827
+ [viewControl displayRectIgnoringOpacity:viewControl.bounds inContext:g];
828
+ } else {
829
+ [ap performAsCurrentDrawingAppearance:^{
830
+ [cell drawWithFrame:NSMakeRect(0, 0, w, h) inView:DummyDrawView()];
831
+ }];
832
+ }
833
+
834
+ [NSGraphicsContext restoreGraphicsState];
835
+ CGImageRef img = CGBitmapContextCreateImage(ctx);
836
+ CGContextRelease(ctx);
837
+ CGColorSpaceRelease(cs);
838
+
839
+ Napi::Object r = Napi::Object::New(env);
840
+ r.Set("image", Napi::External<void>::New(env, (void*)img, [](Napi::Env, void* d) {
841
+ CGImageRelease((CGImageRef)d);
842
+ }));
843
+ r.Set("width", w);
844
+ r.Set("height", h);
845
+ r.Set("scale", scale);
846
+ return r;
847
+ }
848
+
849
+ // setLayerContentsIOSurface(layer, iosurfaceId) — the receiving end of an
850
+ // IOSurface render target (x11-dri's appleCreateTarget): the id is process-
851
+ // global, so the GPU addon and this one never share a pointer. The layer
852
+ // retains the surface; our lookup reference is dropped immediately.
853
+ static Napi::Value SetLayerContentsIOSurface(const Napi::CallbackInfo& info) {
854
+ Napi::Env env = info.Env();
855
+ CALayer* L = Deref<CALayer*>(info[0]);
856
+ uint32_t sid = info[1].As<Napi::Number>().Uint32Value();
857
+ IOSurfaceRef surface = IOSurfaceLookup(sid);
858
+ if (!surface) {
859
+ Napi::Error::New(env, "IOSurfaceLookup: no surface with that id")
860
+ .ThrowAsJavaScriptException();
861
+ return env.Undefined();
862
+ }
863
+ // its own transaction, actions off: a present is a buffer flip, and the
864
+ // implicit action for `contents` would turn it into a crossfade
865
+ [CATransaction begin];
866
+ [CATransaction setDisableActions:YES];
867
+ L.contents = (__bridge id)surface;
868
+ [CATransaction commit];
869
+ CFRelease(surface);
870
+ return env.Undefined();
871
+ }
872
+
873
+ static Napi::Value AppearanceIsDark(const Napi::CallbackInfo& info) {
874
+ EnsureApp();
875
+ NSAppearanceName n = [NSApp.effectiveAppearance
876
+ bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]];
877
+ return Napi::Boolean::New(info.Env(), [n isEqualToString:NSAppearanceNameDarkAqua]);
878
+ }
879
+
880
+ // ---------------------------------------------------------------------------
881
+ // snapshot (renderInContext -> PNG) — for debugging / headless verification
882
+ // ---------------------------------------------------------------------------
883
+
884
+ static Napi::Value SnapshotWindow(const Napi::CallbackInfo& info) {
885
+ Napi::Env env = info.Env();
886
+ NSWindow* win = Deref<NSWindow*>(info[0]);
887
+ NSString* path = ToNSString(info[1]);
888
+
889
+ // Capture our own window's real composited pixels (allowed without the
890
+ // screen-recording permission for windows the process owns). This shows the
891
+ // true WindowServer output, including geometryFlipped, masks, and shadows.
892
+ #pragma clang diagnostic push
893
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
894
+ bool withShadow = info.Length() > 2 && info[2].ToBoolean().Value();
895
+ CGImageRef img = CGWindowListCreateImage(
896
+ CGRectNull, kCGWindowListOptionIncludingWindow, (CGWindowID)win.windowNumber,
897
+ withShadow
898
+ ? (CGWindowImageOption)kCGWindowImageBestResolution
899
+ : (CGWindowImageOption)(kCGWindowImageBoundsIgnoreFraming |
900
+ kCGWindowImageBestResolution));
901
+ #pragma clang diagnostic pop
902
+ if (!img) return Napi::Boolean::New(env, false);
903
+
904
+ NSURL* url = [NSURL fileURLWithPath:path];
905
+ CGImageDestinationRef dst =
906
+ CGImageDestinationCreateWithURL((__bridge CFURLRef)url, CFSTR("public.png"), 1, NULL);
907
+ bool ok = false;
908
+ if (dst) {
909
+ CGImageDestinationAddImage(dst, img, NULL);
910
+ ok = CGImageDestinationFinalize(dst);
911
+ CFRelease(dst);
912
+ }
913
+ CGImageRelease(img);
914
+ return Napi::Boolean::New(env, ok);
915
+ }
916
+
917
+ // ---------------------------------------------------------------------------
918
+ // module init
919
+ // ---------------------------------------------------------------------------
920
+
921
+ // src/backend.mm — the react-x11 backend surface (windows with delegates,
922
+ // enriched events, CG surfaces, CoreText layouts, pasteboard, screens).
923
+ void InitBackend(Napi::Env env, Napi::Object exports);
924
+
925
+ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
926
+ #define FN(js, fn) exports.Set(js, Napi::Function::New(env, fn))
927
+ FN("initApp", InitApp);
928
+ FN("pump", Pump);
929
+ FN("setEventCallback", SetEventCallback);
930
+ FN("postMouseEvent", PostMouseEvent);
931
+ FN("createWindow", CreateWindowFn);
932
+ FN("windowRootLayer", WindowRootLayer);
933
+ FN("windowScale", WindowScale);
934
+ FN("windowContentSize", WindowContentSize);
935
+ FN("windowIsVisible", WindowIsVisible);
936
+ FN("windowNumber", WindowNumber);
937
+ FN("closeWindow", CloseWindow);
938
+ FN("snapshotWindow", SnapshotWindow);
939
+ FN("createLayer", CreateLayer);
940
+ FN("createTextLayer", CreateTextLayer);
941
+ FN("createGradientLayer", CreateGradientLayer);
942
+ FN("createShapeLayer", CreateShapeLayer);
943
+ FN("addSublayer", AddSublayer);
944
+ FN("removeFromSuperlayer", RemoveFromSuperlayer);
945
+ FN("setLayerProps", SetLayerProps);
946
+ FN("setTextProps", SetTextProps);
947
+ FN("setGradientProps", SetGradientProps);
948
+ FN("setShapeProps", SetShapeProps);
949
+ FN("addAnimation", AddAnimation);
950
+ FN("removeAnimation", RemoveAnimation);
951
+ FN("removeAllAnimations", RemoveAllAnimations);
952
+ FN("txBegin", TxBegin);
953
+ FN("txCommit", TxCommit);
954
+ FN("hitTest", HitTest);
955
+ FN("measureText", MeasureText);
956
+ FN("createTextImage", CreateTextImage);
957
+ FN("setContentsImage", SetContentsImage);
958
+ FN("setLayerContentsIOSurface", SetLayerContentsIOSurface);
959
+ FN("drawControl", DrawControl);
960
+ FN("appearanceIsDark", AppearanceIsDark);
961
+ #undef FN
962
+ InitBackend(env, exports);
963
+ return exports;
964
+ }
965
+
966
+ NODE_API_MODULE(calayers, Init)