@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/backend.mm
ADDED
|
@@ -0,0 +1,2512 @@
|
|
|
1
|
+
// @windowkit/appkit backend.mm — the surface the react-x11 Cocoa backend consumes.
|
|
2
|
+
//
|
|
3
|
+
// Everything here is mechanism, no policy: windows with delegates and
|
|
4
|
+
// per-window event routing, an enriched event pump, CoreGraphics bitmap
|
|
5
|
+
// surfaces with a canvas-shaped drawing API, a CoreText layout engine
|
|
6
|
+
// (measure + draw + caret/hit geometry), pasteboard text, screen lists and
|
|
7
|
+
// cursors. The retained-layer API stays in addon.mm; this file is what a
|
|
8
|
+
// renderer paints and listens through.
|
|
9
|
+
//
|
|
10
|
+
// Coordinate rule, stated once: every point that crosses this boundary is
|
|
11
|
+
// TOP-LEFT origin. Window frames and screen rects are top-left in global
|
|
12
|
+
// coordinates (y grows down from the top of the primary screen); event
|
|
13
|
+
// positions are top-left in the window's content view; surfaces are y-down
|
|
14
|
+
// like a canvas. The flips against Cocoa's bottom-up world happen here and
|
|
15
|
+
// nowhere else.
|
|
16
|
+
|
|
17
|
+
#include <napi.h>
|
|
18
|
+
#import <Cocoa/Cocoa.h>
|
|
19
|
+
#import <QuartzCore/QuartzCore.h>
|
|
20
|
+
#import <CoreText/CoreText.h>
|
|
21
|
+
#import <IOSurface/IOSurface.h>
|
|
22
|
+
#include <objc/runtime.h>
|
|
23
|
+
|
|
24
|
+
#include <cmath>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
28
|
+
// --- helpers (self-contained: addon.mm keeps its own copies) ---------------
|
|
29
|
+
|
|
30
|
+
static NSString* BToNSString(Napi::Value v) {
|
|
31
|
+
std::string s = v.As<Napi::String>().Utf8Value();
|
|
32
|
+
return [NSString stringWithUTF8String:s.c_str()];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static double BNumOr(Napi::Object o, const char* k, double d) {
|
|
36
|
+
if (!o.Has(k)) return d;
|
|
37
|
+
Napi::Value v = o.Get(k);
|
|
38
|
+
return v.IsNumber() ? v.As<Napi::Number>().DoubleValue() : d;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static bool BBoolOr(Napi::Object o, const char* k, bool d) {
|
|
42
|
+
if (!o.Has(k)) return d;
|
|
43
|
+
Napi::Value v = o.Get(k);
|
|
44
|
+
return v.IsBoolean() ? v.As<Napi::Boolean>().Value() : d;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
template <typename T>
|
|
48
|
+
static T BDeref(Napi::Value v) {
|
|
49
|
+
return (__bridge T)(v.As<Napi::External<void>>().Data());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static Napi::Value BWrapRetained(Napi::Env env, id obj) {
|
|
53
|
+
void* p = (void*)CFBridgingRetain(obj);
|
|
54
|
+
return Napi::External<void>::New(env, p,
|
|
55
|
+
[](Napi::Env, void* d) { CFRelease(d); });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// [r,g,b,a] 0..1 -> CGColor (caller releases)
|
|
59
|
+
static CGColorRef BMakeColor(Napi::Value v) {
|
|
60
|
+
Napi::Array a = v.As<Napi::Array>();
|
|
61
|
+
double r = a.Get(0u).As<Napi::Number>().DoubleValue();
|
|
62
|
+
double g = a.Get(1u).As<Napi::Number>().DoubleValue();
|
|
63
|
+
double b = a.Get(2u).As<Napi::Number>().DoubleValue();
|
|
64
|
+
double al =
|
|
65
|
+
a.Length() > 3 ? a.Get(3u).As<Napi::Number>().DoubleValue() : 1.0;
|
|
66
|
+
return CGColorCreateSRGB(r, g, b, al);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static void BEnsureApp() {
|
|
70
|
+
static bool inited = false;
|
|
71
|
+
if (inited) return;
|
|
72
|
+
@autoreleasepool {
|
|
73
|
+
[NSApplication sharedApplication];
|
|
74
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
75
|
+
[NSApp finishLaunching];
|
|
76
|
+
}
|
|
77
|
+
inited = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The top of the primary screen, for global coordinate flips. The primary
|
|
81
|
+
// screen is the one whose Cocoa frame origin is (0,0); its top edge is the
|
|
82
|
+
// global top-left origin's y=0.
|
|
83
|
+
static CGFloat PrimaryScreenTop() {
|
|
84
|
+
NSScreen* primary = NSScreen.screens.firstObject;
|
|
85
|
+
return primary ? NSMaxY(primary.frame) : 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// the event callback (backend flavour — richer payloads than addon.mm's)
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
static Napi::FunctionReference gBackendCb;
|
|
93
|
+
// Re-entrancy guard: delegate methods fire inside [NSApp sendEvent:] (live
|
|
94
|
+
// resize, window moves), and each call into JS may pump more native work.
|
|
95
|
+
// The guard only protects against dispatching with no callback installed.
|
|
96
|
+
static bool HasBackendCb() { return !gBackendCb.IsEmpty(); }
|
|
97
|
+
|
|
98
|
+
static void EmitToJS(Napi::Env env, Napi::Object ev) {
|
|
99
|
+
if (!HasBackendCb()) return;
|
|
100
|
+
gBackendCb.Call({ev});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Window bookkeeping: delegate + view need to reach the JS callback with the
|
|
104
|
+
// window's number attached, and windowShouldClose needs to answer NO while
|
|
105
|
+
// telling JS. One delegate class serves every window.
|
|
106
|
+
|
|
107
|
+
@interface CALBackendDelegate : NSObject <NSWindowDelegate> {
|
|
108
|
+
@public
|
|
109
|
+
napi_env env_;
|
|
110
|
+
}
|
|
111
|
+
@end
|
|
112
|
+
|
|
113
|
+
static Napi::Object WindowEvent(Napi::Env env, NSWindow* win,
|
|
114
|
+
const char* type) {
|
|
115
|
+
Napi::Object ev = Napi::Object::New(env);
|
|
116
|
+
ev.Set("type", type);
|
|
117
|
+
ev.Set("windowNumber", (double)win.windowNumber);
|
|
118
|
+
return ev;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static void EmitWindowGeometry(Napi::Env env, NSWindow* win, const char* type,
|
|
122
|
+
bool live) {
|
|
123
|
+
if (!HasBackendCb()) return;
|
|
124
|
+
Napi::HandleScope scope(env);
|
|
125
|
+
Napi::Object ev = WindowEvent(env, win, type);
|
|
126
|
+
NSRect content = [win contentRectForFrameRect:win.frame];
|
|
127
|
+
ev.Set("width", content.size.width);
|
|
128
|
+
ev.Set("height", content.size.height);
|
|
129
|
+
ev.Set("x", content.origin.x);
|
|
130
|
+
ev.Set("y", PrimaryScreenTop() - (content.origin.y + content.size.height));
|
|
131
|
+
ev.Set("live", live);
|
|
132
|
+
EmitToJS(env, ev);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@implementation CALBackendDelegate
|
|
136
|
+
- (void)windowDidResize:(NSNotification*)n {
|
|
137
|
+
NSWindow* win = n.object;
|
|
138
|
+
EmitWindowGeometry(Napi::Env(env_), win, "window-resize", win.inLiveResize);
|
|
139
|
+
}
|
|
140
|
+
- (void)windowDidMove:(NSNotification*)n {
|
|
141
|
+
EmitWindowGeometry(Napi::Env(env_), (NSWindow*)n.object, "window-move",
|
|
142
|
+
false);
|
|
143
|
+
}
|
|
144
|
+
- (BOOL)windowShouldClose:(NSWindow*)sender {
|
|
145
|
+
if (HasBackendCb()) {
|
|
146
|
+
Napi::Env env(env_);
|
|
147
|
+
Napi::HandleScope scope(env);
|
|
148
|
+
EmitToJS(env, WindowEvent(env, sender, "window-close-request"));
|
|
149
|
+
}
|
|
150
|
+
return NO; // closing is the renderer's decision, never AppKit's
|
|
151
|
+
}
|
|
152
|
+
- (void)windowDidBecomeKey:(NSNotification*)n {
|
|
153
|
+
Napi::Env env(env_);
|
|
154
|
+
Napi::HandleScope scope(env);
|
|
155
|
+
EmitToJS(env, WindowEvent(env, (NSWindow*)n.object, "window-focus"));
|
|
156
|
+
}
|
|
157
|
+
- (void)windowDidResignKey:(NSNotification*)n {
|
|
158
|
+
Napi::Env env(env_);
|
|
159
|
+
Napi::HandleScope scope(env);
|
|
160
|
+
EmitToJS(env, WindowEvent(env, (NSWindow*)n.object, "window-blur"));
|
|
161
|
+
}
|
|
162
|
+
- (void)windowDidChangeBackingProperties:(NSNotification*)n {
|
|
163
|
+
NSWindow* win = n.object;
|
|
164
|
+
Napi::Env env(env_);
|
|
165
|
+
Napi::HandleScope scope(env);
|
|
166
|
+
Napi::Object ev = WindowEvent(env, win, "window-scale");
|
|
167
|
+
ev.Set("scale", win.backingScaleFactor);
|
|
168
|
+
EmitToJS(env, ev);
|
|
169
|
+
}
|
|
170
|
+
@end
|
|
171
|
+
|
|
172
|
+
static char kDelegateKey;
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// the hosting view: flipped, layer-hosting, with a tracking area for
|
|
176
|
+
// enter/exit/moved even in non-key windows (menus are non-activating panels)
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
@interface CALBackendView : NSView {
|
|
180
|
+
@public
|
|
181
|
+
NSTrackingArea* tracking_;
|
|
182
|
+
}
|
|
183
|
+
@end
|
|
184
|
+
@implementation CALBackendView
|
|
185
|
+
- (BOOL)acceptsFirstResponder { return YES; }
|
|
186
|
+
- (BOOL)isFlipped { return YES; }
|
|
187
|
+
- (void)keyDown:(NSEvent*)event { (void)event; } // no beep; JS observes keys
|
|
188
|
+
- (void)updateTrackingAreas {
|
|
189
|
+
[super updateTrackingAreas];
|
|
190
|
+
if (tracking_) [self removeTrackingArea:tracking_];
|
|
191
|
+
tracking_ = [[NSTrackingArea alloc]
|
|
192
|
+
initWithRect:NSZeroRect
|
|
193
|
+
options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
|
|
194
|
+
NSTrackingActiveAlways | NSTrackingInVisibleRect)
|
|
195
|
+
owner:self
|
|
196
|
+
userInfo:nil];
|
|
197
|
+
[self addTrackingArea:tracking_];
|
|
198
|
+
}
|
|
199
|
+
// First click on a non-key window should act, not just focus — a menu item
|
|
200
|
+
// in a panel, a button in an unfocused window. Every X11 app behaves so.
|
|
201
|
+
- (BOOL)acceptsFirstMouse:(NSEvent*)event { (void)event; return YES; }
|
|
202
|
+
@end
|
|
203
|
+
|
|
204
|
+
// A panel that can host popups without stealing key status from the owner
|
|
205
|
+
// window (menus, tooltips, dropdowns).
|
|
206
|
+
@interface CALBackendPanel : NSPanel
|
|
207
|
+
@end
|
|
208
|
+
@implementation CALBackendPanel
|
|
209
|
+
- (BOOL)canBecomeKeyWindow { return NO; }
|
|
210
|
+
- (BOOL)canBecomeMainWindow { return NO; }
|
|
211
|
+
@end
|
|
212
|
+
|
|
213
|
+
// A borderless window that can still take the keyboard (managed dialogs
|
|
214
|
+
// with decorations:false, plain toplevels drawn frameless).
|
|
215
|
+
@interface CALBackendKeyWindow : NSWindow
|
|
216
|
+
@end
|
|
217
|
+
@implementation CALBackendKeyWindow
|
|
218
|
+
- (BOOL)canBecomeKeyWindow { return YES; }
|
|
219
|
+
- (BOOL)canBecomeMainWindow { return YES; }
|
|
220
|
+
@end
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// createWindow2 / window management
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
// createWindow2({ width, height, // content size, points
|
|
227
|
+
// title, kind, // 'normal' | 'popup' | 'borderless'
|
|
228
|
+
// x, y, // top-left global, points (optional)
|
|
229
|
+
// resizable, opaque, hasShadow, level, // level: 'normal'|'popup'|'floating'
|
|
230
|
+
// backgroundColor }) // [r,g,b,a] or absent
|
|
231
|
+
static Napi::Value CreateWindow2(const Napi::CallbackInfo& info) {
|
|
232
|
+
Napi::Env env = info.Env();
|
|
233
|
+
BEnsureApp();
|
|
234
|
+
Napi::Object o = info[0].As<Napi::Object>();
|
|
235
|
+
double w = BNumOr(o, "width", 640), h = BNumOr(o, "height", 480);
|
|
236
|
+
std::string kind = o.Has("kind") && o.Get("kind").IsString()
|
|
237
|
+
? o.Get("kind").As<Napi::String>().Utf8Value()
|
|
238
|
+
: "normal";
|
|
239
|
+
bool resizable = BBoolOr(o, "resizable", true);
|
|
240
|
+
|
|
241
|
+
NSWindow* win;
|
|
242
|
+
@autoreleasepool {
|
|
243
|
+
NSRect rect = NSMakeRect(0, 0, w, h);
|
|
244
|
+
if (kind == "popup") {
|
|
245
|
+
win = [[CALBackendPanel alloc]
|
|
246
|
+
initWithContentRect:rect
|
|
247
|
+
styleMask:(NSWindowStyleMaskBorderless |
|
|
248
|
+
NSWindowStyleMaskNonactivatingPanel)
|
|
249
|
+
backing:NSBackingStoreBuffered
|
|
250
|
+
defer:NO];
|
|
251
|
+
win.level = NSPopUpMenuWindowLevel;
|
|
252
|
+
((NSPanel*)win).worksWhenModal = YES;
|
|
253
|
+
} else if (kind == "borderless") {
|
|
254
|
+
win = [[CALBackendKeyWindow alloc]
|
|
255
|
+
initWithContentRect:rect
|
|
256
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
257
|
+
backing:NSBackingStoreBuffered
|
|
258
|
+
defer:NO];
|
|
259
|
+
} else {
|
|
260
|
+
NSWindowStyleMask mask = NSWindowStyleMaskTitled |
|
|
261
|
+
NSWindowStyleMaskClosable |
|
|
262
|
+
NSWindowStyleMaskMiniaturizable;
|
|
263
|
+
if (resizable) mask |= NSWindowStyleMaskResizable;
|
|
264
|
+
win = [[NSWindow alloc] initWithContentRect:rect
|
|
265
|
+
styleMask:mask
|
|
266
|
+
backing:NSBackingStoreBuffered
|
|
267
|
+
defer:NO];
|
|
268
|
+
}
|
|
269
|
+
win.releasedWhenClosed = NO;
|
|
270
|
+
win.acceptsMouseMovedEvents = YES;
|
|
271
|
+
if (o.Has("title") && o.Get("title").IsString())
|
|
272
|
+
win.title = BToNSString(o.Get("title"));
|
|
273
|
+
if (o.Has("level") && o.Get("level").IsString()) {
|
|
274
|
+
std::string level = o.Get("level").As<Napi::String>().Utf8Value();
|
|
275
|
+
if (level == "popup") win.level = NSPopUpMenuWindowLevel;
|
|
276
|
+
else if (level == "floating") win.level = NSFloatingWindowLevel;
|
|
277
|
+
}
|
|
278
|
+
if (o.Has("hasShadow")) win.hasShadow = BBoolOr(o, "hasShadow", true);
|
|
279
|
+
if (o.Has("opaque")) {
|
|
280
|
+
win.opaque = BBoolOr(o, "opaque", true);
|
|
281
|
+
if (!win.opaque) win.backgroundColor = NSColor.clearColor;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
CALBackendView* view = [[CALBackendView alloc] initWithFrame:rect];
|
|
285
|
+
CALayer* root = [CALayer layer];
|
|
286
|
+
root.geometryFlipped = YES;
|
|
287
|
+
[view setLayer:root];
|
|
288
|
+
[view setWantsLayer:YES];
|
|
289
|
+
win.contentView = view;
|
|
290
|
+
root.contentsScale = win.backingScaleFactor;
|
|
291
|
+
if (o.Has("backgroundColor") && o.Get("backgroundColor").IsArray()) {
|
|
292
|
+
CGColorRef c = BMakeColor(o.Get("backgroundColor"));
|
|
293
|
+
root.backgroundColor = c;
|
|
294
|
+
CGColorRelease(c);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Placement: explicit top-left global coordinates, or centered.
|
|
298
|
+
if (o.Has("x") && o.Get("x").IsNumber() && o.Has("y") &&
|
|
299
|
+
o.Get("y").IsNumber()) {
|
|
300
|
+
double x = BNumOr(o, "x", 0), y = BNumOr(o, "y", 0);
|
|
301
|
+
NSRect frame = [win frameRectForContentRect:NSMakeRect(0, 0, w, h)];
|
|
302
|
+
CGFloat titlebar = frame.size.height - h;
|
|
303
|
+
// y is the CONTENT's top edge in top-left global coordinates.
|
|
304
|
+
[win setFrameOrigin:NSMakePoint(x, PrimaryScreenTop() - y - h)];
|
|
305
|
+
(void)titlebar;
|
|
306
|
+
} else {
|
|
307
|
+
[win center];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
CALBackendDelegate* delegate = [[CALBackendDelegate alloc] init];
|
|
311
|
+
delegate->env_ = (napi_env)env;
|
|
312
|
+
win.delegate = delegate;
|
|
313
|
+
objc_setAssociatedObject(win, &kDelegateKey, delegate,
|
|
314
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
315
|
+
[win makeFirstResponder:view];
|
|
316
|
+
}
|
|
317
|
+
return BWrapRetained(env, win);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// showWindow(win, activate) — map. Popups order front without activating.
|
|
321
|
+
static Napi::Value ShowWindowFn(const Napi::CallbackInfo& info) {
|
|
322
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
323
|
+
bool activate = info.Length() > 1 && info[1].ToBoolean().Value();
|
|
324
|
+
if (activate) {
|
|
325
|
+
[win makeKeyAndOrderFront:nil];
|
|
326
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
327
|
+
} else {
|
|
328
|
+
[win orderFrontRegardless];
|
|
329
|
+
}
|
|
330
|
+
return info.Env().Undefined();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static Napi::Value HideWindowFn(const Napi::CallbackInfo& info) {
|
|
334
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
335
|
+
[win orderOut:nil];
|
|
336
|
+
return info.Env().Undefined();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
static Napi::Value SetWindowTitle(const Napi::CallbackInfo& info) {
|
|
340
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
341
|
+
win.title = BToNSString(info[1]);
|
|
342
|
+
return info.Env().Undefined();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// setWindowFrame(win, x, y, w, h) — any argument may be null to keep it.
|
|
346
|
+
// x/y are the content's top-left in global top-left coordinates, points.
|
|
347
|
+
static Napi::Value SetWindowFrame(const Napi::CallbackInfo& info) {
|
|
348
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
349
|
+
NSRect content = [win contentRectForFrameRect:win.frame];
|
|
350
|
+
double topY = PrimaryScreenTop() - (content.origin.y + content.size.height);
|
|
351
|
+
double x = info[1].IsNumber() ? info[1].As<Napi::Number>().DoubleValue()
|
|
352
|
+
: content.origin.x;
|
|
353
|
+
double y = info[2].IsNumber() ? info[2].As<Napi::Number>().DoubleValue()
|
|
354
|
+
: topY;
|
|
355
|
+
double w = info[3].IsNumber() ? info[3].As<Napi::Number>().DoubleValue()
|
|
356
|
+
: content.size.width;
|
|
357
|
+
double h = info[4].IsNumber() ? info[4].As<Napi::Number>().DoubleValue()
|
|
358
|
+
: content.size.height;
|
|
359
|
+
NSRect newContent =
|
|
360
|
+
NSMakeRect(x, PrimaryScreenTop() - y - h, w, h);
|
|
361
|
+
[win setFrame:[win frameRectForContentRect:newContent] display:YES];
|
|
362
|
+
return info.Env().Undefined();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// -> { x, y, width, height, scale, visible, key } — content rect, top-left
|
|
366
|
+
// global coordinates, points.
|
|
367
|
+
static Napi::Value GetWindowFrame(const Napi::CallbackInfo& info) {
|
|
368
|
+
Napi::Env env = info.Env();
|
|
369
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
370
|
+
NSRect content = [win contentRectForFrameRect:win.frame];
|
|
371
|
+
Napi::Object r = Napi::Object::New(env);
|
|
372
|
+
r.Set("x", content.origin.x);
|
|
373
|
+
r.Set("y", PrimaryScreenTop() - (content.origin.y + content.size.height));
|
|
374
|
+
r.Set("width", content.size.width);
|
|
375
|
+
r.Set("height", content.size.height);
|
|
376
|
+
r.Set("scale", win.backingScaleFactor);
|
|
377
|
+
r.Set("visible", (bool)win.isVisible);
|
|
378
|
+
r.Set("key", (bool)win.isKeyWindow);
|
|
379
|
+
return r;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
static Napi::Value SetWindowMinMax(const Napi::CallbackInfo& info) {
|
|
383
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
384
|
+
Napi::Object o = info[1].As<Napi::Object>();
|
|
385
|
+
if (o.Has("minWidth") || o.Has("minHeight"))
|
|
386
|
+
win.contentMinSize =
|
|
387
|
+
NSMakeSize(BNumOr(o, "minWidth", 0), BNumOr(o, "minHeight", 0));
|
|
388
|
+
if (o.Has("maxWidth") || o.Has("maxHeight"))
|
|
389
|
+
win.contentMaxSize = NSMakeSize(BNumOr(o, "maxWidth", 100000),
|
|
390
|
+
BNumOr(o, "maxHeight", 100000));
|
|
391
|
+
return info.Env().Undefined();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
static Napi::Value DestroyWindow2(const Napi::CallbackInfo& info) {
|
|
395
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
396
|
+
win.delegate = nil;
|
|
397
|
+
objc_setAssociatedObject(win, &kDelegateKey, nil,
|
|
398
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
399
|
+
[win orderOut:nil];
|
|
400
|
+
[win close];
|
|
401
|
+
return info.Env().Undefined();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
static Napi::Value ActivateApp(const Napi::CallbackInfo& info) {
|
|
405
|
+
BEnsureApp();
|
|
406
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
407
|
+
return info.Env().Undefined();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// pump2: the enriched event stream
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
static void DispatchEvent2(Napi::Env env, NSEvent* e) {
|
|
415
|
+
if (!HasBackendCb()) return;
|
|
416
|
+
const char* type = nullptr;
|
|
417
|
+
bool mouse = false, key = false, wheel = false, crossing = false;
|
|
418
|
+
switch (e.type) {
|
|
419
|
+
case NSEventTypeLeftMouseDown: type = "mousedown"; mouse = true; break;
|
|
420
|
+
case NSEventTypeLeftMouseUp: type = "mouseup"; mouse = true; break;
|
|
421
|
+
case NSEventTypeRightMouseDown: type = "mousedown"; mouse = true; break;
|
|
422
|
+
case NSEventTypeRightMouseUp: type = "mouseup"; mouse = true; break;
|
|
423
|
+
case NSEventTypeOtherMouseDown: type = "mousedown"; mouse = true; break;
|
|
424
|
+
case NSEventTypeOtherMouseUp: type = "mouseup"; mouse = true; break;
|
|
425
|
+
case NSEventTypeMouseMoved: type = "mousemove"; mouse = true; break;
|
|
426
|
+
case NSEventTypeLeftMouseDragged: type = "mousemove"; mouse = true; break;
|
|
427
|
+
case NSEventTypeRightMouseDragged: type = "mousemove"; mouse = true; break;
|
|
428
|
+
case NSEventTypeOtherMouseDragged: type = "mousemove"; mouse = true; break;
|
|
429
|
+
case NSEventTypeScrollWheel: type = "wheel"; mouse = true; wheel = true; break;
|
|
430
|
+
case NSEventTypeKeyDown: type = "keydown"; key = true; break;
|
|
431
|
+
case NSEventTypeKeyUp: type = "keyup"; key = true; break;
|
|
432
|
+
case NSEventTypeMouseEntered: type = "mouseenter"; crossing = true; break;
|
|
433
|
+
case NSEventTypeMouseExited: type = "mouseleave"; crossing = true; break;
|
|
434
|
+
case NSEventTypeFlagsChanged: type = "flagschanged"; key = true; break;
|
|
435
|
+
default: return;
|
|
436
|
+
}
|
|
437
|
+
if ((mouse || crossing) && !e.window) return;
|
|
438
|
+
|
|
439
|
+
Napi::HandleScope scope(env);
|
|
440
|
+
Napi::Object ev = Napi::Object::New(env);
|
|
441
|
+
ev.Set("type", type);
|
|
442
|
+
if (e.window) ev.Set("windowNumber", (double)e.window.windowNumber);
|
|
443
|
+
ev.Set("time", e.timestamp * 1000.0);
|
|
444
|
+
|
|
445
|
+
NSEventModifierFlags f = e.modifierFlags;
|
|
446
|
+
ev.Set("shift", (bool)(f & NSEventModifierFlagShift));
|
|
447
|
+
ev.Set("control", (bool)(f & NSEventModifierFlagControl));
|
|
448
|
+
ev.Set("option", (bool)(f & NSEventModifierFlagOption));
|
|
449
|
+
ev.Set("command", (bool)(f & NSEventModifierFlagCommand));
|
|
450
|
+
ev.Set("capsLock", (bool)(f & NSEventModifierFlagCapsLock));
|
|
451
|
+
|
|
452
|
+
if ((mouse || crossing) && e.window) {
|
|
453
|
+
NSView* v = e.window.contentView;
|
|
454
|
+
NSPoint p = [v convertPoint:e.locationInWindow fromView:nil];
|
|
455
|
+
ev.Set("x", p.x);
|
|
456
|
+
ev.Set("y", v.isFlipped ? p.y : v.bounds.size.height - p.y);
|
|
457
|
+
// and the same point in global top-left coordinates, for popups
|
|
458
|
+
NSRect r = [e.window
|
|
459
|
+
convertRectToScreen:NSMakeRect(e.locationInWindow.x,
|
|
460
|
+
e.locationInWindow.y, 0, 0)];
|
|
461
|
+
ev.Set("gx", r.origin.x);
|
|
462
|
+
ev.Set("gy", PrimaryScreenTop() - r.origin.y);
|
|
463
|
+
}
|
|
464
|
+
if (mouse && !wheel && !crossing) {
|
|
465
|
+
// 0 left, 1 right, 2 middle in AppKit; X buttons are 1 left, 2 middle,
|
|
466
|
+
// 3 right. Translate here so JS never sees AppKit numbering.
|
|
467
|
+
long b = e.buttonNumber;
|
|
468
|
+
long xbutton = b == 0 ? 1 : b == 1 ? 3 : b == 2 ? 2 : (long)b + 1;
|
|
469
|
+
if (e.type == NSEventTypeMouseMoved ||
|
|
470
|
+
e.type == NSEventTypeLeftMouseDragged ||
|
|
471
|
+
e.type == NSEventTypeRightMouseDragged ||
|
|
472
|
+
e.type == NSEventTypeOtherMouseDragged) {
|
|
473
|
+
xbutton = 0;
|
|
474
|
+
}
|
|
475
|
+
ev.Set("button", (double)xbutton);
|
|
476
|
+
if (e.type == NSEventTypeLeftMouseDown ||
|
|
477
|
+
e.type == NSEventTypeRightMouseDown ||
|
|
478
|
+
e.type == NSEventTypeOtherMouseDown ||
|
|
479
|
+
e.type == NSEventTypeLeftMouseUp ||
|
|
480
|
+
e.type == NSEventTypeRightMouseUp ||
|
|
481
|
+
e.type == NSEventTypeOtherMouseUp) {
|
|
482
|
+
ev.Set("clickCount", (double)e.clickCount);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (wheel) {
|
|
486
|
+
ev.Set("dx", e.scrollingDeltaX);
|
|
487
|
+
ev.Set("dy", e.scrollingDeltaY);
|
|
488
|
+
ev.Set("precise", (bool)e.hasPreciseScrollingDeltas);
|
|
489
|
+
ev.Set("momentum", e.momentumPhase != NSEventPhaseNone);
|
|
490
|
+
}
|
|
491
|
+
if (key && e.type != NSEventTypeFlagsChanged) {
|
|
492
|
+
ev.Set("keyCode", (double)e.keyCode);
|
|
493
|
+
NSString* chars = e.characters;
|
|
494
|
+
NSString* ignoring = e.charactersIgnoringModifiers;
|
|
495
|
+
if (chars) ev.Set("chars", chars.UTF8String);
|
|
496
|
+
if (ignoring) ev.Set("charsShifted", ignoring.UTF8String);
|
|
497
|
+
if (@available(macOS 10.15, *)) {
|
|
498
|
+
NSString* base = [e charactersByApplyingModifiers:0];
|
|
499
|
+
if (base) ev.Set("charsBase", base.UTF8String);
|
|
500
|
+
}
|
|
501
|
+
ev.Set("repeat", (bool)e.isARepeat);
|
|
502
|
+
}
|
|
503
|
+
EmitToJS(env, ev);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
static Napi::Value SetBackendEventCallback(const Napi::CallbackInfo& info) {
|
|
507
|
+
if (info[0].IsFunction()) {
|
|
508
|
+
gBackendCb = Napi::Persistent(info[0].As<Napi::Function>());
|
|
509
|
+
} else {
|
|
510
|
+
gBackendCb.Reset();
|
|
511
|
+
}
|
|
512
|
+
return info.Env().Undefined();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
static Napi::Value Pump2(const Napi::CallbackInfo& info) {
|
|
516
|
+
Napi::Env env = info.Env();
|
|
517
|
+
BEnsureApp();
|
|
518
|
+
@autoreleasepool {
|
|
519
|
+
while (true) {
|
|
520
|
+
NSEvent* e = [NSApp nextEventMatchingMask:NSEventMaskAny
|
|
521
|
+
untilDate:[NSDate distantPast]
|
|
522
|
+
inMode:NSDefaultRunLoopMode
|
|
523
|
+
dequeue:YES];
|
|
524
|
+
if (!e) break;
|
|
525
|
+
DispatchEvent2(env, e);
|
|
526
|
+
[NSApp sendEvent:e];
|
|
527
|
+
}
|
|
528
|
+
[CATransaction flush];
|
|
529
|
+
}
|
|
530
|
+
return env.Undefined();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
// surfaces: CGBitmapContext with canvas-shaped drawing
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
struct CALSurface {
|
|
538
|
+
CGContextRef ctx = nullptr;
|
|
539
|
+
size_t width = 0, height = 0; // pixels
|
|
540
|
+
double scale = 1;
|
|
541
|
+
// when the bitmap lives in an IOSurface (zero-copy presentation), the
|
|
542
|
+
// surface owns a reference and the layer scans out of the same memory
|
|
543
|
+
IOSurfaceRef iosurface = nullptr;
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
static CALSurface* SurfaceFrom(Napi::Value v) {
|
|
547
|
+
return (CALSurface*)v.As<Napi::External<void>>().Data();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// createSurface(widthPx, heightPx, scale) — top-left origin, y down (the
|
|
551
|
+
// base CTM flips Cocoa's bottom-up bitmap once, here).
|
|
552
|
+
static Napi::Value CreateSurface(const Napi::CallbackInfo& info) {
|
|
553
|
+
Napi::Env env = info.Env();
|
|
554
|
+
size_t w = (size_t)info[0].As<Napi::Number>().Int64Value();
|
|
555
|
+
size_t h = (size_t)info[1].As<Napi::Number>().Int64Value();
|
|
556
|
+
double scale = info.Length() > 2 ? info[2].As<Napi::Number>().DoubleValue() : 1;
|
|
557
|
+
if (w < 1) w = 1;
|
|
558
|
+
if (h < 1) h = 1;
|
|
559
|
+
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
560
|
+
CGContextRef ctx = CGBitmapContextCreate(
|
|
561
|
+
NULL, w, h, 8, 0, cs,
|
|
562
|
+
kCGImageAlphaPremultipliedFirst | (CGBitmapInfo)kCGBitmapByteOrder32Host);
|
|
563
|
+
CGColorSpaceRelease(cs);
|
|
564
|
+
if (!ctx) {
|
|
565
|
+
Napi::Error::New(env, "createSurface: CGBitmapContextCreate failed")
|
|
566
|
+
.ThrowAsJavaScriptException();
|
|
567
|
+
return env.Undefined();
|
|
568
|
+
}
|
|
569
|
+
CGContextTranslateCTM(ctx, 0, (CGFloat)h);
|
|
570
|
+
CGContextScaleCTM(ctx, 1, -1);
|
|
571
|
+
CGContextSetInterpolationQuality(ctx, kCGInterpolationMedium);
|
|
572
|
+
auto* s = new CALSurface{ctx, w, h, scale};
|
|
573
|
+
return Napi::External<void>::New(env, s, [](Napi::Env, void* d) {
|
|
574
|
+
auto* s = (CALSurface*)d;
|
|
575
|
+
CGContextRelease(s->ctx);
|
|
576
|
+
delete s;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// createSurfaceIOSurface(widthPx, heightPx, scale)
|
|
581
|
+
// -> { handle, iosurfaceId }
|
|
582
|
+
// The zero-copy presentation surface: the CG bitmap is laid directly over
|
|
583
|
+
// an IOSurface's memory, so presenting is `layer.contents = iosurface` —
|
|
584
|
+
// the WindowServer composites out of the buffer the painters drew into,
|
|
585
|
+
// and the window-sized CGImage copy the plain surface pays per frame
|
|
586
|
+
// never happens. Same top-left CTM contract as createSurface.
|
|
587
|
+
static Napi::Value CreateSurfaceIOSurface(const Napi::CallbackInfo& info) {
|
|
588
|
+
Napi::Env env = info.Env();
|
|
589
|
+
size_t w = (size_t)info[0].As<Napi::Number>().Int64Value();
|
|
590
|
+
size_t h = (size_t)info[1].As<Napi::Number>().Int64Value();
|
|
591
|
+
double scale = info.Length() > 2 ? info[2].As<Napi::Number>().DoubleValue() : 1;
|
|
592
|
+
if (w < 1) w = 1;
|
|
593
|
+
if (h < 1) h = 1;
|
|
594
|
+
|
|
595
|
+
bool shared = info.Length() > 3 && info[3].ToBoolean().Value();
|
|
596
|
+
NSMutableDictionary* props = [@{
|
|
597
|
+
(id)kIOSurfaceWidth : @(w),
|
|
598
|
+
(id)kIOSurfaceHeight : @(h),
|
|
599
|
+
(id)kIOSurfaceBytesPerElement : @4,
|
|
600
|
+
(id)kIOSurfacePixelFormat : @((uint32_t)'BGRA'),
|
|
601
|
+
} mutableCopy];
|
|
602
|
+
// kIOSurfaceIsGlobal is the v1 cross-process route: a pane process
|
|
603
|
+
// creates its buffers with it and the host looks them up by plain id.
|
|
604
|
+
// Deprecated but stable; the clean upgrade is a mach-port handshake. Set
|
|
605
|
+
// ONLY when sharing — an explicit @NO disables the global registry entry
|
|
606
|
+
// that same-process IOSurfaceLookup (the window's own present) relies on.
|
|
607
|
+
if (shared) {
|
|
608
|
+
#pragma clang diagnostic push
|
|
609
|
+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
610
|
+
props[(id)kIOSurfaceIsGlobal] = @YES;
|
|
611
|
+
#pragma clang diagnostic pop
|
|
612
|
+
}
|
|
613
|
+
IOSurfaceRef ios = IOSurfaceCreate((__bridge CFDictionaryRef)props);
|
|
614
|
+
if (!ios) {
|
|
615
|
+
Napi::Error::New(env, "IOSurfaceCreate failed").ThrowAsJavaScriptException();
|
|
616
|
+
return env.Undefined();
|
|
617
|
+
}
|
|
618
|
+
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
619
|
+
CGContextRef ctx = CGBitmapContextCreateWithData(
|
|
620
|
+
IOSurfaceGetBaseAddress(ios), w, h, 8, IOSurfaceGetBytesPerRow(ios), cs,
|
|
621
|
+
kCGImageAlphaPremultipliedFirst | (CGBitmapInfo)kCGBitmapByteOrder32Host,
|
|
622
|
+
NULL, NULL);
|
|
623
|
+
CGColorSpaceRelease(cs);
|
|
624
|
+
if (!ctx) {
|
|
625
|
+
CFRelease(ios);
|
|
626
|
+
Napi::Error::New(env, "CGBitmapContextCreateWithData over IOSurface failed")
|
|
627
|
+
.ThrowAsJavaScriptException();
|
|
628
|
+
return env.Undefined();
|
|
629
|
+
}
|
|
630
|
+
CGContextTranslateCTM(ctx, 0, (CGFloat)h);
|
|
631
|
+
CGContextScaleCTM(ctx, 1, -1);
|
|
632
|
+
CGContextSetInterpolationQuality(ctx, kCGInterpolationMedium);
|
|
633
|
+
auto* s = new CALSurface{ctx, w, h, scale, ios};
|
|
634
|
+
Napi::Object out = Napi::Object::New(env);
|
|
635
|
+
out.Set("handle", Napi::External<void>::New(env, s, [](Napi::Env, void* d) {
|
|
636
|
+
auto* p = (CALSurface*)d;
|
|
637
|
+
CGContextRelease(p->ctx);
|
|
638
|
+
if (p->iosurface) CFRelease(p->iosurface);
|
|
639
|
+
delete p;
|
|
640
|
+
}));
|
|
641
|
+
out.Set("iosurfaceId", (double)IOSurfaceGetID(ios));
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// surfaceFromIOSurfaceID(id, scale) -> { handle, width, height }
|
|
646
|
+
// The consumer end of a shared pane buffer: look the surface up by its
|
|
647
|
+
// process-global id and lay a CG bitmap over its memory, so a child
|
|
648
|
+
// process paints into the very bytes the host's layer scans out of.
|
|
649
|
+
static Napi::Value SurfaceFromIOSurfaceID(const Napi::CallbackInfo& info) {
|
|
650
|
+
Napi::Env env = info.Env();
|
|
651
|
+
uint32_t sid = info[0].As<Napi::Number>().Uint32Value();
|
|
652
|
+
double scale = info.Length() > 1 ? info[1].As<Napi::Number>().DoubleValue() : 1;
|
|
653
|
+
IOSurfaceRef ios = IOSurfaceLookup(sid);
|
|
654
|
+
if (!ios) {
|
|
655
|
+
Napi::Error::New(env, "IOSurfaceLookup: no surface with that id")
|
|
656
|
+
.ThrowAsJavaScriptException();
|
|
657
|
+
return env.Undefined();
|
|
658
|
+
}
|
|
659
|
+
size_t w = IOSurfaceGetWidth(ios);
|
|
660
|
+
size_t h = IOSurfaceGetHeight(ios);
|
|
661
|
+
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
662
|
+
CGContextRef ctx = CGBitmapContextCreateWithData(
|
|
663
|
+
IOSurfaceGetBaseAddress(ios), w, h, 8, IOSurfaceGetBytesPerRow(ios), cs,
|
|
664
|
+
kCGImageAlphaPremultipliedFirst | (CGBitmapInfo)kCGBitmapByteOrder32Host,
|
|
665
|
+
NULL, NULL);
|
|
666
|
+
CGColorSpaceRelease(cs);
|
|
667
|
+
if (!ctx) {
|
|
668
|
+
CFRelease(ios);
|
|
669
|
+
Napi::Error::New(env, "CGBitmapContextCreateWithData over looked-up IOSurface failed")
|
|
670
|
+
.ThrowAsJavaScriptException();
|
|
671
|
+
return env.Undefined();
|
|
672
|
+
}
|
|
673
|
+
CGContextTranslateCTM(ctx, 0, (CGFloat)h);
|
|
674
|
+
CGContextScaleCTM(ctx, 1, -1);
|
|
675
|
+
CGContextSetInterpolationQuality(ctx, kCGInterpolationMedium);
|
|
676
|
+
auto* s = new CALSurface{ctx, w, h, scale, ios};
|
|
677
|
+
Napi::Object out = Napi::Object::New(env);
|
|
678
|
+
out.Set("handle", Napi::External<void>::New(env, s, [](Napi::Env, void* d) {
|
|
679
|
+
auto* p = (CALSurface*)d;
|
|
680
|
+
CGContextRelease(p->ctx);
|
|
681
|
+
if (p->iosurface) CFRelease(p->iosurface);
|
|
682
|
+
delete p;
|
|
683
|
+
}));
|
|
684
|
+
out.Set("width", (double)w);
|
|
685
|
+
out.Set("height", (double)h);
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// CPU access bracketing for an IOSurface-backed surface: lock before the
|
|
690
|
+
// frame's first draw, unlock before handing the buffer to the layer. No-op
|
|
691
|
+
// on a plain surface, so callers need not care which kind they hold.
|
|
692
|
+
static Napi::Value SurfaceLock(const Napi::CallbackInfo& info) {
|
|
693
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
694
|
+
if (s->iosurface) IOSurfaceLock(s->iosurface, 0, NULL);
|
|
695
|
+
return info.Env().Undefined();
|
|
696
|
+
}
|
|
697
|
+
static Napi::Value SurfaceUnlock(const Napi::CallbackInfo& info) {
|
|
698
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
699
|
+
if (s->iosurface) IOSurfaceUnlock(s->iosurface, 0, NULL);
|
|
700
|
+
return info.Env().Undefined();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// copySurfaceRegion(src, dst, [x, y, w, h, ...]) — bring a swapchain's new
|
|
704
|
+
// back buffer current: memcpy the named device-px rects. Same-size
|
|
705
|
+
// surfaces only; rects are clamped. Null/empty rects list copies all.
|
|
706
|
+
static Napi::Value CopySurfaceRegion(const Napi::CallbackInfo& info) {
|
|
707
|
+
Napi::Env env = info.Env();
|
|
708
|
+
CALSurface* src = SurfaceFrom(info[0]);
|
|
709
|
+
CALSurface* dst = SurfaceFrom(info[1]);
|
|
710
|
+
if (src->width != dst->width || src->height != dst->height) {
|
|
711
|
+
Napi::Error::New(env, "copySurfaceRegion: size mismatch")
|
|
712
|
+
.ThrowAsJavaScriptException();
|
|
713
|
+
return env.Undefined();
|
|
714
|
+
}
|
|
715
|
+
const uint8_t* sbase = (const uint8_t*)CGBitmapContextGetData(src->ctx);
|
|
716
|
+
uint8_t* dbase = (uint8_t*)CGBitmapContextGetData(dst->ctx);
|
|
717
|
+
size_t srow = CGBitmapContextGetBytesPerRow(src->ctx);
|
|
718
|
+
size_t drow = CGBitmapContextGetBytesPerRow(dst->ctx);
|
|
719
|
+
if (!sbase || !dbase) return env.Undefined();
|
|
720
|
+
auto copyRect = [&](long x, long y, long w, long h) {
|
|
721
|
+
if (x < 0) { w += x; x = 0; }
|
|
722
|
+
if (y < 0) { h += y; y = 0; }
|
|
723
|
+
if (x + w > (long)src->width) w = (long)src->width - x;
|
|
724
|
+
if (y + h > (long)src->height) h = (long)src->height - y;
|
|
725
|
+
if (w <= 0 || h <= 0) return;
|
|
726
|
+
for (long r = 0; r < h; r++) {
|
|
727
|
+
memcpy(dbase + (size_t)(y + r) * drow + (size_t)x * 4,
|
|
728
|
+
sbase + (size_t)(y + r) * srow + (size_t)x * 4, (size_t)w * 4);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
if (info.Length() < 3 || info[2].IsNull() || info[2].IsUndefined()) {
|
|
732
|
+
copyRect(0, 0, (long)src->width, (long)src->height);
|
|
733
|
+
return env.Undefined();
|
|
734
|
+
}
|
|
735
|
+
Napi::Array rects = info[2].As<Napi::Array>();
|
|
736
|
+
if (rects.Length() == 0) {
|
|
737
|
+
copyRect(0, 0, (long)src->width, (long)src->height);
|
|
738
|
+
return env.Undefined();
|
|
739
|
+
}
|
|
740
|
+
for (uint32_t i = 0; i + 3 < rects.Length(); i += 4) {
|
|
741
|
+
copyRect((long)rects.Get(i).As<Napi::Number>().Int64Value(),
|
|
742
|
+
(long)rects.Get(i + 1).As<Napi::Number>().Int64Value(),
|
|
743
|
+
(long)rects.Get(i + 2).As<Napi::Number>().Int64Value(),
|
|
744
|
+
(long)rects.Get(i + 3).As<Napi::Number>().Int64Value());
|
|
745
|
+
}
|
|
746
|
+
return env.Undefined();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
static Napi::Value SurfaceSize(const Napi::CallbackInfo& info) {
|
|
750
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
751
|
+
Napi::Object r = Napi::Object::New(info.Env());
|
|
752
|
+
r.Set("width", (double)s->width);
|
|
753
|
+
r.Set("height", (double)s->height);
|
|
754
|
+
r.Set("scale", s->scale);
|
|
755
|
+
return r;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
// the macOS main menu — the global-menu adapter's native half. The JS side
|
|
760
|
+
// owns the item model (react-x11's dbusmenu snapshot machinery, stable ids
|
|
761
|
+
// via IdAllocator); this side turns one spec into an NSMenu tree and fires
|
|
762
|
+
// a backend event with the item's id on activation, delivered through the
|
|
763
|
+
// same callback every other event takes. Menu tracking is one of AppKit's
|
|
764
|
+
// modal loops, and those already call into JS here (live resize does), so
|
|
765
|
+
// activation needs no extra plumbing.
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
|
|
768
|
+
static NSString* BStrOr(Napi::Object o, const char* k, NSString* d);
|
|
769
|
+
|
|
770
|
+
@interface CALMenuTarget : NSObject {
|
|
771
|
+
@public
|
|
772
|
+
napi_env env_;
|
|
773
|
+
}
|
|
774
|
+
- (void)activate:(NSMenuItem*)sender;
|
|
775
|
+
@end
|
|
776
|
+
@implementation CALMenuTarget
|
|
777
|
+
- (void)activate:(NSMenuItem*)sender {
|
|
778
|
+
Napi::Env env(env_);
|
|
779
|
+
Napi::HandleScope scope(env);
|
|
780
|
+
Napi::Object ev = Napi::Object::New(env);
|
|
781
|
+
ev.Set("type", "menu-activate");
|
|
782
|
+
ev.Set("id", (double)sender.tag);
|
|
783
|
+
EmitToJS(env, ev);
|
|
784
|
+
}
|
|
785
|
+
@end
|
|
786
|
+
|
|
787
|
+
static CALMenuTarget* gMenuTarget = nil;
|
|
788
|
+
|
|
789
|
+
static NSMenu* BuildMenuFrom(Napi::Env env, Napi::Array items);
|
|
790
|
+
|
|
791
|
+
static NSMenuItem* BuildMenuItem(Napi::Env env, Napi::Object o) {
|
|
792
|
+
if (BBoolOr(o, "separator", false)) return [NSMenuItem separatorItem];
|
|
793
|
+
NSMenuItem* it = [[NSMenuItem alloc] initWithTitle:BStrOr(o, "title", @"")
|
|
794
|
+
action:nil
|
|
795
|
+
keyEquivalent:@""];
|
|
796
|
+
it.tag = (NSInteger)BNumOr(o, "id", 0);
|
|
797
|
+
it.enabled = BBoolOr(o, "enabled", true);
|
|
798
|
+
it.hidden = BBoolOr(o, "hidden", false);
|
|
799
|
+
it.state = BBoolOr(o, "checked", false) ? NSControlStateValueOn
|
|
800
|
+
: NSControlStateValueOff;
|
|
801
|
+
NSString* key = BStrOr(o, "key", @"");
|
|
802
|
+
if (key.length) {
|
|
803
|
+
it.keyEquivalent = key;
|
|
804
|
+
it.keyEquivalentModifierMask =
|
|
805
|
+
(NSUInteger)BNumOr(o, "modifiers", NSEventModifierFlagCommand);
|
|
806
|
+
}
|
|
807
|
+
// Icons, the serialisable pair from the dbusmenu vocabulary. `iconName`
|
|
808
|
+
// is read in the platform's own icon theme — SF Symbols — which renders
|
|
809
|
+
// as a template and follows the menu's appearance for free; a name the
|
|
810
|
+
// symbol catalogue does not know simply misses (a freedesktop name on
|
|
811
|
+
// its way to a Linux panel does the same in reverse). `iconData` is
|
|
812
|
+
// literal pixels (PNG bytes on the bus) and is the fallback.
|
|
813
|
+
NSString* iconName = BStrOr(o, "iconName", @"");
|
|
814
|
+
NSImage* icon = nil;
|
|
815
|
+
if (iconName.length) {
|
|
816
|
+
icon = [NSImage imageWithSystemSymbolName:iconName
|
|
817
|
+
accessibilityDescription:nil];
|
|
818
|
+
}
|
|
819
|
+
if (!icon && o.Has("iconData")) {
|
|
820
|
+
Napi::Value v = o.Get("iconData");
|
|
821
|
+
if (v.IsBuffer()) {
|
|
822
|
+
Napi::Buffer<uint8_t> buf = v.As<Napi::Buffer<uint8_t>>();
|
|
823
|
+
NSData* bytes = [NSData dataWithBytes:buf.Data() length:buf.Length()];
|
|
824
|
+
icon = [[NSImage alloc] initWithData:bytes];
|
|
825
|
+
if (icon) icon.size = NSMakeSize(16, 16);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (icon) it.image = icon;
|
|
829
|
+
bool hasChildren = false;
|
|
830
|
+
if (o.Has("items")) {
|
|
831
|
+
Napi::Value v = o.Get("items");
|
|
832
|
+
if (v.IsArray() && v.As<Napi::Array>().Length() > 0) {
|
|
833
|
+
NSMenu* sub = BuildMenuFrom(env, v.As<Napi::Array>());
|
|
834
|
+
sub.title = it.title;
|
|
835
|
+
it.submenu = sub;
|
|
836
|
+
hasChildren = true;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!hasChildren) {
|
|
840
|
+
it.target = gMenuTarget;
|
|
841
|
+
it.action = @selector(activate:);
|
|
842
|
+
}
|
|
843
|
+
return it;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
static NSMenu* BuildMenuFrom(Napi::Env env, Napi::Array items) {
|
|
847
|
+
NSMenu* m = [[NSMenu alloc] initWithTitle:@""];
|
|
848
|
+
// we own enabled/hidden; AppKit's validation would grey everything whose
|
|
849
|
+
// target it cannot interrogate
|
|
850
|
+
m.autoenablesItems = NO;
|
|
851
|
+
for (uint32_t i = 0; i < items.Length(); i++) {
|
|
852
|
+
Napi::Value v = items.Get(i);
|
|
853
|
+
if (!v.IsObject()) continue;
|
|
854
|
+
[m addItem:BuildMenuItem(env, v.As<Napi::Object>())];
|
|
855
|
+
}
|
|
856
|
+
return m;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// setMainMenu(spec) — spec: [{title, items: [...]}, ...]. Entry 0 is the
|
|
860
|
+
// app menu (macOS shows the process name for its title regardless).
|
|
861
|
+
static Napi::Value SetMainMenuFn(const Napi::CallbackInfo& info) {
|
|
862
|
+
Napi::Env env = info.Env();
|
|
863
|
+
BEnsureApp();
|
|
864
|
+
if (!gMenuTarget) gMenuTarget = [CALMenuTarget new];
|
|
865
|
+
gMenuTarget->env_ = env;
|
|
866
|
+
Napi::Array spec = info[0].As<Napi::Array>();
|
|
867
|
+
NSMenu* main = [[NSMenu alloc] initWithTitle:@"MainMenu"];
|
|
868
|
+
main.autoenablesItems = NO;
|
|
869
|
+
for (uint32_t i = 0; i < spec.Length(); i++) {
|
|
870
|
+
Napi::Value v = spec.Get(i);
|
|
871
|
+
if (!v.IsObject()) continue;
|
|
872
|
+
Napi::Object m = v.As<Napi::Object>();
|
|
873
|
+
NSString* title = BStrOr(m, "title", @"");
|
|
874
|
+
NSMenuItem* holder = [[NSMenuItem alloc] initWithTitle:title
|
|
875
|
+
action:nil
|
|
876
|
+
keyEquivalent:@""];
|
|
877
|
+
Napi::Value items = m.Get("items");
|
|
878
|
+
NSMenu* sub = items.IsArray()
|
|
879
|
+
? BuildMenuFrom(env, items.As<Napi::Array>())
|
|
880
|
+
: [[NSMenu alloc] initWithTitle:title];
|
|
881
|
+
sub.autoenablesItems = NO;
|
|
882
|
+
sub.title = title;
|
|
883
|
+
holder.submenu = sub;
|
|
884
|
+
[main addItem:holder];
|
|
885
|
+
}
|
|
886
|
+
[NSApp setMainMenu:main];
|
|
887
|
+
return env.Undefined();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
static Napi::Object MenuInfo(Napi::Env env, NSMenu* menu) {
|
|
891
|
+
Napi::Object out = Napi::Object::New(env);
|
|
892
|
+
out.Set("title", [menu.title UTF8String]);
|
|
893
|
+
Napi::Array arr = Napi::Array::New(env, menu.numberOfItems);
|
|
894
|
+
for (NSInteger i = 0; i < menu.numberOfItems; i++) {
|
|
895
|
+
NSMenuItem* it = [menu itemAtIndex:i];
|
|
896
|
+
Napi::Object io = Napi::Object::New(env);
|
|
897
|
+
io.Set("title", [it.title UTF8String]);
|
|
898
|
+
io.Set("id", (double)it.tag);
|
|
899
|
+
io.Set("enabled", (bool)it.enabled);
|
|
900
|
+
io.Set("hidden", (bool)it.hidden);
|
|
901
|
+
io.Set("separator", (bool)it.separatorItem);
|
|
902
|
+
io.Set("checked", it.state == NSControlStateValueOn);
|
|
903
|
+
io.Set("hasImage", it.image != nil);
|
|
904
|
+
io.Set("key", [it.keyEquivalent UTF8String]);
|
|
905
|
+
if (it.submenu) io.Set("submenu", MenuInfo(env, it.submenu));
|
|
906
|
+
arr.Set((uint32_t)i, io);
|
|
907
|
+
}
|
|
908
|
+
out.Set("items", arr);
|
|
909
|
+
return out;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// mainMenuInfo() — the installed menu bar as data, for tests.
|
|
913
|
+
static Napi::Value MainMenuInfoFn(const Napi::CallbackInfo& info) {
|
|
914
|
+
Napi::Env env = info.Env();
|
|
915
|
+
NSMenu* main = [NSApp mainMenu];
|
|
916
|
+
if (!main) return env.Null();
|
|
917
|
+
return MenuInfo(env, main);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// activateMenuItem([i, j, ...]) — walk the installed bar by index and fire
|
|
921
|
+
// the leaf's action, the way tracking would. For tests.
|
|
922
|
+
static Napi::Value ActivateMenuItemFn(const Napi::CallbackInfo& info) {
|
|
923
|
+
Napi::Env env = info.Env();
|
|
924
|
+
Napi::Array path = info[0].As<Napi::Array>();
|
|
925
|
+
NSMenu* menu = [NSApp mainMenu];
|
|
926
|
+
if (!menu) return Napi::Boolean::New(env, false);
|
|
927
|
+
for (uint32_t d = 0; d + 1 < path.Length(); d++) {
|
|
928
|
+
NSInteger i = (NSInteger)path.Get(d).As<Napi::Number>().Int64Value();
|
|
929
|
+
if (i < 0 || i >= menu.numberOfItems) return Napi::Boolean::New(env, false);
|
|
930
|
+
menu = [menu itemAtIndex:i].submenu;
|
|
931
|
+
if (!menu) return Napi::Boolean::New(env, false);
|
|
932
|
+
}
|
|
933
|
+
NSInteger leaf = (NSInteger)
|
|
934
|
+
path.Get(path.Length() - 1).As<Napi::Number>().Int64Value();
|
|
935
|
+
if (leaf < 0 || leaf >= menu.numberOfItems)
|
|
936
|
+
return Napi::Boolean::New(env, false);
|
|
937
|
+
[menu performActionForItemAtIndex:leaf];
|
|
938
|
+
return Napi::Boolean::New(env, true);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ---------------------------------------------------------------------------
|
|
942
|
+
// native control bezels — NSCell/NSControl rendered offscreen (the WebKit/
|
|
943
|
+
// Gecko form-control technique), measured and drawn in one vocabulary so the
|
|
944
|
+
// JS side can cache by parameters. Everything is in points; the surface's
|
|
945
|
+
// own scale says how many pixels a point is worth.
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
|
|
948
|
+
static NSString* BStrOr(Napi::Object o, const char* k, NSString* d) {
|
|
949
|
+
if (!o.Has(k)) return d;
|
|
950
|
+
Napi::Value v = o.Get(k);
|
|
951
|
+
return v.IsString() ? BToNSString(v) : d;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
static NSView* BezelDrawView() {
|
|
955
|
+
// Cells only consult the view for flippedness and appearance; it never
|
|
956
|
+
// needs a window.
|
|
957
|
+
static CALBackendView* v = nil;
|
|
958
|
+
if (!v) v = [[CALBackendView alloc] initWithFrame:NSMakeRect(0, 0, 1000, 1000)];
|
|
959
|
+
return v;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// The two render paths AppKit leaves us: classic cells draw offscreen via
|
|
963
|
+
// drawWithFrame:, while NSSlider's cell now renders through the view's layer
|
|
964
|
+
// machinery and NSSwitch has no cell at all — those go through a real
|
|
965
|
+
// offscreen NSControl and displayRectIgnoringOpacity:inContext:.
|
|
966
|
+
struct BezelControl {
|
|
967
|
+
NSCell* cell = nil;
|
|
968
|
+
NSControl* view = nil;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
static BezelControl BuildBezel(Napi::Env env, Napi::Object o) {
|
|
972
|
+
BezelControl out;
|
|
973
|
+
NSString* kind = BStrOr(o, "kind", @"push");
|
|
974
|
+
bool pressed = BBoolOr(o, "pressed", false);
|
|
975
|
+
bool enabled = BBoolOr(o, "enabled", true);
|
|
976
|
+
int state = (int)BNumOr(o, "state", 0); // 0 off, 1 on
|
|
977
|
+
|
|
978
|
+
if ([kind isEqualToString:@"checkbox"] || [kind isEqualToString:@"radio"] ||
|
|
979
|
+
[kind isEqualToString:@"push"]) {
|
|
980
|
+
NSButtonCell* c = [[NSButtonCell alloc] initTextCell:BStrOr(o, "title", @"")];
|
|
981
|
+
if ([kind isEqualToString:@"checkbox"]) {
|
|
982
|
+
c.buttonType = NSButtonTypeSwitch;
|
|
983
|
+
} else if ([kind isEqualToString:@"radio"]) {
|
|
984
|
+
c.buttonType = NSButtonTypeRadio;
|
|
985
|
+
} else {
|
|
986
|
+
c.buttonType = NSButtonTypeMomentaryPushIn;
|
|
987
|
+
c.bezelStyle = NSBezelStylePush;
|
|
988
|
+
// the Return key equivalent is what makes AppKit fill it with the
|
|
989
|
+
// user's accent — the "default button" look
|
|
990
|
+
if (BBoolOr(o, "isDefault", false)) c.keyEquivalent = @"\r";
|
|
991
|
+
}
|
|
992
|
+
c.state = state == 1 ? NSControlStateValueOn : NSControlStateValueOff;
|
|
993
|
+
out.cell = c;
|
|
994
|
+
} else if ([kind isEqualToString:@"popup"]) {
|
|
995
|
+
NSPopUpButtonCell* c = [[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO];
|
|
996
|
+
[c addItemWithTitle:BStrOr(o, "title", @"")];
|
|
997
|
+
out.cell = c;
|
|
998
|
+
} else if ([kind isEqualToString:@"slider"]) {
|
|
999
|
+
NSSlider* s = [[NSSlider alloc] init];
|
|
1000
|
+
s.minValue = 0;
|
|
1001
|
+
s.maxValue = 1;
|
|
1002
|
+
s.doubleValue = BNumOr(o, "value", 0.5);
|
|
1003
|
+
out.view = s;
|
|
1004
|
+
} else if ([kind isEqualToString:@"switch"]) {
|
|
1005
|
+
NSSwitch* s = [[NSSwitch alloc] init];
|
|
1006
|
+
s.state = state == 1 ? NSControlStateValueOn : NSControlStateValueOff;
|
|
1007
|
+
out.view = s;
|
|
1008
|
+
} else {
|
|
1009
|
+
Napi::Error::New(env, "unknown control kind").ThrowAsJavaScriptException();
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
NSString* sz = BStrOr(o, "controlSize", @"regular");
|
|
1014
|
+
NSControlSize csize = NSControlSizeRegular;
|
|
1015
|
+
if ([sz isEqualToString:@"small"]) csize = NSControlSizeSmall;
|
|
1016
|
+
else if ([sz isEqualToString:@"mini"]) csize = NSControlSizeMini;
|
|
1017
|
+
else if ([sz isEqualToString:@"large"]) csize = NSControlSizeLarge;
|
|
1018
|
+
|
|
1019
|
+
if (out.cell) {
|
|
1020
|
+
out.cell.controlSize = csize;
|
|
1021
|
+
out.cell.font =
|
|
1022
|
+
[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:csize]];
|
|
1023
|
+
out.cell.enabled = enabled;
|
|
1024
|
+
out.cell.highlighted = pressed;
|
|
1025
|
+
} else if (out.view) {
|
|
1026
|
+
out.view.controlSize = csize;
|
|
1027
|
+
out.view.enabled = enabled;
|
|
1028
|
+
}
|
|
1029
|
+
return out;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
static NSAppearance* BezelAppearance(Napi::Object o) {
|
|
1033
|
+
NSString* name = BStrOr(o, "appearance", @"system");
|
|
1034
|
+
if ([name isEqualToString:@"dark"])
|
|
1035
|
+
return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
|
|
1036
|
+
if ([name isEqualToString:@"light"])
|
|
1037
|
+
return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
|
|
1038
|
+
return NSApp.effectiveAppearance;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// measureControl({kind, controlSize, title?}) -> {width, height} in points —
|
|
1042
|
+
// the control's natural size, which is the size the bezel is *designed* at:
|
|
1043
|
+
// stretching a checkbox distorts it, so layout adopts these.
|
|
1044
|
+
static Napi::Value MeasureControl(const Napi::CallbackInfo& info) {
|
|
1045
|
+
Napi::Env env = info.Env();
|
|
1046
|
+
BEnsureApp();
|
|
1047
|
+
Napi::Object o = info[0].As<Napi::Object>();
|
|
1048
|
+
BezelControl c = BuildBezel(env, o);
|
|
1049
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
1050
|
+
double w = 0, h = 0;
|
|
1051
|
+
if (c.cell) {
|
|
1052
|
+
NSSize natural = c.cell.cellSize;
|
|
1053
|
+
w = ceil(natural.width);
|
|
1054
|
+
h = ceil(natural.height);
|
|
1055
|
+
} else if (c.view) {
|
|
1056
|
+
NSSize natural = c.view.intrinsicContentSize;
|
|
1057
|
+
w = natural.width > 0 ? ceil(natural.width) : 100;
|
|
1058
|
+
h = natural.height > 0 ? ceil(natural.height) : 22;
|
|
1059
|
+
}
|
|
1060
|
+
Napi::Object r = Napi::Object::New(env);
|
|
1061
|
+
r.Set("width", w);
|
|
1062
|
+
r.Set("height", h);
|
|
1063
|
+
return r;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// drawControlIntoSurface(surface, params) — render the bezel to fill the
|
|
1067
|
+
// whole surface (surface px / scale = the frame in points). Clears first:
|
|
1068
|
+
// bezels are alpha-composited art, not opaque tiles.
|
|
1069
|
+
static Napi::Value DrawControlIntoSurface(const Napi::CallbackInfo& info) {
|
|
1070
|
+
Napi::Env env = info.Env();
|
|
1071
|
+
BEnsureApp();
|
|
1072
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1073
|
+
Napi::Object o = info[1].As<Napi::Object>();
|
|
1074
|
+
BezelControl c = BuildBezel(env, o);
|
|
1075
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
1076
|
+
|
|
1077
|
+
double scale = s->scale > 0 ? s->scale : 1;
|
|
1078
|
+
double w = s->width / scale, h = s->height / scale;
|
|
1079
|
+
|
|
1080
|
+
CGContextSaveGState(s->ctx);
|
|
1081
|
+
// the surface's base CTM is already top-left-origin device pixels; clear
|
|
1082
|
+
// in that space, then move to points for AppKit
|
|
1083
|
+
CGContextClearRect(s->ctx, CGRectMake(0, 0, (CGFloat)s->width, (CGFloat)s->height));
|
|
1084
|
+
CGContextScaleCTM(s->ctx, scale, scale);
|
|
1085
|
+
|
|
1086
|
+
NSGraphicsContext* g =
|
|
1087
|
+
[NSGraphicsContext graphicsContextWithCGContext:s->ctx flipped:YES];
|
|
1088
|
+
[NSGraphicsContext saveGraphicsState];
|
|
1089
|
+
[NSGraphicsContext setCurrentContext:g];
|
|
1090
|
+
|
|
1091
|
+
NSAppearance* ap = BezelAppearance(o);
|
|
1092
|
+
if (c.view) {
|
|
1093
|
+
c.view.frame = NSMakeRect(0, 0, w, h);
|
|
1094
|
+
c.view.appearance = ap;
|
|
1095
|
+
[c.view layoutSubtreeIfNeeded];
|
|
1096
|
+
[c.view displayRectIgnoringOpacity:c.view.bounds inContext:g];
|
|
1097
|
+
} else if (c.cell) {
|
|
1098
|
+
NSCell* cell = c.cell;
|
|
1099
|
+
[ap performAsCurrentDrawingAppearance:^{
|
|
1100
|
+
[cell drawWithFrame:NSMakeRect(0, 0, w, h) inView:BezelDrawView()];
|
|
1101
|
+
}];
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
[NSGraphicsContext restoreGraphicsState];
|
|
1105
|
+
CGContextRestoreGState(s->ctx);
|
|
1106
|
+
return env.Undefined();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// --- drawing verbs. All take the surface handle first. ---------------------
|
|
1110
|
+
|
|
1111
|
+
static Napi::Value CtxSave(const Napi::CallbackInfo& info) {
|
|
1112
|
+
CGContextSaveGState(SurfaceFrom(info[0])->ctx);
|
|
1113
|
+
return info.Env().Undefined();
|
|
1114
|
+
}
|
|
1115
|
+
static Napi::Value CtxRestore(const Napi::CallbackInfo& info) {
|
|
1116
|
+
CGContextRestoreGState(SurfaceFrom(info[0])->ctx);
|
|
1117
|
+
return info.Env().Undefined();
|
|
1118
|
+
}
|
|
1119
|
+
static Napi::Value CtxTranslate(const Napi::CallbackInfo& info) {
|
|
1120
|
+
CGContextTranslateCTM(SurfaceFrom(info[0])->ctx,
|
|
1121
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1122
|
+
info[2].As<Napi::Number>().DoubleValue());
|
|
1123
|
+
return info.Env().Undefined();
|
|
1124
|
+
}
|
|
1125
|
+
static Napi::Value CtxScale(const Napi::CallbackInfo& info) {
|
|
1126
|
+
CGContextScaleCTM(SurfaceFrom(info[0])->ctx,
|
|
1127
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1128
|
+
info[2].As<Napi::Number>().DoubleValue());
|
|
1129
|
+
return info.Env().Undefined();
|
|
1130
|
+
}
|
|
1131
|
+
static Napi::Value CtxTransform(const Napi::CallbackInfo& info) {
|
|
1132
|
+
CGContextConcatCTM(SurfaceFrom(info[0])->ctx,
|
|
1133
|
+
CGAffineTransformMake(
|
|
1134
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1135
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1136
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1137
|
+
info[4].As<Napi::Number>().DoubleValue(),
|
|
1138
|
+
info[5].As<Napi::Number>().DoubleValue(),
|
|
1139
|
+
info[6].As<Napi::Number>().DoubleValue()));
|
|
1140
|
+
return info.Env().Undefined();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
static Napi::Value CtxRotate(const Napi::CallbackInfo& info) {
|
|
1144
|
+
CGContextRotateCTM(SurfaceFrom(info[0])->ctx,
|
|
1145
|
+
info[1].As<Napi::Number>().DoubleValue());
|
|
1146
|
+
return info.Env().Undefined();
|
|
1147
|
+
}
|
|
1148
|
+
static Napi::Value CtxBeginPath(const Napi::CallbackInfo& info) {
|
|
1149
|
+
CGContextBeginPath(SurfaceFrom(info[0])->ctx);
|
|
1150
|
+
return info.Env().Undefined();
|
|
1151
|
+
}
|
|
1152
|
+
static Napi::Value CtxMoveTo(const Napi::CallbackInfo& info) {
|
|
1153
|
+
CGContextMoveToPoint(SurfaceFrom(info[0])->ctx,
|
|
1154
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1155
|
+
info[2].As<Napi::Number>().DoubleValue());
|
|
1156
|
+
return info.Env().Undefined();
|
|
1157
|
+
}
|
|
1158
|
+
static Napi::Value CtxLineTo(const Napi::CallbackInfo& info) {
|
|
1159
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1160
|
+
double x = info[1].As<Napi::Number>().DoubleValue();
|
|
1161
|
+
double y = info[2].As<Napi::Number>().DoubleValue();
|
|
1162
|
+
if (CGContextIsPathEmpty(s->ctx)) CGContextMoveToPoint(s->ctx, x, y);
|
|
1163
|
+
else CGContextAddLineToPoint(s->ctx, x, y);
|
|
1164
|
+
return info.Env().Undefined();
|
|
1165
|
+
}
|
|
1166
|
+
static Napi::Value CtxRect(const Napi::CallbackInfo& info) {
|
|
1167
|
+
CGContextAddRect(SurfaceFrom(info[0])->ctx,
|
|
1168
|
+
CGRectMake(info[1].As<Napi::Number>().DoubleValue(),
|
|
1169
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1170
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1171
|
+
info[4].As<Napi::Number>().DoubleValue()));
|
|
1172
|
+
return info.Env().Undefined();
|
|
1173
|
+
}
|
|
1174
|
+
// roundRect(surface, x, y, w, h, r0, r1, r2, r3) — per-corner radii,
|
|
1175
|
+
// top-left/top-right/bottom-right/bottom-left, already clamped by JS.
|
|
1176
|
+
static Napi::Value CtxRoundRect(const Napi::CallbackInfo& info) {
|
|
1177
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1178
|
+
double x = info[1].As<Napi::Number>().DoubleValue();
|
|
1179
|
+
double y = info[2].As<Napi::Number>().DoubleValue();
|
|
1180
|
+
double w = info[3].As<Napi::Number>().DoubleValue();
|
|
1181
|
+
double h = info[4].As<Napi::Number>().DoubleValue();
|
|
1182
|
+
double tl = info[5].As<Napi::Number>().DoubleValue();
|
|
1183
|
+
double tr = info[6].As<Napi::Number>().DoubleValue();
|
|
1184
|
+
double br = info[7].As<Napi::Number>().DoubleValue();
|
|
1185
|
+
double bl = info[8].As<Napi::Number>().DoubleValue();
|
|
1186
|
+
CGMutablePathRef p = CGPathCreateMutable();
|
|
1187
|
+
CGPathMoveToPoint(p, NULL, x + tl, y);
|
|
1188
|
+
CGPathAddLineToPoint(p, NULL, x + w - tr, y);
|
|
1189
|
+
CGPathAddArcToPoint(p, NULL, x + w, y, x + w, y + tr, tr);
|
|
1190
|
+
CGPathAddLineToPoint(p, NULL, x + w, y + h - br);
|
|
1191
|
+
CGPathAddArcToPoint(p, NULL, x + w, y + h, x + w - br, y + h, br);
|
|
1192
|
+
CGPathAddLineToPoint(p, NULL, x + bl, y + h);
|
|
1193
|
+
CGPathAddArcToPoint(p, NULL, x, y + h, x, y + h - bl, bl);
|
|
1194
|
+
CGPathAddLineToPoint(p, NULL, x, y + tl);
|
|
1195
|
+
CGPathAddArcToPoint(p, NULL, x, y, x + tl, y, tl);
|
|
1196
|
+
CGPathCloseSubpath(p);
|
|
1197
|
+
CGContextAddPath(s->ctx, p);
|
|
1198
|
+
CGPathRelease(p);
|
|
1199
|
+
return info.Env().Undefined();
|
|
1200
|
+
}
|
|
1201
|
+
static Napi::Value CtxArc(const Napi::CallbackInfo& info) {
|
|
1202
|
+
// arc(surface, x, y, r, a0, a1, anticlockwise). Angles live in user
|
|
1203
|
+
// space, where canvas's y-down "clockwise" sweep is the INCREASING-angle
|
|
1204
|
+
// direction — which is what CG calls counterclockwise (clockwise = 0).
|
|
1205
|
+
// The flag therefore maps straight across, not inverted: getting this
|
|
1206
|
+
// backwards leaves full circles (donuts) looking right and every partial
|
|
1207
|
+
// arc sweeping the long way round — the raster-gate gauges caught it.
|
|
1208
|
+
CGContextAddArc(SurfaceFrom(info[0])->ctx,
|
|
1209
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1210
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1211
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1212
|
+
info[4].As<Napi::Number>().DoubleValue(),
|
|
1213
|
+
info[5].As<Napi::Number>().DoubleValue(),
|
|
1214
|
+
info[6].ToBoolean().Value() ? 1 : 0);
|
|
1215
|
+
return info.Env().Undefined();
|
|
1216
|
+
}
|
|
1217
|
+
static Napi::Value CtxEllipse(const Napi::CallbackInfo& info) {
|
|
1218
|
+
CGContextAddEllipseInRect(
|
|
1219
|
+
SurfaceFrom(info[0])->ctx,
|
|
1220
|
+
CGRectMake(info[1].As<Napi::Number>().DoubleValue() -
|
|
1221
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1222
|
+
info[2].As<Napi::Number>().DoubleValue() -
|
|
1223
|
+
info[4].As<Napi::Number>().DoubleValue(),
|
|
1224
|
+
info[3].As<Napi::Number>().DoubleValue() * 2,
|
|
1225
|
+
info[4].As<Napi::Number>().DoubleValue() * 2));
|
|
1226
|
+
return info.Env().Undefined();
|
|
1227
|
+
}
|
|
1228
|
+
static Napi::Value CtxCurveTo(const Napi::CallbackInfo& info) {
|
|
1229
|
+
CGContextAddCurveToPoint(SurfaceFrom(info[0])->ctx,
|
|
1230
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1231
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1232
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1233
|
+
info[4].As<Napi::Number>().DoubleValue(),
|
|
1234
|
+
info[5].As<Napi::Number>().DoubleValue(),
|
|
1235
|
+
info[6].As<Napi::Number>().DoubleValue());
|
|
1236
|
+
return info.Env().Undefined();
|
|
1237
|
+
}
|
|
1238
|
+
static Napi::Value CtxQuadTo(const Napi::CallbackInfo& info) {
|
|
1239
|
+
CGContextAddQuadCurveToPoint(SurfaceFrom(info[0])->ctx,
|
|
1240
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1241
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1242
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1243
|
+
info[4].As<Napi::Number>().DoubleValue());
|
|
1244
|
+
return info.Env().Undefined();
|
|
1245
|
+
}
|
|
1246
|
+
static Napi::Value CtxClosePath(const Napi::CallbackInfo& info) {
|
|
1247
|
+
CGContextClosePath(SurfaceFrom(info[0])->ctx);
|
|
1248
|
+
return info.Env().Undefined();
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
static Napi::Value CtxSetFillColor(const Napi::CallbackInfo& info) {
|
|
1252
|
+
CGContextSetRGBFillColor(SurfaceFrom(info[0])->ctx,
|
|
1253
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1254
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1255
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1256
|
+
info[4].As<Napi::Number>().DoubleValue());
|
|
1257
|
+
return info.Env().Undefined();
|
|
1258
|
+
}
|
|
1259
|
+
static Napi::Value CtxSetStrokeColor(const Napi::CallbackInfo& info) {
|
|
1260
|
+
CGContextSetRGBStrokeColor(SurfaceFrom(info[0])->ctx,
|
|
1261
|
+
info[1].As<Napi::Number>().DoubleValue(),
|
|
1262
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1263
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1264
|
+
info[4].As<Napi::Number>().DoubleValue());
|
|
1265
|
+
return info.Env().Undefined();
|
|
1266
|
+
}
|
|
1267
|
+
static Napi::Value CtxSetLineWidth(const Napi::CallbackInfo& info) {
|
|
1268
|
+
CGContextSetLineWidth(SurfaceFrom(info[0])->ctx,
|
|
1269
|
+
info[1].As<Napi::Number>().DoubleValue());
|
|
1270
|
+
return info.Env().Undefined();
|
|
1271
|
+
}
|
|
1272
|
+
static Napi::Value CtxSetGlobalAlpha(const Napi::CallbackInfo& info) {
|
|
1273
|
+
CGContextSetAlpha(SurfaceFrom(info[0])->ctx,
|
|
1274
|
+
info[1].As<Napi::Number>().DoubleValue());
|
|
1275
|
+
return info.Env().Undefined();
|
|
1276
|
+
}
|
|
1277
|
+
static Napi::Value CtxSetLineCap(const Napi::CallbackInfo& info) {
|
|
1278
|
+
std::string cap = info[1].As<Napi::String>().Utf8Value();
|
|
1279
|
+
CGContextSetLineCap(SurfaceFrom(info[0])->ctx,
|
|
1280
|
+
cap == "round" ? kCGLineCapRound
|
|
1281
|
+
: cap == "square" ? kCGLineCapSquare
|
|
1282
|
+
: kCGLineCapButt);
|
|
1283
|
+
return info.Env().Undefined();
|
|
1284
|
+
}
|
|
1285
|
+
static Napi::Value CtxSetLineJoin(const Napi::CallbackInfo& info) {
|
|
1286
|
+
std::string join = info[1].As<Napi::String>().Utf8Value();
|
|
1287
|
+
CGContextSetLineJoin(SurfaceFrom(info[0])->ctx,
|
|
1288
|
+
join == "round" ? kCGLineJoinRound
|
|
1289
|
+
: join == "bevel" ? kCGLineJoinBevel
|
|
1290
|
+
: kCGLineJoinMiter);
|
|
1291
|
+
return info.Env().Undefined();
|
|
1292
|
+
}
|
|
1293
|
+
static Napi::Value CtxSetLineDash(const Napi::CallbackInfo& info) {
|
|
1294
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1295
|
+
Napi::Array a = info[1].As<Napi::Array>();
|
|
1296
|
+
double offset =
|
|
1297
|
+
info.Length() > 2 ? info[2].As<Napi::Number>().DoubleValue() : 0;
|
|
1298
|
+
std::vector<CGFloat> lengths;
|
|
1299
|
+
for (uint32_t i = 0; i < a.Length(); i++)
|
|
1300
|
+
lengths.push_back(a.Get(i).As<Napi::Number>().DoubleValue());
|
|
1301
|
+
CGContextSetLineDash(s->ctx, offset, lengths.empty() ? NULL : lengths.data(),
|
|
1302
|
+
lengths.size());
|
|
1303
|
+
return info.Env().Undefined();
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Canvas keeps the path across fill/stroke/clip; CG consumes it. Copy before,
|
|
1307
|
+
// re-add after — the CTM is unchanged in between, so the round trip is exact.
|
|
1308
|
+
static void KeepPathAround(CGContextRef ctx, void (^op)(void)) {
|
|
1309
|
+
CGPathRef kept = CGContextCopyPath(ctx);
|
|
1310
|
+
op();
|
|
1311
|
+
if (kept) {
|
|
1312
|
+
CGContextAddPath(ctx, kept);
|
|
1313
|
+
CGPathRelease(kept);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
static Napi::Value CtxFill(const Napi::CallbackInfo& info) {
|
|
1318
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1319
|
+
bool evenOdd = info.Length() > 1 && info[1].ToBoolean().Value();
|
|
1320
|
+
KeepPathAround(s->ctx, ^{
|
|
1321
|
+
if (evenOdd) CGContextEOFillPath(s->ctx);
|
|
1322
|
+
else CGContextFillPath(s->ctx);
|
|
1323
|
+
});
|
|
1324
|
+
return info.Env().Undefined();
|
|
1325
|
+
}
|
|
1326
|
+
static Napi::Value CtxStroke(const Napi::CallbackInfo& info) {
|
|
1327
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1328
|
+
KeepPathAround(s->ctx, ^{ CGContextStrokePath(s->ctx); });
|
|
1329
|
+
return info.Env().Undefined();
|
|
1330
|
+
}
|
|
1331
|
+
static Napi::Value CtxClip(const Napi::CallbackInfo& info) {
|
|
1332
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1333
|
+
KeepPathAround(s->ctx, ^{ CGContextClip(s->ctx); });
|
|
1334
|
+
return info.Env().Undefined();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
static Napi::Value CtxFillRect(const Napi::CallbackInfo& info) {
|
|
1338
|
+
CGContextFillRect(SurfaceFrom(info[0])->ctx,
|
|
1339
|
+
CGRectMake(info[1].As<Napi::Number>().DoubleValue(),
|
|
1340
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1341
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1342
|
+
info[4].As<Napi::Number>().DoubleValue()));
|
|
1343
|
+
return info.Env().Undefined();
|
|
1344
|
+
}
|
|
1345
|
+
static Napi::Value CtxStrokeRect(const Napi::CallbackInfo& info) {
|
|
1346
|
+
CGContextStrokeRect(SurfaceFrom(info[0])->ctx,
|
|
1347
|
+
CGRectMake(info[1].As<Napi::Number>().DoubleValue(),
|
|
1348
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1349
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1350
|
+
info[4].As<Napi::Number>().DoubleValue()));
|
|
1351
|
+
return info.Env().Undefined();
|
|
1352
|
+
}
|
|
1353
|
+
static Napi::Value CtxClearRect(const Napi::CallbackInfo& info) {
|
|
1354
|
+
CGContextClearRect(SurfaceFrom(info[0])->ctx,
|
|
1355
|
+
CGRectMake(info[1].As<Napi::Number>().DoubleValue(),
|
|
1356
|
+
info[2].As<Napi::Number>().DoubleValue(),
|
|
1357
|
+
info[3].As<Napi::Number>().DoubleValue(),
|
|
1358
|
+
info[4].As<Napi::Number>().DoubleValue()));
|
|
1359
|
+
return info.Env().Undefined();
|
|
1360
|
+
}
|
|
1361
|
+
// fillRects(surface, flat [x,y,w,h,...]) — one call for a batch of fills.
|
|
1362
|
+
static Napi::Value CtxFillRects(const Napi::CallbackInfo& info) {
|
|
1363
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1364
|
+
Napi::Array a = info[1].As<Napi::Array>();
|
|
1365
|
+
std::vector<CGRect> rects;
|
|
1366
|
+
for (uint32_t i = 0; i + 3 < a.Length(); i += 4) {
|
|
1367
|
+
rects.push_back(CGRectMake(a.Get(i).As<Napi::Number>().DoubleValue(),
|
|
1368
|
+
a.Get(i + 1).As<Napi::Number>().DoubleValue(),
|
|
1369
|
+
a.Get(i + 2).As<Napi::Number>().DoubleValue(),
|
|
1370
|
+
a.Get(i + 3).As<Napi::Number>().DoubleValue()));
|
|
1371
|
+
}
|
|
1372
|
+
if (!rects.empty()) CGContextFillRects(s->ctx, rects.data(), rects.size());
|
|
1373
|
+
return info.Env().Undefined();
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// fillLinearGradient(surface, x0, y0, x1, y1, stops [offset,r,g,b,a,...],
|
|
1377
|
+
// mode: 0 = fill current path, 1 = fill rect args follow)
|
|
1378
|
+
static Napi::Value CtxFillLinearGradient(const Napi::CallbackInfo& info) {
|
|
1379
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1380
|
+
double x0 = info[1].As<Napi::Number>().DoubleValue();
|
|
1381
|
+
double y0 = info[2].As<Napi::Number>().DoubleValue();
|
|
1382
|
+
double x1 = info[3].As<Napi::Number>().DoubleValue();
|
|
1383
|
+
double y1 = info[4].As<Napi::Number>().DoubleValue();
|
|
1384
|
+
Napi::Array stopsArr = info[5].As<Napi::Array>();
|
|
1385
|
+
std::vector<CGFloat> locs;
|
|
1386
|
+
std::vector<CGFloat> comps;
|
|
1387
|
+
for (uint32_t i = 0; i + 4 < stopsArr.Length(); i += 5) {
|
|
1388
|
+
locs.push_back(stopsArr.Get(i).As<Napi::Number>().DoubleValue());
|
|
1389
|
+
comps.push_back(stopsArr.Get(i + 1).As<Napi::Number>().DoubleValue());
|
|
1390
|
+
comps.push_back(stopsArr.Get(i + 2).As<Napi::Number>().DoubleValue());
|
|
1391
|
+
comps.push_back(stopsArr.Get(i + 3).As<Napi::Number>().DoubleValue());
|
|
1392
|
+
comps.push_back(stopsArr.Get(i + 4).As<Napi::Number>().DoubleValue());
|
|
1393
|
+
}
|
|
1394
|
+
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
1395
|
+
CGGradientRef grad = CGGradientCreateWithColorComponents(
|
|
1396
|
+
cs, comps.data(), locs.data(), locs.size());
|
|
1397
|
+
CGColorSpaceRelease(cs);
|
|
1398
|
+
CGContextSaveGState(s->ctx);
|
|
1399
|
+
if (info.Length() > 6 && info[6].IsNumber()) {
|
|
1400
|
+
// clip to the given rect (fillRect with a gradient fillStyle)
|
|
1401
|
+
CGContextClipToRect(s->ctx,
|
|
1402
|
+
CGRectMake(info[6].As<Napi::Number>().DoubleValue(),
|
|
1403
|
+
info[7].As<Napi::Number>().DoubleValue(),
|
|
1404
|
+
info[8].As<Napi::Number>().DoubleValue(),
|
|
1405
|
+
info[9].As<Napi::Number>().DoubleValue()));
|
|
1406
|
+
} else {
|
|
1407
|
+
KeepPathAround(s->ctx, ^{ CGContextClip(s->ctx); });
|
|
1408
|
+
}
|
|
1409
|
+
CGContextDrawLinearGradient(
|
|
1410
|
+
s->ctx, grad, CGPointMake(x0, y0), CGPointMake(x1, y1),
|
|
1411
|
+
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
|
|
1412
|
+
CGContextRestoreGState(s->ctx);
|
|
1413
|
+
CGGradientRelease(grad);
|
|
1414
|
+
return info.Env().Undefined();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// drawSurface(dst, src, sx, sy, sw, sh, dx, dy, dw, dh)
|
|
1418
|
+
static Napi::Value CtxDrawSurface(const Napi::CallbackInfo& info) {
|
|
1419
|
+
CALSurface* dst = SurfaceFrom(info[0]);
|
|
1420
|
+
CALSurface* src = SurfaceFrom(info[1]);
|
|
1421
|
+
double sx = info[2].As<Napi::Number>().DoubleValue();
|
|
1422
|
+
double sy = info[3].As<Napi::Number>().DoubleValue();
|
|
1423
|
+
double sw = info[4].As<Napi::Number>().DoubleValue();
|
|
1424
|
+
double sh = info[5].As<Napi::Number>().DoubleValue();
|
|
1425
|
+
double dx = info[6].As<Napi::Number>().DoubleValue();
|
|
1426
|
+
double dy = info[7].As<Napi::Number>().DoubleValue();
|
|
1427
|
+
double dw = info[8].As<Napi::Number>().DoubleValue();
|
|
1428
|
+
double dh = info[9].As<Napi::Number>().DoubleValue();
|
|
1429
|
+
CGImageRef whole = CGBitmapContextCreateImage(src->ctx);
|
|
1430
|
+
if (!whole) return info.Env().Undefined();
|
|
1431
|
+
CGImageRef part = whole;
|
|
1432
|
+
bool cropped = false;
|
|
1433
|
+
if (sx != 0 || sy != 0 || sw != (double)src->width ||
|
|
1434
|
+
sh != (double)src->height) {
|
|
1435
|
+
part = CGImageCreateWithImageInRect(whole, CGRectMake(sx, sy, sw, sh));
|
|
1436
|
+
cropped = true;
|
|
1437
|
+
}
|
|
1438
|
+
if (part) {
|
|
1439
|
+
// the base CTM is flipped; flip back around the destination rect so the
|
|
1440
|
+
// image lands upright
|
|
1441
|
+
CGContextSaveGState(dst->ctx);
|
|
1442
|
+
CGContextTranslateCTM(dst->ctx, dx, dy + dh);
|
|
1443
|
+
CGContextScaleCTM(dst->ctx, 1, -1);
|
|
1444
|
+
CGContextDrawImage(dst->ctx, CGRectMake(0, 0, dw, dh), part);
|
|
1445
|
+
CGContextRestoreGState(dst->ctx);
|
|
1446
|
+
}
|
|
1447
|
+
if (cropped && part) CGImageRelease(part);
|
|
1448
|
+
CGImageRelease(whole);
|
|
1449
|
+
return info.Env().Undefined();
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// putImageData(surface, buffer RGBA straight, w, h, dx, dy) — writes pixels
|
|
1453
|
+
// directly, transform- and clip-free, per the canvas contract.
|
|
1454
|
+
static Napi::Value CtxPutImageData(const Napi::CallbackInfo& info) {
|
|
1455
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1456
|
+
Napi::Buffer<uint8_t> buf = info[1].As<Napi::Buffer<uint8_t>>();
|
|
1457
|
+
long w = info[2].As<Napi::Number>().Int64Value();
|
|
1458
|
+
long h = info[3].As<Napi::Number>().Int64Value();
|
|
1459
|
+
long dx = info[4].As<Napi::Number>().Int64Value();
|
|
1460
|
+
long dy = info[5].As<Napi::Number>().Int64Value();
|
|
1461
|
+
uint8_t* dst = (uint8_t*)CGBitmapContextGetData(s->ctx);
|
|
1462
|
+
size_t stride = CGBitmapContextGetBytesPerRow(s->ctx);
|
|
1463
|
+
if (!dst) return info.Env().Undefined();
|
|
1464
|
+
const uint8_t* src = buf.Data();
|
|
1465
|
+
for (long row = 0; row < h; row++) {
|
|
1466
|
+
long ty = dy + row;
|
|
1467
|
+
if (ty < 0 || ty >= (long)s->height) continue;
|
|
1468
|
+
for (long col = 0; col < w; col++) {
|
|
1469
|
+
long tx = dx + col;
|
|
1470
|
+
if (tx < 0 || tx >= (long)s->width) continue;
|
|
1471
|
+
const uint8_t* p = src + (row * w + col) * 4;
|
|
1472
|
+
uint8_t r = p[0], g = p[1], b = p[2], a = p[3];
|
|
1473
|
+
// premultiply, stored little-endian BGRA (ByteOrder32Host + AlphaFirst)
|
|
1474
|
+
uint8_t* q = dst + ty * stride + tx * 4;
|
|
1475
|
+
q[0] = (uint8_t)((b * a + 127) / 255);
|
|
1476
|
+
q[1] = (uint8_t)((g * a + 127) / 255);
|
|
1477
|
+
q[2] = (uint8_t)((r * a + 127) / 255);
|
|
1478
|
+
q[3] = a;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return info.Env().Undefined();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// getImageData(surface, x, y, w, h) -> Buffer RGBA straight
|
|
1485
|
+
static Napi::Value CtxGetImageData(const Napi::CallbackInfo& info) {
|
|
1486
|
+
Napi::Env env = info.Env();
|
|
1487
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1488
|
+
long x = info[1].As<Napi::Number>().Int64Value();
|
|
1489
|
+
long y = info[2].As<Napi::Number>().Int64Value();
|
|
1490
|
+
long w = info[3].As<Napi::Number>().Int64Value();
|
|
1491
|
+
long h = info[4].As<Napi::Number>().Int64Value();
|
|
1492
|
+
Napi::Buffer<uint8_t> out = Napi::Buffer<uint8_t>::New(env, (size_t)(w * h * 4));
|
|
1493
|
+
uint8_t* dst = out.Data();
|
|
1494
|
+
const uint8_t* srcBase = (const uint8_t*)CGBitmapContextGetData(s->ctx);
|
|
1495
|
+
size_t stride = CGBitmapContextGetBytesPerRow(s->ctx);
|
|
1496
|
+
for (long row = 0; row < h; row++) {
|
|
1497
|
+
long sy = y + row;
|
|
1498
|
+
for (long col = 0; col < w; col++) {
|
|
1499
|
+
long sx = x + col;
|
|
1500
|
+
uint8_t* q = dst + (row * w + col) * 4;
|
|
1501
|
+
if (!srcBase || sx < 0 || sy < 0 || sx >= (long)s->width ||
|
|
1502
|
+
sy >= (long)s->height) {
|
|
1503
|
+
q[0] = q[1] = q[2] = q[3] = 0;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const uint8_t* p = srcBase + sy * stride + sx * 4;
|
|
1507
|
+
uint8_t b = p[0], g = p[1], r = p[2], a = p[3];
|
|
1508
|
+
if (a == 0) {
|
|
1509
|
+
q[0] = q[1] = q[2] = q[3] = 0;
|
|
1510
|
+
} else {
|
|
1511
|
+
q[0] = (uint8_t)std::min(255l, (long)r * 255 / a);
|
|
1512
|
+
q[1] = (uint8_t)std::min(255l, (long)g * 255 / a);
|
|
1513
|
+
q[2] = (uint8_t)std::min(255l, (long)b * 255 / a);
|
|
1514
|
+
q[3] = a;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return out;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// surfaceToLayer(surface, layer) — hand the bitmap to a layer as contents.
|
|
1522
|
+
// CGBitmapContextCreateImage is copy-on-write, so this is cheap per frame.
|
|
1523
|
+
static Napi::Value SurfaceToLayer(const Napi::CallbackInfo& info) {
|
|
1524
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1525
|
+
CALayer* L = BDeref<CALayer*>(info[1]);
|
|
1526
|
+
CGImageRef img = CGBitmapContextCreateImage(s->ctx);
|
|
1527
|
+
[CATransaction begin];
|
|
1528
|
+
[CATransaction setDisableActions:YES];
|
|
1529
|
+
L.contents = (__bridge id)img;
|
|
1530
|
+
L.contentsScale = s->scale;
|
|
1531
|
+
[CATransaction commit];
|
|
1532
|
+
CGImageRelease(img);
|
|
1533
|
+
return info.Env().Undefined();
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// scrollSurface(surface, x, y, w, h, dx, dy) — scroll the pixels WITHIN
|
|
1537
|
+
// the rect by (dx, dy), ntk Window.scrollRegion's exact contract: the
|
|
1538
|
+
// destination band is rect ∩ (rect + delta), so nothing is ever written
|
|
1539
|
+
// outside the rect (an upward scroll used to stamp the moved band over
|
|
1540
|
+
// whatever sat above the viewport). Returns whether anything moved.
|
|
1541
|
+
static Napi::Value ScrollSurface(const Napi::CallbackInfo& info) {
|
|
1542
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
1543
|
+
long x = info[1].As<Napi::Number>().Int64Value();
|
|
1544
|
+
long y = info[2].As<Napi::Number>().Int64Value();
|
|
1545
|
+
long w = info[3].As<Napi::Number>().Int64Value();
|
|
1546
|
+
long h = info[4].As<Napi::Number>().Int64Value();
|
|
1547
|
+
long dx = info[5].As<Napi::Number>().Int64Value();
|
|
1548
|
+
long dy = info[6].As<Napi::Number>().Int64Value();
|
|
1549
|
+
uint8_t* base = (uint8_t*)CGBitmapContextGetData(s->ctx);
|
|
1550
|
+
size_t stride = CGBitmapContextGetBytesPerRow(s->ctx);
|
|
1551
|
+
if (!base || (dx == 0 && dy == 0))
|
|
1552
|
+
return Napi::Boolean::New(info.Env(), false);
|
|
1553
|
+
auto clampL = [](long v, long lo, long hi) {
|
|
1554
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
1555
|
+
};
|
|
1556
|
+
long sw = (long)s->width, sh = (long)s->height;
|
|
1557
|
+
long x0 = clampL(x, 0, sw), y0 = clampL(y, 0, sh);
|
|
1558
|
+
long x1 = clampL(x + w, 0, sw), y1 = clampL(y + h, 0, sh);
|
|
1559
|
+
// the band that survives: dest = clamped rect ∩ (clamped rect + delta)
|
|
1560
|
+
long dstX0 = std::max(x0, x0 + dx);
|
|
1561
|
+
long dstY0 = std::max(y0, y0 + dy);
|
|
1562
|
+
long dstX1 = std::min(x1, x1 + dx);
|
|
1563
|
+
long dstY1 = std::min(y1, y1 + dy);
|
|
1564
|
+
if (dstX1 <= dstX0 || dstY1 <= dstY0)
|
|
1565
|
+
return Napi::Boolean::New(info.Env(), false);
|
|
1566
|
+
long copyW = dstX1 - dstX0;
|
|
1567
|
+
if (dy <= 0) {
|
|
1568
|
+
for (long ty = dstY0; ty < dstY1; ty++) {
|
|
1569
|
+
memmove(base + ty * stride + dstX0 * 4,
|
|
1570
|
+
base + (ty - dy) * stride + (dstX0 - dx) * 4,
|
|
1571
|
+
(size_t)copyW * 4);
|
|
1572
|
+
}
|
|
1573
|
+
} else {
|
|
1574
|
+
for (long ty = dstY1 - 1; ty >= dstY0; ty--) {
|
|
1575
|
+
memmove(base + ty * stride + dstX0 * 4,
|
|
1576
|
+
base + (ty - dy) * stride + (dstX0 - dx) * 4,
|
|
1577
|
+
(size_t)copyW * 4);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return Napi::Boolean::New(info.Env(), true);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// ---------------------------------------------------------------------------
|
|
1584
|
+
// fonts + text layout (CoreText)
|
|
1585
|
+
// ---------------------------------------------------------------------------
|
|
1586
|
+
|
|
1587
|
+
// matchFont({ families: [..], size, weight (100-900), italic }) -> font handle
|
|
1588
|
+
static NSFont* ResolveFamily(NSString* family, double size, double weight,
|
|
1589
|
+
bool italic) {
|
|
1590
|
+
NSFontWeight w = NSFontWeightRegular;
|
|
1591
|
+
if (weight <= 150) w = NSFontWeightUltraLight;
|
|
1592
|
+
else if (weight <= 250) w = NSFontWeightThin;
|
|
1593
|
+
else if (weight <= 350) w = NSFontWeightLight;
|
|
1594
|
+
else if (weight <= 450) w = NSFontWeightRegular;
|
|
1595
|
+
else if (weight <= 550) w = NSFontWeightMedium;
|
|
1596
|
+
else if (weight <= 650) w = NSFontWeightSemibold;
|
|
1597
|
+
else if (weight <= 750) w = NSFontWeightBold;
|
|
1598
|
+
else if (weight <= 850) w = NSFontWeightHeavy;
|
|
1599
|
+
else w = NSFontWeightBlack;
|
|
1600
|
+
|
|
1601
|
+
NSFont* font = nil;
|
|
1602
|
+
NSString* lower = family.lowercaseString;
|
|
1603
|
+
if ([lower isEqualToString:@"sans-serif"] ||
|
|
1604
|
+
[lower isEqualToString:@"system-ui"] || [lower isEqualToString:@"ui-sans-serif"]) {
|
|
1605
|
+
font = [NSFont systemFontOfSize:size weight:w];
|
|
1606
|
+
} else if ([lower isEqualToString:@"monospace"] ||
|
|
1607
|
+
[lower isEqualToString:@"ui-monospace"]) {
|
|
1608
|
+
if (@available(macOS 10.15, *)) {
|
|
1609
|
+
font = [NSFont monospacedSystemFontOfSize:size weight:w];
|
|
1610
|
+
} else {
|
|
1611
|
+
font = [NSFont fontWithName:@"Menlo" size:size];
|
|
1612
|
+
}
|
|
1613
|
+
} else if ([lower isEqualToString:@"serif"]) {
|
|
1614
|
+
font = [NSFont fontWithName:@"Times New Roman" size:size];
|
|
1615
|
+
} else if ([lower isEqualToString:@"cursive"]) {
|
|
1616
|
+
font = [NSFont fontWithName:@"Snell Roundhand" size:size];
|
|
1617
|
+
} else {
|
|
1618
|
+
// A named family. Build a descriptor so weight/width participate in
|
|
1619
|
+
// matching; verify the match really is this family (CoreText silently
|
|
1620
|
+
// falls back to Helvetica otherwise, which must read as "not found"
|
|
1621
|
+
// so the next family in the list gets its turn).
|
|
1622
|
+
NSMutableDictionary* traits = [NSMutableDictionary dictionary];
|
|
1623
|
+
traits[NSFontWeightTrait] = @(w);
|
|
1624
|
+
if (italic) traits[NSFontSlantTrait] = @(0.2);
|
|
1625
|
+
NSFontDescriptor* d = [NSFontDescriptor fontDescriptorWithFontAttributes:@{
|
|
1626
|
+
NSFontFamilyAttribute : family,
|
|
1627
|
+
NSFontTraitsAttribute : traits,
|
|
1628
|
+
}];
|
|
1629
|
+
font = [NSFont fontWithDescriptor:d size:size];
|
|
1630
|
+
if (font && ![font.familyName isEqualToString:family] &&
|
|
1631
|
+
![font.familyName.lowercaseString isEqualToString:lower]) {
|
|
1632
|
+
// try by PostScript / display name before giving up
|
|
1633
|
+
NSFont* byName = [NSFont fontWithName:family size:size];
|
|
1634
|
+
font = byName &&
|
|
1635
|
+
([byName.familyName.lowercaseString isEqualToString:lower] ||
|
|
1636
|
+
[byName.fontName.lowercaseString isEqualToString:lower])
|
|
1637
|
+
? byName
|
|
1638
|
+
: nil;
|
|
1639
|
+
}
|
|
1640
|
+
if (font && weight >= 550) {
|
|
1641
|
+
NSFont* bolder = [[NSFontManager sharedFontManager]
|
|
1642
|
+
convertFont:font
|
|
1643
|
+
toHaveTrait:NSBoldFontMask];
|
|
1644
|
+
if (bolder) font = bolder;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (font && italic) {
|
|
1648
|
+
NSFont* it = [[NSFontManager sharedFontManager] convertFont:font
|
|
1649
|
+
toHaveTrait:NSItalicFontMask];
|
|
1650
|
+
if (it) font = it;
|
|
1651
|
+
}
|
|
1652
|
+
return font;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
static Napi::Value MatchFont(const Napi::CallbackInfo& info) {
|
|
1656
|
+
Napi::Env env = info.Env();
|
|
1657
|
+
Napi::Object o = info[0].As<Napi::Object>();
|
|
1658
|
+
double size = BNumOr(o, "size", 14);
|
|
1659
|
+
double weight = BNumOr(o, "weight", 400);
|
|
1660
|
+
bool italic = BBoolOr(o, "italic", false);
|
|
1661
|
+
NSFont* font = nil;
|
|
1662
|
+
if (o.Has("families") && o.Get("families").IsArray()) {
|
|
1663
|
+
Napi::Array fams = o.Get("families").As<Napi::Array>();
|
|
1664
|
+
for (uint32_t i = 0; i < fams.Length() && !font; i++) {
|
|
1665
|
+
if (!fams.Get(i).IsString()) continue;
|
|
1666
|
+
font = ResolveFamily(BToNSString(fams.Get(i)), size, weight, italic);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (!font) {
|
|
1670
|
+
NSFontWeight w = weight >= 550 ? NSFontWeightSemibold : NSFontWeightRegular;
|
|
1671
|
+
font = [NSFont systemFontOfSize:size weight:w];
|
|
1672
|
+
}
|
|
1673
|
+
return BWrapRetained(env, font);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
static Napi::Value FontMetrics(const Napi::CallbackInfo& info) {
|
|
1677
|
+
Napi::Env env = info.Env();
|
|
1678
|
+
NSFont* font = BDeref<NSFont*>(info[0]);
|
|
1679
|
+
CTFontRef ct = (__bridge CTFontRef)font;
|
|
1680
|
+
Napi::Object r = Napi::Object::New(env);
|
|
1681
|
+
r.Set("ascent", CTFontGetAscent(ct));
|
|
1682
|
+
r.Set("descent", CTFontGetDescent(ct));
|
|
1683
|
+
r.Set("leading", CTFontGetLeading(ct));
|
|
1684
|
+
r.Set("capHeight", CTFontGetCapHeight(ct));
|
|
1685
|
+
r.Set("xHeight", CTFontGetXHeight(ct));
|
|
1686
|
+
r.Set("size", CTFontGetSize(ct));
|
|
1687
|
+
r.Set("familyName", font.familyName ? font.familyName.UTF8String : "");
|
|
1688
|
+
r.Set("postScriptName", font.fontName.UTF8String);
|
|
1689
|
+
return r;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
static Napi::Value FontHasGlyph(const Napi::CallbackInfo& info) {
|
|
1693
|
+
NSFont* font = BDeref<NSFont*>(info[0]);
|
|
1694
|
+
std::string ch = info[1].As<Napi::String>().Utf8Value();
|
|
1695
|
+
NSString* s = [NSString stringWithUTF8String:ch.c_str()];
|
|
1696
|
+
if (s.length == 0) return Napi::Boolean::New(info.Env(), false);
|
|
1697
|
+
unichar buf[2];
|
|
1698
|
+
NSUInteger len = std::min((NSUInteger)2, s.length);
|
|
1699
|
+
[s getCharacters:buf range:NSMakeRange(0, len)];
|
|
1700
|
+
CGGlyph glyphs[2];
|
|
1701
|
+
bool ok = CTFontGetGlyphsForCharacters((__bridge CTFontRef)font, buf, glyphs,
|
|
1702
|
+
(CFIndex)len);
|
|
1703
|
+
return Napi::Boolean::New(info.Env(), ok);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// --- direct font handles (custom faces that bypass registry matching) -----
|
|
1707
|
+
|
|
1708
|
+
static double CssWeightOfCTFont(CTFontRef ct) {
|
|
1709
|
+
double weight = 400;
|
|
1710
|
+
CFDictionaryRef traits = CTFontCopyTraits(ct);
|
|
1711
|
+
if (traits) {
|
|
1712
|
+
CFNumberRef w =
|
|
1713
|
+
(CFNumberRef)CFDictionaryGetValue(traits, kCTFontWeightTrait);
|
|
1714
|
+
if (w) {
|
|
1715
|
+
double t = 0;
|
|
1716
|
+
CFNumberGetValue(w, kCFNumberDoubleType, &t);
|
|
1717
|
+
// AppKit's weight trait scale, approximately, back to CSS steps
|
|
1718
|
+
weight = t <= -0.5 ? 200
|
|
1719
|
+
: t <= -0.25 ? 300
|
|
1720
|
+
: t < 0.1 ? 400
|
|
1721
|
+
: t < 0.27 ? 500
|
|
1722
|
+
: t < 0.35 ? 600
|
|
1723
|
+
: t < 0.5 ? 700
|
|
1724
|
+
: t < 0.62 ? 800
|
|
1725
|
+
: 900;
|
|
1726
|
+
}
|
|
1727
|
+
CFRelease(traits);
|
|
1728
|
+
}
|
|
1729
|
+
return weight;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// fontFromData(buffer) -> { cg: External<CGFont>, familyName,
|
|
1733
|
+
// postScriptName, weight, italic }. The CGFont is the process's own handle
|
|
1734
|
+
// to the face — no registry round trip, so a face CoreText refuses to
|
|
1735
|
+
// register (in-memory data) still renders. Registration is attempted as a
|
|
1736
|
+
// best effort so descriptor matching elsewhere can also find it.
|
|
1737
|
+
static Napi::Value FontFromData(const Napi::CallbackInfo& info) {
|
|
1738
|
+
Napi::Env env = info.Env();
|
|
1739
|
+
Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
|
|
1740
|
+
CFDataRef data = CFDataCreate(NULL, buf.Data(), (CFIndex)buf.Length());
|
|
1741
|
+
CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
|
|
1742
|
+
CFRelease(data);
|
|
1743
|
+
CGFontRef cg = provider ? CGFontCreateWithDataProvider(provider) : NULL;
|
|
1744
|
+
if (provider) CGDataProviderRelease(provider);
|
|
1745
|
+
if (!cg) return env.Null();
|
|
1746
|
+
CTFontManagerRegisterGraphicsFont(cg, NULL); // best effort
|
|
1747
|
+
CTFontRef ct = CTFontCreateWithGraphicsFont(cg, 12, NULL, NULL);
|
|
1748
|
+
Napi::Object r = Napi::Object::New(env);
|
|
1749
|
+
r.Set("cg", Napi::External<void>::New(env, (void*)cg, [](Napi::Env, void* d) {
|
|
1750
|
+
CGFontRelease((CGFontRef)d);
|
|
1751
|
+
}));
|
|
1752
|
+
CFStringRef fam = CTFontCopyFamilyName(ct);
|
|
1753
|
+
CFStringRef ps = CTFontCopyPostScriptName(ct);
|
|
1754
|
+
if (fam) {
|
|
1755
|
+
r.Set("familyName", [(__bridge NSString*)fam UTF8String]);
|
|
1756
|
+
CFRelease(fam);
|
|
1757
|
+
}
|
|
1758
|
+
if (ps) {
|
|
1759
|
+
r.Set("postScriptName", [(__bridge NSString*)ps UTF8String]);
|
|
1760
|
+
CFRelease(ps);
|
|
1761
|
+
}
|
|
1762
|
+
r.Set("weight", CssWeightOfCTFont(ct));
|
|
1763
|
+
r.Set("italic",
|
|
1764
|
+
(bool)(CTFontGetSymbolicTraits(ct) & kCTFontTraitItalic));
|
|
1765
|
+
CFRelease(ct);
|
|
1766
|
+
return r;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// cgFontWithSize(cgExternal, size) -> CTFont handle (what layouts take)
|
|
1770
|
+
static Napi::Value CgFontWithSize(const Napi::CallbackInfo& info) {
|
|
1771
|
+
Napi::Env env = info.Env();
|
|
1772
|
+
CGFontRef cg = (CGFontRef)info[0].As<Napi::External<void>>().Data();
|
|
1773
|
+
double size = info[1].As<Napi::Number>().DoubleValue();
|
|
1774
|
+
CTFontRef ct = CTFontCreateWithGraphicsFont(cg, size, NULL, NULL);
|
|
1775
|
+
if (!ct) return env.Null();
|
|
1776
|
+
return Napi::External<void>::New(env, (void*)ct, [](Napi::Env, void* d) {
|
|
1777
|
+
CFRelease(d);
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// fontByPostScriptName(name, size) -> CTFont handle or null. Exact: a
|
|
1782
|
+
// fallback answer (a substituted face) reads as null so the caller can try
|
|
1783
|
+
// the next route.
|
|
1784
|
+
static Napi::Value FontByPostScriptName(const Napi::CallbackInfo& info) {
|
|
1785
|
+
Napi::Env env = info.Env();
|
|
1786
|
+
NSString* name = BToNSString(info[0]);
|
|
1787
|
+
double size = info[1].As<Napi::Number>().DoubleValue();
|
|
1788
|
+
CTFontRef ct =
|
|
1789
|
+
CTFontCreateWithName((__bridge CFStringRef)name, size, NULL);
|
|
1790
|
+
if (!ct) return env.Null();
|
|
1791
|
+
CFStringRef got = CTFontCopyPostScriptName(ct);
|
|
1792
|
+
bool exact = got && [(__bridge NSString*)got isEqualToString:name];
|
|
1793
|
+
if (got) CFRelease(got);
|
|
1794
|
+
if (!exact) {
|
|
1795
|
+
CFRelease(ct);
|
|
1796
|
+
return env.Null();
|
|
1797
|
+
}
|
|
1798
|
+
return Napi::External<void>::New(env, (void*)ct, [](Napi::Env, void* d) {
|
|
1799
|
+
CFRelease(d);
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// fontApplyVariations(ctExternal, { wght: 600, opsz: 28, ... }) -> CTFont
|
|
1804
|
+
static Napi::Value FontApplyVariations(const Napi::CallbackInfo& info) {
|
|
1805
|
+
Napi::Env env = info.Env();
|
|
1806
|
+
CTFontRef base = (CTFontRef)info[0].As<Napi::External<void>>().Data();
|
|
1807
|
+
Napi::Object vars = info[1].As<Napi::Object>();
|
|
1808
|
+
Napi::Array names = vars.GetPropertyNames();
|
|
1809
|
+
NSMutableDictionary* axes = [NSMutableDictionary dictionary];
|
|
1810
|
+
for (uint32_t i = 0; i < names.Length(); i++) {
|
|
1811
|
+
std::string tag = names.Get(i).As<Napi::String>().Utf8Value();
|
|
1812
|
+
if (tag.size() != 4) continue;
|
|
1813
|
+
Napi::Value v = vars.Get(tag.c_str());
|
|
1814
|
+
if (!v.IsNumber()) continue;
|
|
1815
|
+
uint32_t code = ((uint32_t)tag[0] << 24) | ((uint32_t)tag[1] << 16) |
|
|
1816
|
+
((uint32_t)tag[2] << 8) | (uint32_t)tag[3];
|
|
1817
|
+
axes[@(code)] = @(v.As<Napi::Number>().DoubleValue());
|
|
1818
|
+
}
|
|
1819
|
+
if (axes.count == 0) return info[0];
|
|
1820
|
+
CTFontDescriptorRef d = CTFontDescriptorCreateWithAttributes(
|
|
1821
|
+
(__bridge CFDictionaryRef)
|
|
1822
|
+
@{(__bridge id)kCTFontVariationAttribute : axes});
|
|
1823
|
+
CTFontRef ct =
|
|
1824
|
+
CTFontCreateCopyWithAttributes(base, CTFontGetSize(base), NULL, d);
|
|
1825
|
+
CFRelease(d);
|
|
1826
|
+
if (!ct) return info[0];
|
|
1827
|
+
return Napi::External<void>::New(env, (void*)ct, [](Napi::Env, void* d2) {
|
|
1828
|
+
CFRelease(d2);
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// listFonts({ family? , limit? }) -> [{ postScriptName, familyName,
|
|
1833
|
+
// styleName, path }]. With a family: that family's faces, in CoreText's
|
|
1834
|
+
// matching order. Without: every installed face (bounded by limit).
|
|
1835
|
+
static Napi::Value ListFonts(const Napi::CallbackInfo& info) {
|
|
1836
|
+
Napi::Env env = info.Env();
|
|
1837
|
+
Napi::Object o = info.Length() > 0 && info[0].IsObject()
|
|
1838
|
+
? info[0].As<Napi::Object>()
|
|
1839
|
+
: Napi::Object::New(env);
|
|
1840
|
+
long limit = (long)BNumOr(o, "limit", 400);
|
|
1841
|
+
NSString* family = o.Has("family") && o.Get("family").IsString()
|
|
1842
|
+
? BToNSString(o.Get("family"))
|
|
1843
|
+
: nil;
|
|
1844
|
+
CFArrayRef matches = NULL;
|
|
1845
|
+
if (family && family.length > 0) {
|
|
1846
|
+
CTFontDescriptorRef d = CTFontDescriptorCreateWithAttributes(
|
|
1847
|
+
(__bridge CFDictionaryRef)
|
|
1848
|
+
@{(__bridge id)kCTFontFamilyNameAttribute : family});
|
|
1849
|
+
matches = CTFontDescriptorCreateMatchingFontDescriptors(d, NULL);
|
|
1850
|
+
CFRelease(d);
|
|
1851
|
+
} else {
|
|
1852
|
+
CTFontCollectionRef all = CTFontCollectionCreateFromAvailableFonts(NULL);
|
|
1853
|
+
matches = CTFontCollectionCreateMatchingFontDescriptors(all);
|
|
1854
|
+
CFRelease(all);
|
|
1855
|
+
}
|
|
1856
|
+
Napi::Array out = Napi::Array::New(env);
|
|
1857
|
+
if (!matches) return out;
|
|
1858
|
+
CFIndex count = CFArrayGetCount(matches);
|
|
1859
|
+
uint32_t written = 0;
|
|
1860
|
+
for (CFIndex i = 0; i < count && written < (uint32_t)limit; i++) {
|
|
1861
|
+
CTFontDescriptorRef d =
|
|
1862
|
+
(CTFontDescriptorRef)CFArrayGetValueAtIndex(matches, i);
|
|
1863
|
+
Napi::Object row = Napi::Object::New(env);
|
|
1864
|
+
CFStringRef ps = (CFStringRef)CTFontDescriptorCopyAttribute(
|
|
1865
|
+
d, kCTFontNameAttribute);
|
|
1866
|
+
CFStringRef fam = (CFStringRef)CTFontDescriptorCopyAttribute(
|
|
1867
|
+
d, kCTFontFamilyNameAttribute);
|
|
1868
|
+
CFStringRef styleName = (CFStringRef)CTFontDescriptorCopyAttribute(
|
|
1869
|
+
d, kCTFontStyleNameAttribute);
|
|
1870
|
+
CFURLRef url =
|
|
1871
|
+
(CFURLRef)CTFontDescriptorCopyAttribute(d, kCTFontURLAttribute);
|
|
1872
|
+
if (ps) row.Set("postScriptName", [(__bridge NSString*)ps UTF8String]);
|
|
1873
|
+
if (fam) row.Set("familyName", [(__bridge NSString*)fam UTF8String]);
|
|
1874
|
+
if (styleName) row.Set("styleName", [(__bridge NSString*)styleName UTF8String]);
|
|
1875
|
+
if (url) {
|
|
1876
|
+
NSString* path = ((__bridge NSURL*)url).path;
|
|
1877
|
+
if (path) row.Set("path", path.UTF8String);
|
|
1878
|
+
}
|
|
1879
|
+
if (ps) CFRelease(ps);
|
|
1880
|
+
if (fam) CFRelease(fam);
|
|
1881
|
+
if (styleName) CFRelease(styleName);
|
|
1882
|
+
if (url) CFRelease(url);
|
|
1883
|
+
out.Set(written++, row);
|
|
1884
|
+
}
|
|
1885
|
+
CFRelease(matches);
|
|
1886
|
+
return out;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// loadFontData(buffer) -> registers the font with CoreText, returns the
|
|
1890
|
+
// PostScript name (for app-supplied font files — react-x11's loadFont()).
|
|
1891
|
+
static Napi::Value LoadFontData(const Napi::CallbackInfo& info) {
|
|
1892
|
+
Napi::Env env = info.Env();
|
|
1893
|
+
Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
|
|
1894
|
+
CFDataRef data = CFDataCreate(NULL, buf.Data(), (CFIndex)buf.Length());
|
|
1895
|
+
CTFontDescriptorRef desc = CTFontManagerCreateFontDescriptorFromData(data);
|
|
1896
|
+
CFRelease(data);
|
|
1897
|
+
if (!desc) return env.Null();
|
|
1898
|
+
CFErrorRef err = NULL;
|
|
1899
|
+
CTFontManagerRegisterFontDescriptors((__bridge CFArrayRef)@[ (__bridge id)desc ],
|
|
1900
|
+
kCTFontManagerScopeProcess, YES, NULL);
|
|
1901
|
+
(void)err;
|
|
1902
|
+
CTFontRef font = CTFontCreateWithFontDescriptor(desc, 12, NULL);
|
|
1903
|
+
CFRelease(desc);
|
|
1904
|
+
if (!font) return env.Null();
|
|
1905
|
+
CFStringRef ps = CTFontCopyPostScriptName(font);
|
|
1906
|
+
CFStringRef fam = CTFontCopyFamilyName(font);
|
|
1907
|
+
CFRelease(font);
|
|
1908
|
+
Napi::Object r = Napi::Object::New(env);
|
|
1909
|
+
r.Set("postScriptName", [( __bridge NSString*)ps UTF8String]);
|
|
1910
|
+
r.Set("familyName", [( __bridge NSString*)fam UTF8String]);
|
|
1911
|
+
CFRelease(ps);
|
|
1912
|
+
CFRelease(fam);
|
|
1913
|
+
return r;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// --- the layout object -----------------------------------------------------
|
|
1917
|
+
|
|
1918
|
+
struct CALRun {
|
|
1919
|
+
double x = 0, width = 0;
|
|
1920
|
+
long start = 0, end = 0; // UTF-16 units
|
|
1921
|
+
bool rtl = false;
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
struct CALLine {
|
|
1925
|
+
CTLineRef line = nullptr;
|
|
1926
|
+
double x = 0, y = 0, width = 0, height = 0, baseline = 0, ascent = 0,
|
|
1927
|
+
descent = 0;
|
|
1928
|
+
long start = 0, end = 0; // UTF-16 units
|
|
1929
|
+
bool hardBreak = false; // the line ends with a newline it owns
|
|
1930
|
+
std::vector<CALRun> runs;
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
struct CALLayout {
|
|
1934
|
+
std::vector<CALLine> lines;
|
|
1935
|
+
double width = 0, height = 0;
|
|
1936
|
+
~CALLayout() {
|
|
1937
|
+
for (auto& l : lines)
|
|
1938
|
+
if (l.line) CFRelease(l.line);
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
static CALLayout* LayoutFrom(Napi::Value v) {
|
|
1943
|
+
return (CALLayout*)v.As<Napi::External<void>>().Data();
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// createLayout({ spans: [{text, font (handle), color:[r,g,b,a]}],
|
|
1947
|
+
// maxWidth?, align: 0 left | 0.5 center | 1 right,
|
|
1948
|
+
// lineHeight?, maxLines?, ellipsis?, rtl? })
|
|
1949
|
+
// -> { handle, width, height,
|
|
1950
|
+
// lines: [{x,y,width,height,baseline,descent,start,end,
|
|
1951
|
+
// runs:[{x,width,start,end,rtl}]}] }
|
|
1952
|
+
static Napi::Value CreateLayout(const Napi::CallbackInfo& info) {
|
|
1953
|
+
Napi::Env env = info.Env();
|
|
1954
|
+
Napi::Object o = info[0].As<Napi::Object>();
|
|
1955
|
+
double maxWidth = BNumOr(o, "maxWidth", 0);
|
|
1956
|
+
bool bounded = maxWidth > 0 && std::isfinite(maxWidth);
|
|
1957
|
+
double flush = BNumOr(o, "align", 0);
|
|
1958
|
+
double lineHeight = BNumOr(o, "lineHeight", 0);
|
|
1959
|
+
long maxLines = (long)BNumOr(o, "maxLines", 0);
|
|
1960
|
+
bool ellipsis = BBoolOr(o, "ellipsis", false);
|
|
1961
|
+
bool rtl = BBoolOr(o, "rtl", false);
|
|
1962
|
+
if (ellipsis && maxLines <= 0) maxLines = 1;
|
|
1963
|
+
|
|
1964
|
+
NSMutableAttributedString* as = [[NSMutableAttributedString alloc] init];
|
|
1965
|
+
NSDictionary* lastAttrs = nil;
|
|
1966
|
+
Napi::Array spans = o.Get("spans").As<Napi::Array>();
|
|
1967
|
+
for (uint32_t i = 0; i < spans.Length(); i++) {
|
|
1968
|
+
Napi::Object span = spans.Get(i).As<Napi::Object>();
|
|
1969
|
+
NSString* text = span.Has("text") && span.Get("text").IsString()
|
|
1970
|
+
? BToNSString(span.Get("text"))
|
|
1971
|
+
: @"";
|
|
1972
|
+
if (text.length == 0) continue;
|
|
1973
|
+
NSFont* font = BDeref<NSFont*>(span.Get("font"));
|
|
1974
|
+
NSMutableParagraphStyle* para = [[NSMutableParagraphStyle alloc] init];
|
|
1975
|
+
para.baseWritingDirection =
|
|
1976
|
+
rtl ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
|
|
1977
|
+
NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
|
|
1978
|
+
attrs[(__bridge id)kCTFontAttributeName] = font;
|
|
1979
|
+
attrs[NSParagraphStyleAttributeName] = para;
|
|
1980
|
+
if (span.Has("color") && span.Get("color").IsArray()) {
|
|
1981
|
+
CGColorRef color = BMakeColor(span.Get("color"));
|
|
1982
|
+
attrs[(__bridge id)kCTForegroundColorAttributeName] =
|
|
1983
|
+
(__bridge id)color;
|
|
1984
|
+
CGColorRelease(color);
|
|
1985
|
+
} else {
|
|
1986
|
+
// no colour on the span: the glyphs take the drawing context's fill,
|
|
1987
|
+
// exactly like fillText — the contract layout.draw() has on ntk
|
|
1988
|
+
attrs[(__bridge id)kCTForegroundColorFromContextAttributeName] = @YES;
|
|
1989
|
+
}
|
|
1990
|
+
lastAttrs = attrs;
|
|
1991
|
+
[as appendAttributedString:[[NSAttributedString alloc] initWithString:text
|
|
1992
|
+
attributes:attrs]];
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
auto* layout = new CALLayout();
|
|
1996
|
+
long total = (long)as.length;
|
|
1997
|
+
if (total > 0) {
|
|
1998
|
+
CTTypesetterRef ts =
|
|
1999
|
+
CTTypesetterCreateWithAttributedString((__bridge CFAttributedStringRef)as);
|
|
2000
|
+
double y = 0;
|
|
2001
|
+
long start = 0;
|
|
2002
|
+
long lineIndex = 0;
|
|
2003
|
+
double breakWidth = bounded ? maxWidth : 1e9;
|
|
2004
|
+
while (start < total) {
|
|
2005
|
+
long count =
|
|
2006
|
+
(long)CTTypesetterSuggestLineBreak(ts, start, breakWidth);
|
|
2007
|
+
if (count <= 0) count = 1;
|
|
2008
|
+
bool lastAllowed = maxLines > 0 && lineIndex == maxLines - 1;
|
|
2009
|
+
bool more = start + count < total;
|
|
2010
|
+
CTLineRef line = nullptr;
|
|
2011
|
+
long lineEnd = start + count;
|
|
2012
|
+
if (lastAllowed && more && ellipsis && lastAttrs) {
|
|
2013
|
+
// shape the whole remainder, then truncate it into the width
|
|
2014
|
+
CTLineRef whole =
|
|
2015
|
+
CTTypesetterCreateLine(ts, CFRangeMake(start, total - start));
|
|
2016
|
+
NSAttributedString* tokenStr =
|
|
2017
|
+
[[NSAttributedString alloc] initWithString:@"…"
|
|
2018
|
+
attributes:lastAttrs];
|
|
2019
|
+
CTLineRef token = CTLineCreateWithAttributedString(
|
|
2020
|
+
(__bridge CFAttributedStringRef)tokenStr);
|
|
2021
|
+
line = CTLineCreateTruncatedLine(whole, bounded ? maxWidth : 1e9,
|
|
2022
|
+
kCTLineTruncationEnd, token);
|
|
2023
|
+
if (!line) {
|
|
2024
|
+
line = whole;
|
|
2025
|
+
} else {
|
|
2026
|
+
CFRelease(whole);
|
|
2027
|
+
}
|
|
2028
|
+
CFRelease(token);
|
|
2029
|
+
lineEnd = total;
|
|
2030
|
+
} else {
|
|
2031
|
+
line = CTTypesetterCreateLine(ts, CFRangeMake(start, count));
|
|
2032
|
+
}
|
|
2033
|
+
CGFloat ascent = 0, descent = 0, leading = 0;
|
|
2034
|
+
double lw = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
2035
|
+
double natural = ascent + descent + leading;
|
|
2036
|
+
double advance = natural * (lineHeight > 0 ? lineHeight : 1);
|
|
2037
|
+
CALLine L;
|
|
2038
|
+
L.line = line;
|
|
2039
|
+
L.width = lw;
|
|
2040
|
+
L.height = advance;
|
|
2041
|
+
L.ascent = ascent;
|
|
2042
|
+
L.descent = descent;
|
|
2043
|
+
L.y = y;
|
|
2044
|
+
L.baseline = y + ascent;
|
|
2045
|
+
L.start = start;
|
|
2046
|
+
L.end = lineEnd;
|
|
2047
|
+
if (lineEnd > start) {
|
|
2048
|
+
unichar last = [[as string] characterAtIndex:(NSUInteger)(lineEnd - 1)];
|
|
2049
|
+
L.hardBreak =
|
|
2050
|
+
last == '\n' || last == '\r' || last == 0x2028 || last == 0x2029;
|
|
2051
|
+
}
|
|
2052
|
+
if (bounded && flush > 0) {
|
|
2053
|
+
L.x = CTLineGetPenOffsetForFlush(line, flush, maxWidth);
|
|
2054
|
+
}
|
|
2055
|
+
// runs, for selection bands
|
|
2056
|
+
CFArrayRef runs = CTLineGetGlyphRuns(line);
|
|
2057
|
+
for (CFIndex ri = 0; ri < CFArrayGetCount(runs); ri++) {
|
|
2058
|
+
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, ri);
|
|
2059
|
+
CFRange range = CTRunGetStringRange(run);
|
|
2060
|
+
CGFloat rascent, rdescent, rleading;
|
|
2061
|
+
double rwidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0),
|
|
2062
|
+
&rascent, &rdescent,
|
|
2063
|
+
&rleading);
|
|
2064
|
+
double rx = 0;
|
|
2065
|
+
if (CTRunGetGlyphCount(run) > 0) {
|
|
2066
|
+
const CGPoint* positions = CTRunGetPositionsPtr(run);
|
|
2067
|
+
if (positions) {
|
|
2068
|
+
rx = positions[0].x;
|
|
2069
|
+
} else {
|
|
2070
|
+
CGPoint first;
|
|
2071
|
+
CTRunGetPositions(run, CFRangeMake(0, 1), &first);
|
|
2072
|
+
rx = first.x;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
CALRun R;
|
|
2076
|
+
R.x = rx;
|
|
2077
|
+
R.width = rwidth;
|
|
2078
|
+
R.start = range.location;
|
|
2079
|
+
R.end = range.location + range.length;
|
|
2080
|
+
R.rtl = (CTRunGetStatus(run) & kCTRunStatusRightToLeft) != 0;
|
|
2081
|
+
L.runs.push_back(R);
|
|
2082
|
+
}
|
|
2083
|
+
layout->lines.push_back(L);
|
|
2084
|
+
layout->width = std::max(layout->width, lw);
|
|
2085
|
+
y += advance;
|
|
2086
|
+
lineIndex++;
|
|
2087
|
+
start = lineEnd;
|
|
2088
|
+
if (maxLines > 0 && lineIndex >= maxLines) break;
|
|
2089
|
+
}
|
|
2090
|
+
layout->height = y;
|
|
2091
|
+
CFRelease(ts);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
Napi::Object r = Napi::Object::New(env);
|
|
2095
|
+
r.Set("handle", Napi::External<void>::New(env, layout, [](Napi::Env, void* d) {
|
|
2096
|
+
delete (CALLayout*)d;
|
|
2097
|
+
}));
|
|
2098
|
+
r.Set("width", layout->width);
|
|
2099
|
+
r.Set("height", layout->height);
|
|
2100
|
+
Napi::Array lines = Napi::Array::New(env, layout->lines.size());
|
|
2101
|
+
for (size_t i = 0; i < layout->lines.size(); i++) {
|
|
2102
|
+
const CALLine& L = layout->lines[i];
|
|
2103
|
+
Napi::Object lo = Napi::Object::New(env);
|
|
2104
|
+
lo.Set("x", L.x);
|
|
2105
|
+
lo.Set("y", L.y);
|
|
2106
|
+
lo.Set("width", L.width);
|
|
2107
|
+
lo.Set("height", L.height);
|
|
2108
|
+
lo.Set("baseline", L.baseline);
|
|
2109
|
+
lo.Set("ascent", L.ascent);
|
|
2110
|
+
lo.Set("descent", L.descent);
|
|
2111
|
+
lo.Set("start", (double)L.start);
|
|
2112
|
+
lo.Set("end", (double)L.end);
|
|
2113
|
+
Napi::Array runs = Napi::Array::New(env, L.runs.size());
|
|
2114
|
+
for (size_t j = 0; j < L.runs.size(); j++) {
|
|
2115
|
+
const CALRun& R = L.runs[j];
|
|
2116
|
+
Napi::Object ro = Napi::Object::New(env);
|
|
2117
|
+
ro.Set("x", R.x);
|
|
2118
|
+
ro.Set("width", R.width);
|
|
2119
|
+
ro.Set("start", (double)R.start);
|
|
2120
|
+
ro.Set("end", (double)R.end);
|
|
2121
|
+
ro.Set("rtl", R.rtl);
|
|
2122
|
+
runs.Set((uint32_t)j, ro);
|
|
2123
|
+
}
|
|
2124
|
+
lo.Set("runs", runs);
|
|
2125
|
+
lines.Set((uint32_t)i, lo);
|
|
2126
|
+
}
|
|
2127
|
+
r.Set("lines", lines);
|
|
2128
|
+
return r;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// drawLayout(surface, layoutHandle, x, y) — honours the surface CTM and clip.
|
|
2132
|
+
static Napi::Value DrawLayout(const Napi::CallbackInfo& info) {
|
|
2133
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
2134
|
+
CALLayout* layout = LayoutFrom(info[1]);
|
|
2135
|
+
double x = info[2].As<Napi::Number>().DoubleValue();
|
|
2136
|
+
double y = info[3].As<Napi::Number>().DoubleValue();
|
|
2137
|
+
CGContextRef ctx = s->ctx;
|
|
2138
|
+
CGContextSaveGState(ctx);
|
|
2139
|
+
// The base CTM is y-flipped for canvas semantics; text needs unflipping
|
|
2140
|
+
// per glyph run. Standard recipe: flip the text matrix, position each
|
|
2141
|
+
// line at its baseline in the flipped space.
|
|
2142
|
+
CGContextSetTextMatrix(ctx, CGAffineTransformMakeScale(1, -1));
|
|
2143
|
+
for (const CALLine& L : layout->lines) {
|
|
2144
|
+
CGContextSetTextPosition(ctx, x + L.x, y + L.baseline);
|
|
2145
|
+
CTLineDraw(L.line, ctx);
|
|
2146
|
+
}
|
|
2147
|
+
CGContextRestoreGState(ctx);
|
|
2148
|
+
return info.Env().Undefined();
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// drawLayoutGradient(surface, layoutHandle, x, y, x0, y0, x1, y1,
|
|
2152
|
+
// stops [offset,r,g,b,a,...])
|
|
2153
|
+
// The glyph outlines become the clip and a linear gradient fills through
|
|
2154
|
+
// them — gradient text ink, canvas-style.
|
|
2155
|
+
static Napi::Value DrawLayoutGradient(const Napi::CallbackInfo& info) {
|
|
2156
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
2157
|
+
CALLayout* layout = LayoutFrom(info[1]);
|
|
2158
|
+
double x = info[2].As<Napi::Number>().DoubleValue();
|
|
2159
|
+
double y = info[3].As<Napi::Number>().DoubleValue();
|
|
2160
|
+
double gx0 = info[4].As<Napi::Number>().DoubleValue();
|
|
2161
|
+
double gy0 = info[5].As<Napi::Number>().DoubleValue();
|
|
2162
|
+
double gx1 = info[6].As<Napi::Number>().DoubleValue();
|
|
2163
|
+
double gy1 = info[7].As<Napi::Number>().DoubleValue();
|
|
2164
|
+
Napi::Array stopsArr = info[8].As<Napi::Array>();
|
|
2165
|
+
std::vector<CGFloat> locs;
|
|
2166
|
+
std::vector<CGFloat> comps;
|
|
2167
|
+
for (uint32_t i = 0; i + 4 < stopsArr.Length(); i += 5) {
|
|
2168
|
+
locs.push_back(stopsArr.Get(i).As<Napi::Number>().DoubleValue());
|
|
2169
|
+
for (uint32_t c = 1; c <= 4; c++)
|
|
2170
|
+
comps.push_back(stopsArr.Get(i + c).As<Napi::Number>().DoubleValue());
|
|
2171
|
+
}
|
|
2172
|
+
CGContextRef ctx = s->ctx;
|
|
2173
|
+
CGContextSaveGState(ctx);
|
|
2174
|
+
// CTLineDraw saves/restores the graphics state internally, so a clip
|
|
2175
|
+
// accumulated through kCGTextClip is popped with it — the classic trap.
|
|
2176
|
+
// Build the outline path by hand instead: every glyph's path, flipped
|
|
2177
|
+
// around its baseline into this surface's y-down space.
|
|
2178
|
+
CGMutablePathRef outline = CGPathCreateMutable();
|
|
2179
|
+
for (const CALLine& L : layout->lines) {
|
|
2180
|
+
CFArrayRef runs = CTLineGetGlyphRuns(L.line);
|
|
2181
|
+
for (CFIndex ri = 0; ri < CFArrayGetCount(runs); ri++) {
|
|
2182
|
+
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, ri);
|
|
2183
|
+
CFDictionaryRef attrs = CTRunGetAttributes(run);
|
|
2184
|
+
CTFontRef font =
|
|
2185
|
+
(CTFontRef)CFDictionaryGetValue(attrs, kCTFontAttributeName);
|
|
2186
|
+
if (!font) continue;
|
|
2187
|
+
CFIndex count = CTRunGetGlyphCount(run);
|
|
2188
|
+
std::vector<CGGlyph> glyphs((size_t)count);
|
|
2189
|
+
std::vector<CGPoint> positions((size_t)count);
|
|
2190
|
+
CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs.data());
|
|
2191
|
+
CTRunGetPositions(run, CFRangeMake(0, 0), positions.data());
|
|
2192
|
+
for (CFIndex g = 0; g < count; g++) {
|
|
2193
|
+
CGAffineTransform t = {1, 0, 0, -1,
|
|
2194
|
+
x + L.x + positions[(size_t)g].x,
|
|
2195
|
+
y + L.baseline - positions[(size_t)g].y};
|
|
2196
|
+
CGPathRef gp = CTFontCreatePathForGlyph(font, glyphs[(size_t)g], &t);
|
|
2197
|
+
if (gp) {
|
|
2198
|
+
CGPathAddPath(outline, NULL, gp);
|
|
2199
|
+
CGPathRelease(gp);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
CGContextBeginPath(ctx);
|
|
2205
|
+
CGContextAddPath(ctx, outline);
|
|
2206
|
+
CGPathRelease(outline);
|
|
2207
|
+
CGContextClip(ctx);
|
|
2208
|
+
if (!locs.empty()) {
|
|
2209
|
+
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
2210
|
+
CGGradientRef grad = CGGradientCreateWithColorComponents(
|
|
2211
|
+
cs, comps.data(), locs.data(), locs.size());
|
|
2212
|
+
CGColorSpaceRelease(cs);
|
|
2213
|
+
CGContextDrawLinearGradient(ctx, grad, CGPointMake(gx0, gy0),
|
|
2214
|
+
CGPointMake(gx1, gy1),
|
|
2215
|
+
kCGGradientDrawsBeforeStartLocation |
|
|
2216
|
+
kCGGradientDrawsAfterEndLocation);
|
|
2217
|
+
CGGradientRelease(grad);
|
|
2218
|
+
}
|
|
2219
|
+
CGContextRestoreGState(ctx);
|
|
2220
|
+
return info.Env().Undefined();
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// ctxSetShadow(surface, blur, dx, dy, r, g, b, a) — blur <= 0 clears.
|
|
2224
|
+
static Napi::Value CtxSetShadow(const Napi::CallbackInfo& info) {
|
|
2225
|
+
CALSurface* s = SurfaceFrom(info[0]);
|
|
2226
|
+
double blur = info[1].As<Napi::Number>().DoubleValue();
|
|
2227
|
+
if (blur <= 0) {
|
|
2228
|
+
CGContextSetShadowWithColor(s->ctx, CGSizeMake(0, 0), 0, NULL);
|
|
2229
|
+
return info.Env().Undefined();
|
|
2230
|
+
}
|
|
2231
|
+
double dx = info[2].As<Napi::Number>().DoubleValue();
|
|
2232
|
+
double dy = info[3].As<Napi::Number>().DoubleValue();
|
|
2233
|
+
CGColorRef color = CGColorCreateSRGB(
|
|
2234
|
+
info[4].As<Napi::Number>().DoubleValue(),
|
|
2235
|
+
info[5].As<Napi::Number>().DoubleValue(),
|
|
2236
|
+
info[6].As<Napi::Number>().DoubleValue(),
|
|
2237
|
+
info[7].As<Napi::Number>().DoubleValue());
|
|
2238
|
+
// the base CTM is y-flipped, so a downward canvas offset is a negative
|
|
2239
|
+
// CG one
|
|
2240
|
+
CGContextSetShadowWithColor(s->ctx, CGSizeMake(dx, -dy), blur, color);
|
|
2241
|
+
CGColorRelease(color);
|
|
2242
|
+
return info.Env().Undefined();
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// layoutIndexAt(layoutHandle, x, y) -> UTF-16 index
|
|
2246
|
+
static Napi::Value LayoutIndexAt(const Napi::CallbackInfo& info) {
|
|
2247
|
+
CALLayout* layout = LayoutFrom(info[0]);
|
|
2248
|
+
double x = info[1].As<Napi::Number>().DoubleValue();
|
|
2249
|
+
double y = info[2].As<Napi::Number>().DoubleValue();
|
|
2250
|
+
if (layout->lines.empty()) return Napi::Number::New(info.Env(), 0);
|
|
2251
|
+
const CALLine* pick = &layout->lines.back();
|
|
2252
|
+
for (const CALLine& L : layout->lines) {
|
|
2253
|
+
if (y < L.y + L.height) {
|
|
2254
|
+
pick = &L;
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
CFIndex idx =
|
|
2259
|
+
CTLineGetStringIndexForPosition(pick->line, CGPointMake(x - pick->x, 0));
|
|
2260
|
+
if (idx == kCFNotFound) idx = pick->end;
|
|
2261
|
+
// Trailing-newline aware, ntk's contract: a hit at or past the right edge
|
|
2262
|
+
// of a hard-wrapped line answers the end of its VISIBLE content. The index
|
|
2263
|
+
// after the newline is the next line's start, and a caret sent there has
|
|
2264
|
+
// visually not moved — vertical arrow movement then sticks on the
|
|
2265
|
+
// boundary instead of climbing.
|
|
2266
|
+
if (pick->hardBreak && idx >= pick->end) idx = pick->end - 1;
|
|
2267
|
+
return Napi::Number::New(info.Env(), (double)idx);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// layoutCaret(layoutHandle, utf16Index) -> { x, y, height, line }
|
|
2271
|
+
// `line` is the line INDEX — the field ntk's caretPosition contract carries
|
|
2272
|
+
// and vertical caret movement steps by (lines[pos.line + delta]); without
|
|
2273
|
+
// it an arrow-down in a textarea indexes lines[NaN].
|
|
2274
|
+
static Napi::Value LayoutCaret(const Napi::CallbackInfo& info) {
|
|
2275
|
+
Napi::Env env = info.Env();
|
|
2276
|
+
CALLayout* layout = LayoutFrom(info[0]);
|
|
2277
|
+
long idx = info[1].As<Napi::Number>().Int64Value();
|
|
2278
|
+
Napi::Object r = Napi::Object::New(env);
|
|
2279
|
+
if (layout->lines.empty()) {
|
|
2280
|
+
r.Set("x", 0);
|
|
2281
|
+
r.Set("y", 0);
|
|
2282
|
+
r.Set("height", 0);
|
|
2283
|
+
r.Set("line", 0);
|
|
2284
|
+
return r;
|
|
2285
|
+
}
|
|
2286
|
+
size_t li = layout->lines.size() - 1;
|
|
2287
|
+
for (size_t i = 0; i < layout->lines.size(); i++) {
|
|
2288
|
+
const CALLine& L = layout->lines[i];
|
|
2289
|
+
// an index at a line's end belongs to that line, not the next one's start
|
|
2290
|
+
if (idx < L.end || (idx == L.end && i == layout->lines.size() - 1)) {
|
|
2291
|
+
li = i;
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const CALLine* pick = &layout->lines[li];
|
|
2296
|
+
double x = CTLineGetOffsetForStringIndex(pick->line, idx, NULL);
|
|
2297
|
+
r.Set("x", pick->x + x);
|
|
2298
|
+
r.Set("y", pick->y);
|
|
2299
|
+
r.Set("height", pick->height);
|
|
2300
|
+
r.Set("line", (double)li);
|
|
2301
|
+
return r;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// ---------------------------------------------------------------------------
|
|
2305
|
+
// pasteboard
|
|
2306
|
+
// ---------------------------------------------------------------------------
|
|
2307
|
+
|
|
2308
|
+
static Napi::Value PbWriteTextFn(const Napi::CallbackInfo& info) {
|
|
2309
|
+
NSPasteboard* pb = NSPasteboard.generalPasteboard;
|
|
2310
|
+
[pb clearContents];
|
|
2311
|
+
[pb setString:BToNSString(info[0]) forType:NSPasteboardTypeString];
|
|
2312
|
+
return info.Env().Undefined();
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
static Napi::Value PbReadTextFn(const Napi::CallbackInfo& info) {
|
|
2316
|
+
NSString* s =
|
|
2317
|
+
[NSPasteboard.generalPasteboard stringForType:NSPasteboardTypeString];
|
|
2318
|
+
return s ? Napi::Value(Napi::String::New(info.Env(), s.UTF8String))
|
|
2319
|
+
: Napi::Value(info.Env().Null());
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
static Napi::Value PbClearFn(const Napi::CallbackInfo& info) {
|
|
2323
|
+
[NSPasteboard.generalPasteboard clearContents];
|
|
2324
|
+
return info.Env().Undefined();
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
static Napi::Value PbChangeCountFn(const Napi::CallbackInfo& info) {
|
|
2328
|
+
return Napi::Number::New(info.Env(),
|
|
2329
|
+
(double)NSPasteboard.generalPasteboard.changeCount);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// ---------------------------------------------------------------------------
|
|
2333
|
+
// screens + cursors + appearance
|
|
2334
|
+
// ---------------------------------------------------------------------------
|
|
2335
|
+
|
|
2336
|
+
static Napi::Value ListScreens(const Napi::CallbackInfo& info) {
|
|
2337
|
+
Napi::Env env = info.Env();
|
|
2338
|
+
BEnsureApp();
|
|
2339
|
+
CGFloat top = PrimaryScreenTop();
|
|
2340
|
+
NSArray<NSScreen*>* screens = NSScreen.screens;
|
|
2341
|
+
Napi::Array out = Napi::Array::New(env, screens.count);
|
|
2342
|
+
for (NSUInteger i = 0; i < screens.count; i++) {
|
|
2343
|
+
NSScreen* s = screens[i];
|
|
2344
|
+
Napi::Object o = Napi::Object::New(env);
|
|
2345
|
+
NSRect f = s.frame, v = s.visibleFrame;
|
|
2346
|
+
o.Set("x", f.origin.x);
|
|
2347
|
+
o.Set("y", top - (f.origin.y + f.size.height));
|
|
2348
|
+
o.Set("width", f.size.width);
|
|
2349
|
+
o.Set("height", f.size.height);
|
|
2350
|
+
Napi::Object work = Napi::Object::New(env);
|
|
2351
|
+
work.Set("x", v.origin.x);
|
|
2352
|
+
work.Set("y", top - (v.origin.y + v.size.height));
|
|
2353
|
+
work.Set("width", v.size.width);
|
|
2354
|
+
work.Set("height", v.size.height);
|
|
2355
|
+
o.Set("visible", work);
|
|
2356
|
+
o.Set("scale", s.backingScaleFactor);
|
|
2357
|
+
o.Set("primary", i == 0);
|
|
2358
|
+
out.Set((uint32_t)i, o);
|
|
2359
|
+
}
|
|
2360
|
+
return out;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
static Napi::Value SetCursorFn(const Napi::CallbackInfo& info) {
|
|
2364
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
2365
|
+
NSCursor* c = nil;
|
|
2366
|
+
if (name == "text") c = NSCursor.IBeamCursor;
|
|
2367
|
+
else if (name == "pointer") c = NSCursor.pointingHandCursor;
|
|
2368
|
+
else if (name == "crosshair") c = NSCursor.crosshairCursor;
|
|
2369
|
+
else if (name == "grab") c = NSCursor.openHandCursor;
|
|
2370
|
+
else if (name == "grabbing") c = NSCursor.closedHandCursor;
|
|
2371
|
+
else if (name == "ew-resize" || name == "col-resize")
|
|
2372
|
+
c = NSCursor.resizeLeftRightCursor;
|
|
2373
|
+
else if (name == "ns-resize" || name == "row-resize")
|
|
2374
|
+
c = NSCursor.resizeUpDownCursor;
|
|
2375
|
+
else if (name == "not-allowed") c = NSCursor.operationNotAllowedCursor;
|
|
2376
|
+
else c = NSCursor.arrowCursor;
|
|
2377
|
+
[c set];
|
|
2378
|
+
return info.Env().Undefined();
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// postKeyEvent(win, down, keyCode, chars, modifiers) — synthetic keys for
|
|
2382
|
+
// tests, through the real pump like postMouseEvent.
|
|
2383
|
+
static Napi::Value PostKeyEvent(const Napi::CallbackInfo& info) {
|
|
2384
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
2385
|
+
bool down = info[1].ToBoolean().Value();
|
|
2386
|
+
unsigned short keyCode = (unsigned short)info[2].As<Napi::Number>().Uint32Value();
|
|
2387
|
+
NSString* chars = info.Length() > 3 && info[3].IsString()
|
|
2388
|
+
? BToNSString(info[3])
|
|
2389
|
+
: @"";
|
|
2390
|
+
NSEventModifierFlags flags = 0;
|
|
2391
|
+
if (info.Length() > 4 && info[4].IsObject()) {
|
|
2392
|
+
Napi::Object m = info[4].As<Napi::Object>();
|
|
2393
|
+
if (BBoolOr(m, "shift", false)) flags |= NSEventModifierFlagShift;
|
|
2394
|
+
if (BBoolOr(m, "control", false)) flags |= NSEventModifierFlagControl;
|
|
2395
|
+
if (BBoolOr(m, "option", false)) flags |= NSEventModifierFlagOption;
|
|
2396
|
+
if (BBoolOr(m, "command", false)) flags |= NSEventModifierFlagCommand;
|
|
2397
|
+
}
|
|
2398
|
+
NSEvent* e = [NSEvent keyEventWithType:down ? NSEventTypeKeyDown : NSEventTypeKeyUp
|
|
2399
|
+
location:NSMakePoint(0, 0)
|
|
2400
|
+
modifierFlags:flags
|
|
2401
|
+
timestamp:NSProcessInfo.processInfo.systemUptime
|
|
2402
|
+
windowNumber:win.windowNumber
|
|
2403
|
+
context:nil
|
|
2404
|
+
characters:chars
|
|
2405
|
+
charactersIgnoringModifiers:chars
|
|
2406
|
+
isARepeat:NO
|
|
2407
|
+
keyCode:keyCode];
|
|
2408
|
+
[NSApp postEvent:e atStart:NO];
|
|
2409
|
+
return info.Env().Undefined();
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
// invalidateWindowShadow(win) — a transparent window's shadow is computed
|
|
2414
|
+
// by AppKit from the content's opaque shape; repaints do not recompute it
|
|
2415
|
+
// automatically, so a popup presented after its map keeps whatever shape
|
|
2416
|
+
// AppKit guessed first (a full-frame dark square). Call after presenting.
|
|
2417
|
+
static Napi::Value InvalidateWindowShadow(const Napi::CallbackInfo& info) {
|
|
2418
|
+
NSWindow* win = BDeref<NSWindow*>(info[0]);
|
|
2419
|
+
[win invalidateShadow];
|
|
2420
|
+
return info.Env().Undefined();
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// ---------------------------------------------------------------------------
|
|
2424
|
+
// registration (called from addon.mm's Init)
|
|
2425
|
+
// ---------------------------------------------------------------------------
|
|
2426
|
+
|
|
2427
|
+
void InitBackend(Napi::Env env, Napi::Object exports) {
|
|
2428
|
+
#define BFN(js, fn) exports.Set(js, Napi::Function::New(env, fn))
|
|
2429
|
+
BFN("createWindow2", CreateWindow2);
|
|
2430
|
+
BFN("showWindow", ShowWindowFn);
|
|
2431
|
+
BFN("hideWindow", HideWindowFn);
|
|
2432
|
+
BFN("setWindowTitle", SetWindowTitle);
|
|
2433
|
+
BFN("setWindowFrame", SetWindowFrame);
|
|
2434
|
+
BFN("getWindowFrame", GetWindowFrame);
|
|
2435
|
+
BFN("setWindowMinMax", SetWindowMinMax);
|
|
2436
|
+
BFN("destroyWindow2", DestroyWindow2);
|
|
2437
|
+
BFN("invalidateWindowShadow", InvalidateWindowShadow);
|
|
2438
|
+
BFN("activateApp", ActivateApp);
|
|
2439
|
+
BFN("setBackendEventCallback", SetBackendEventCallback);
|
|
2440
|
+
BFN("pump2", Pump2);
|
|
2441
|
+
BFN("createSurface", CreateSurface);
|
|
2442
|
+
BFN("createSurfaceIOSurface", CreateSurfaceIOSurface);
|
|
2443
|
+
BFN("surfaceFromIOSurfaceID", SurfaceFromIOSurfaceID);
|
|
2444
|
+
BFN("surfaceLock", SurfaceLock);
|
|
2445
|
+
BFN("surfaceUnlock", SurfaceUnlock);
|
|
2446
|
+
BFN("copySurfaceRegion", CopySurfaceRegion);
|
|
2447
|
+
BFN("surfaceSize", SurfaceSize);
|
|
2448
|
+
BFN("ctxSave", CtxSave);
|
|
2449
|
+
BFN("ctxRestore", CtxRestore);
|
|
2450
|
+
BFN("ctxTranslate", CtxTranslate);
|
|
2451
|
+
BFN("ctxScale", CtxScale);
|
|
2452
|
+
BFN("ctxRotate", CtxRotate);
|
|
2453
|
+
BFN("ctxTransform", CtxTransform);
|
|
2454
|
+
BFN("ctxBeginPath", CtxBeginPath);
|
|
2455
|
+
BFN("ctxMoveTo", CtxMoveTo);
|
|
2456
|
+
BFN("ctxLineTo", CtxLineTo);
|
|
2457
|
+
BFN("ctxRect", CtxRect);
|
|
2458
|
+
BFN("ctxRoundRect", CtxRoundRect);
|
|
2459
|
+
BFN("ctxArc", CtxArc);
|
|
2460
|
+
BFN("ctxEllipse", CtxEllipse);
|
|
2461
|
+
BFN("ctxCurveTo", CtxCurveTo);
|
|
2462
|
+
BFN("ctxQuadTo", CtxQuadTo);
|
|
2463
|
+
BFN("ctxClosePath", CtxClosePath);
|
|
2464
|
+
BFN("ctxSetFillColor", CtxSetFillColor);
|
|
2465
|
+
BFN("ctxSetStrokeColor", CtxSetStrokeColor);
|
|
2466
|
+
BFN("ctxSetLineWidth", CtxSetLineWidth);
|
|
2467
|
+
BFN("ctxSetGlobalAlpha", CtxSetGlobalAlpha);
|
|
2468
|
+
BFN("ctxSetLineCap", CtxSetLineCap);
|
|
2469
|
+
BFN("ctxSetLineJoin", CtxSetLineJoin);
|
|
2470
|
+
BFN("ctxSetLineDash", CtxSetLineDash);
|
|
2471
|
+
BFN("ctxFill", CtxFill);
|
|
2472
|
+
BFN("ctxStroke", CtxStroke);
|
|
2473
|
+
BFN("ctxClip", CtxClip);
|
|
2474
|
+
BFN("ctxFillRect", CtxFillRect);
|
|
2475
|
+
BFN("ctxStrokeRect", CtxStrokeRect);
|
|
2476
|
+
BFN("ctxClearRect", CtxClearRect);
|
|
2477
|
+
BFN("ctxFillRects", CtxFillRects);
|
|
2478
|
+
BFN("ctxFillLinearGradient", CtxFillLinearGradient);
|
|
2479
|
+
BFN("ctxDrawSurface", CtxDrawSurface);
|
|
2480
|
+
BFN("ctxPutImageData", CtxPutImageData);
|
|
2481
|
+
BFN("ctxGetImageData", CtxGetImageData);
|
|
2482
|
+
BFN("surfaceToLayer", SurfaceToLayer);
|
|
2483
|
+
BFN("scrollSurface", ScrollSurface);
|
|
2484
|
+
BFN("matchFont", MatchFont);
|
|
2485
|
+
BFN("fontMetrics", FontMetrics);
|
|
2486
|
+
BFN("fontHasGlyph", FontHasGlyph);
|
|
2487
|
+
BFN("fontFromData", FontFromData);
|
|
2488
|
+
BFN("cgFontWithSize", CgFontWithSize);
|
|
2489
|
+
BFN("fontByPostScriptName", FontByPostScriptName);
|
|
2490
|
+
BFN("fontApplyVariations", FontApplyVariations);
|
|
2491
|
+
BFN("drawLayoutGradient", DrawLayoutGradient);
|
|
2492
|
+
BFN("ctxSetShadow", CtxSetShadow);
|
|
2493
|
+
BFN("listFonts", ListFonts);
|
|
2494
|
+
BFN("loadFontData", LoadFontData);
|
|
2495
|
+
BFN("createLayout", CreateLayout);
|
|
2496
|
+
BFN("drawLayout", DrawLayout);
|
|
2497
|
+
BFN("layoutIndexAt", LayoutIndexAt);
|
|
2498
|
+
BFN("layoutCaret", LayoutCaret);
|
|
2499
|
+
BFN("pasteboardWriteText", PbWriteTextFn);
|
|
2500
|
+
BFN("pasteboardReadText", PbReadTextFn);
|
|
2501
|
+
BFN("pasteboardClear", PbClearFn);
|
|
2502
|
+
BFN("pasteboardChangeCount", PbChangeCountFn);
|
|
2503
|
+
BFN("setMainMenu", SetMainMenuFn);
|
|
2504
|
+
BFN("mainMenuInfo", MainMenuInfoFn);
|
|
2505
|
+
BFN("activateMenuItem", ActivateMenuItemFn);
|
|
2506
|
+
BFN("measureControl", MeasureControl);
|
|
2507
|
+
BFN("drawControlIntoSurface", DrawControlIntoSurface);
|
|
2508
|
+
BFN("listScreens", ListScreens);
|
|
2509
|
+
BFN("setCursor", SetCursorFn);
|
|
2510
|
+
BFN("postKeyEvent", PostKeyEvent);
|
|
2511
|
+
#undef BFN
|
|
2512
|
+
}
|