@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/LICENSE +19 -0
- package/README.md +192 -0
- package/binding.gyp +27 -0
- package/index.js +184 -0
- package/package.json +57 -0
- package/prebuilds/darwin-arm64/calayers.node +0 -0
- package/prebuilds/darwin-x64/calayers.node +0 -0
- package/scripts/install.js +54 -0
- package/src/addon.mm +966 -0
- package/src/backend.mm +2512 -0
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)
|