basalt-appkit 0.0.0-rc5
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 +21 -0
- package/cli/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
#import "RnAppKitView.h"
|
|
2
|
+
|
|
3
|
+
#include "ControlMetrics.h"
|
|
4
|
+
#include "Backface.h"
|
|
5
|
+
#include "FocusRing.h"
|
|
6
|
+
#include "ScrollIndicator.h"
|
|
7
|
+
|
|
8
|
+
#include <vector>
|
|
9
|
+
|
|
10
|
+
#import "AppKitTextPeer.h"
|
|
11
|
+
|
|
12
|
+
#include "PointerButtons.h"
|
|
13
|
+
|
|
14
|
+
#include <cmath>
|
|
15
|
+
|
|
16
|
+
#import "RnTextLayout.h"
|
|
17
|
+
|
|
18
|
+
#import <QuartzCore/QuartzCore.h>
|
|
19
|
+
|
|
20
|
+
// Topmost first: AppKit's subviews array is back to front, and a hit test wants
|
|
21
|
+
// the view that is drawn on top of the others.
|
|
22
|
+
//
|
|
23
|
+
// A child is only entered when the point is inside its frame, which means a
|
|
24
|
+
// child that overflows a parent -- React Native's default, since `overflow` is
|
|
25
|
+
// `visible` -- is not reachable through that parent. In practice Fabric's view
|
|
26
|
+
// flattening hoists most such children out to an ancestor that does contain
|
|
27
|
+
// them, so this matters less than it reads; the GTK side has the same limit for
|
|
28
|
+
// the same reason.
|
|
29
|
+
// The spelling React Native uses for the prop, which is also CSS's, so the
|
|
30
|
+
// three hosts' dumps say the same words.
|
|
31
|
+
static const char *RnAppKitPointerEventsName(RnAppKitPointerEvents mode) {
|
|
32
|
+
switch (mode) {
|
|
33
|
+
case RnAppKitPointerEventsNone:
|
|
34
|
+
return "none";
|
|
35
|
+
case RnAppKitPointerEventsBoxNone:
|
|
36
|
+
return "box-none";
|
|
37
|
+
case RnAppKitPointerEventsBoxOnly:
|
|
38
|
+
return "box-only";
|
|
39
|
+
case RnAppKitPointerEventsAuto:
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return "auto";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
RnAppKitView *RnAppKitHitTest(RnAppKitView *root, CGFloat x, CGFloat y) {
|
|
46
|
+
// `pointerEvents: none` takes the view and everything inside it out of hit
|
|
47
|
+
// testing entirely, so the caller's loop carries on to whatever is behind.
|
|
48
|
+
if (root == nil || root.hidden || root.rnPointerEvents == RnAppKitPointerEventsNone) {
|
|
49
|
+
return nil;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The point arrives relative to this view's visible top-left; child frames
|
|
53
|
+
// are in its *bounds* space. For an unscrolled view those are the same, and
|
|
54
|
+
// for a scrolled one they differ by exactly the offset -- which is what makes
|
|
55
|
+
// hit testing follow the scroll without anything here knowing about
|
|
56
|
+
// ScrollViews.
|
|
57
|
+
const NSRect bounds = root.bounds;
|
|
58
|
+
const CGFloat bx = x + bounds.origin.x;
|
|
59
|
+
const CGFloat by = y + bounds.origin.y;
|
|
60
|
+
|
|
61
|
+
if (!NSPointInRect(NSMakePoint(bx, by), bounds)) {
|
|
62
|
+
return nil;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Back to front, so reversed is topmost first -- and "topmost" has to mean
|
|
66
|
+
// what was painted last, which zIndex can change. Win32's hitTest walks its
|
|
67
|
+
// own childrenInPaintOrder the same way round.
|
|
68
|
+
//
|
|
69
|
+
// `box-only` is the one mode that skips this: the box is the target and
|
|
70
|
+
// nothing inside it is, which is what makes an overlay swallow a press meant
|
|
71
|
+
// for a button drawn on top of it.
|
|
72
|
+
if (root.rnPointerEvents != RnAppKitPointerEventsBoxOnly) {
|
|
73
|
+
for (RnAppKitView *child in [root rnChildrenInPaintOrder].reverseObjectEnumerator) {
|
|
74
|
+
// Into the child's own space by inverting what places it in this one.
|
|
75
|
+
// Subtracting the frame origin would be the same thing for an
|
|
76
|
+
// untransformed view and wrong for every other: a rotated view was
|
|
77
|
+
// clickable where it would have been rather than where it is drawn.
|
|
78
|
+
const CGAffineTransform toParent = [child rnLocalToParent];
|
|
79
|
+
|
|
80
|
+
// A transform can be degenerate -- `scale: 0` is legal and draws
|
|
81
|
+
// nothing. There is no point inside something with no area, so it is
|
|
82
|
+
// skipped rather than inverted.
|
|
83
|
+
const CGFloat determinant = toParent.a * toParent.d - toParent.b * toParent.c;
|
|
84
|
+
if (determinant == 0.0) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CGPoint local =
|
|
89
|
+
CGPointApplyAffineTransform(NSMakePoint(bx, by), CGAffineTransformInvert(toParent));
|
|
90
|
+
RnAppKitView *hit = RnAppKitHitTest(child, local.x, local.y);
|
|
91
|
+
if (hit != nil) {
|
|
92
|
+
return hit;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `box-none` is transparent to a press that misses everything inside it.
|
|
98
|
+
// Returning nil rather than the parent is the whole of it: the caller is
|
|
99
|
+
// partway through its own list of children, so the press carries on to the
|
|
100
|
+
// sibling *behind* this view -- which is what the absolutely-positioned
|
|
101
|
+
// overlay this mode exists for is asking for.
|
|
102
|
+
if (root.rnPointerEvents == RnAppKitPointerEventsBoxNone) {
|
|
103
|
+
return nil;
|
|
104
|
+
}
|
|
105
|
+
return root;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A rounded rectangle with a different radius pair at each corner.
|
|
109
|
+
//
|
|
110
|
+
// CGPathAddArcToPoint only draws circular arcs, and React Native's radii are
|
|
111
|
+
// elliptical, so the corners are cubic Beziers. kappa is the usual constant
|
|
112
|
+
// for approximating a quarter ellipse with one curve -- the error is under a
|
|
113
|
+
// thousandth of the radius, which is far below a pixel at any radius an
|
|
114
|
+
// interface uses.
|
|
115
|
+
static CGPathRef RnAppKitCreateRoundedPath(CGRect rect, const CGFloat radii[8]) {
|
|
116
|
+
static const CGFloat kappa = (CGFloat)0.5522847498307936;
|
|
117
|
+
const CGFloat x = CGRectGetMinX(rect);
|
|
118
|
+
const CGFloat y = CGRectGetMinY(rect);
|
|
119
|
+
const CGFloat w = CGRectGetWidth(rect);
|
|
120
|
+
const CGFloat h = CGRectGetHeight(rect);
|
|
121
|
+
|
|
122
|
+
// Clamp so that two radii along one edge can never exceed it. The values
|
|
123
|
+
// arriving from Fabric are already clamped, but this path is also built for
|
|
124
|
+
// the *inner* edge of a border, whose radii are reduced by the border widths
|
|
125
|
+
// and can be anything.
|
|
126
|
+
CGFloat r[8];
|
|
127
|
+
for (int i = 0; i < 8; i++) {
|
|
128
|
+
r[i] = radii[i] > 0 ? radii[i] : 0;
|
|
129
|
+
}
|
|
130
|
+
const CGFloat tlw = r[0], tlh = r[1], trw = r[2], trh = r[3];
|
|
131
|
+
const CGFloat brw = r[4], brh = r[5], blw = r[6], blh = r[7];
|
|
132
|
+
|
|
133
|
+
CGMutablePathRef path = CGPathCreateMutable();
|
|
134
|
+
CGPathMoveToPoint(path, NULL, x + tlw, y);
|
|
135
|
+
CGPathAddLineToPoint(path, NULL, x + w - trw, y);
|
|
136
|
+
CGPathAddCurveToPoint(path, NULL,
|
|
137
|
+
x + w - trw + trw * kappa, y,
|
|
138
|
+
x + w, y + trh - trh * kappa,
|
|
139
|
+
x + w, y + trh);
|
|
140
|
+
CGPathAddLineToPoint(path, NULL, x + w, y + h - brh);
|
|
141
|
+
CGPathAddCurveToPoint(path, NULL,
|
|
142
|
+
x + w, y + h - brh + brh * kappa,
|
|
143
|
+
x + w - brw + brw * kappa, y + h,
|
|
144
|
+
x + w - brw, y + h);
|
|
145
|
+
CGPathAddLineToPoint(path, NULL, x + blw, y + h);
|
|
146
|
+
CGPathAddCurveToPoint(path, NULL,
|
|
147
|
+
x + blw - blw * kappa, y + h,
|
|
148
|
+
x, y + h - blh + blh * kappa,
|
|
149
|
+
x, y + h - blh);
|
|
150
|
+
CGPathAddLineToPoint(path, NULL, x, y + tlh);
|
|
151
|
+
CGPathAddCurveToPoint(path, NULL,
|
|
152
|
+
x, y + tlh - tlh * kappa,
|
|
153
|
+
x + tlw - tlw * kappa, y,
|
|
154
|
+
x + tlw, y);
|
|
155
|
+
CGPathCloseSubpath(path);
|
|
156
|
+
return path;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clips to the side of the line through `p` with direction `dir` that contains
|
|
160
|
+
// `inside`. Two of these give an edge the sector it owns at a corner.
|
|
161
|
+
//
|
|
162
|
+
// A half-plane rather than a polygon because a sector has no far end: the
|
|
163
|
+
// border band at a rounded corner runs as far out as the radius takes it, and
|
|
164
|
+
// any bounded shape has to guess how far that is.
|
|
165
|
+
static void RnAppKitClipToHalfPlane(CGContextRef context,
|
|
166
|
+
CGPoint p,
|
|
167
|
+
CGPoint dir,
|
|
168
|
+
CGPoint inside,
|
|
169
|
+
CGFloat extent) {
|
|
170
|
+
const CGFloat length = std::hypot(dir.x, dir.y);
|
|
171
|
+
if (length < (CGFloat)1e-6) {
|
|
172
|
+
// Both borders meeting here are zero wide, so there is nothing to divide.
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const CGPoint along = {dir.x / length, dir.y / length};
|
|
176
|
+
const CGPoint normal = {-along.y, along.x};
|
|
177
|
+
const CGFloat side =
|
|
178
|
+
(inside.x - p.x) * normal.x + (inside.y - p.y) * normal.y;
|
|
179
|
+
const CGFloat sign = side < 0 ? -1 : 1;
|
|
180
|
+
|
|
181
|
+
const CGPoint a = {p.x + along.x * extent, p.y + along.y * extent};
|
|
182
|
+
const CGPoint b = {p.x - along.x * extent, p.y - along.y * extent};
|
|
183
|
+
const CGPoint c = {b.x + normal.x * sign * extent, b.y + normal.y * sign * extent};
|
|
184
|
+
const CGPoint d = {a.x + normal.x * sign * extent, a.y + normal.y * sign * extent};
|
|
185
|
+
|
|
186
|
+
CGContextBeginPath(context);
|
|
187
|
+
CGContextMoveToPoint(context, a.x, a.y);
|
|
188
|
+
CGContextAddLineToPoint(context, b.x, b.y);
|
|
189
|
+
CGContextAddLineToPoint(context, c.x, c.y);
|
|
190
|
+
CGContextAddLineToPoint(context, d.x, d.y);
|
|
191
|
+
CGContextClosePath(context);
|
|
192
|
+
CGContextClip(context);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The overlay scrollbars, as a view of their own.
|
|
196
|
+
//
|
|
197
|
+
// AppKit paints subviews over their superview, and a ScrollView always has one
|
|
198
|
+
// -- its content -- covering the whole of it, so a thumb drawn in the scroll
|
|
199
|
+
// view's own `drawRect:` would be behind everything it is meant to float over.
|
|
200
|
+
// This sits above them all instead. See core/ScrollIndicator.h for the
|
|
201
|
+
// geometry, which all three hosts share.
|
|
202
|
+
@interface RnAppKitScrollIndicatorView : NSView
|
|
203
|
+
@property(nonatomic) CGFloat rnVerticalOffset;
|
|
204
|
+
@property(nonatomic) CGFloat rnVerticalLength;
|
|
205
|
+
@property(nonatomic) CGFloat rnHorizontalOffset;
|
|
206
|
+
@property(nonatomic) CGFloat rnHorizontalLength;
|
|
207
|
+
@end
|
|
208
|
+
|
|
209
|
+
@implementation RnAppKitScrollIndicatorView
|
|
210
|
+
|
|
211
|
+
// Top-left origin, like every other view here and like React Native.
|
|
212
|
+
- (BOOL)isFlipped {
|
|
213
|
+
return YES;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Paint and nothing else: a press falls through to the content underneath.
|
|
217
|
+
// Nothing here is a drag target yet -- docs/backlog records that.
|
|
218
|
+
- (NSView *)hitTest:(NSPoint)point {
|
|
219
|
+
(void)point;
|
|
220
|
+
return nil;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
- (void)rnFillPill:(NSRect)bar inContext:(CGContextRef)context {
|
|
224
|
+
const CGFloat radius = basalt::kScrollIndicatorThickness / 2.0;
|
|
225
|
+
CGPathRef path = CGPathCreateWithRoundedRect(bar, radius, radius, NULL);
|
|
226
|
+
CGContextAddPath(context, path);
|
|
227
|
+
CGContextFillPath(context);
|
|
228
|
+
CGPathRelease(path);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
232
|
+
(void)dirtyRect;
|
|
233
|
+
if (_rnVerticalLength <= 0.0 && _rnHorizontalLength <= 0.0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
CGContextRef context = [NSGraphicsContext currentContext].CGContext;
|
|
237
|
+
const NSSize size = self.bounds.size;
|
|
238
|
+
const CGFloat thickness = basalt::kScrollIndicatorThickness;
|
|
239
|
+
const CGFloat inset = basalt::kScrollIndicatorInset;
|
|
240
|
+
// Neutral and translucent, so it reads over light and dark content alike --
|
|
241
|
+
// the same colour the GTK and Win32 hosts use.
|
|
242
|
+
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 0.35);
|
|
243
|
+
|
|
244
|
+
if (_rnVerticalLength > 0.0) {
|
|
245
|
+
[self rnFillPill:NSMakeRect(size.width - thickness - inset,
|
|
246
|
+
_rnVerticalOffset,
|
|
247
|
+
thickness,
|
|
248
|
+
_rnVerticalLength)
|
|
249
|
+
inContext:context];
|
|
250
|
+
}
|
|
251
|
+
if (_rnHorizontalLength > 0.0) {
|
|
252
|
+
[self rnFillPill:NSMakeRect(_rnHorizontalOffset,
|
|
253
|
+
size.height - thickness - inset,
|
|
254
|
+
_rnHorizontalLength,
|
|
255
|
+
thickness)
|
|
256
|
+
inContext:context];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@end
|
|
261
|
+
|
|
262
|
+
@implementation RnAppKitView {
|
|
263
|
+
// React DevTools' overlay rectangles: eight floats each, plus a fill flag.
|
|
264
|
+
// See setRnHighlights:filled:count:.
|
|
265
|
+
std::vector<float> _highlights;
|
|
266
|
+
std::vector<bool> _highlightFilled;
|
|
267
|
+
NSString *_roleName;
|
|
268
|
+
RnTextLayout *_textLayout;
|
|
269
|
+
CGImageRef _image;
|
|
270
|
+
NSColor *_imageTint;
|
|
271
|
+
RnAppKitImageFit _imageFit;
|
|
272
|
+
BOOL _hasBackgroundColor;
|
|
273
|
+
CGFloat _backgroundComponents[4];
|
|
274
|
+
CGFloat _opacity;
|
|
275
|
+
BOOL _clipsChildren;
|
|
276
|
+
CGFloat _cornerRadius;
|
|
277
|
+
// Eight floats: a horizontal and a vertical radius per corner, in the order
|
|
278
|
+
// top-left, top-right, bottom-right, bottom-left. React Native clamps
|
|
279
|
+
// opposite corners against the frame before these arrive, so nothing here
|
|
280
|
+
// has to.
|
|
281
|
+
CGFloat _borderRadii[8];
|
|
282
|
+
BOOL _hasBorderRadii;
|
|
283
|
+
// top, right, bottom, left -- the order CSS names them.
|
|
284
|
+
CGFloat _borderWidths[4];
|
|
285
|
+
// Four RGBA quadruples, in the same edge order.
|
|
286
|
+
CGFloat _borderColors[16];
|
|
287
|
+
BOOL _hasBorders;
|
|
288
|
+
CATransform3D _transform;
|
|
289
|
+
BOOL _hasTransform;
|
|
290
|
+
BOOL _hidesBackFace;
|
|
291
|
+
BOOL _hiddenByApp;
|
|
292
|
+
BOOL _hiddenByBackFace;
|
|
293
|
+
NSInteger _zIndex;
|
|
294
|
+
// Only the surface root has one; see -updateTrackingAreas.
|
|
295
|
+
NSTrackingArea *_rnHoverTrackingArea;
|
|
296
|
+
// Only a ScrollView with something to indicate has one.
|
|
297
|
+
RnAppKitScrollIndicatorView *_indicators;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
+ (instancetype)viewWithTag:(NSInteger)tag {
|
|
301
|
+
RnAppKitView *view = [[RnAppKitView alloc] initWithFrame:NSZeroRect];
|
|
302
|
+
view->_rnTag = tag;
|
|
303
|
+
return view;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
- (void)dealloc {
|
|
307
|
+
CGImageRelease(_image);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
- (instancetype)initWithFrame:(NSRect)frame {
|
|
311
|
+
self = [super initWithFrame:frame];
|
|
312
|
+
if (self != nil) {
|
|
313
|
+
_opacity = 1.0;
|
|
314
|
+
_imageFit = RnAppKitImageFitCover;
|
|
315
|
+
_transform = CATransform3DIdentity;
|
|
316
|
+
// Layer-backed from the start rather than on demand: a view that acquires a
|
|
317
|
+
// layer later loses whatever was set on it before, and the props arrive in
|
|
318
|
+
// whatever order the mutation stream happens to carry them.
|
|
319
|
+
self.wantsLayer = YES;
|
|
320
|
+
// Frames come from Fabric already resolved. Autoresizing would fight them.
|
|
321
|
+
self.autoresizingMask = NSViewNotSizable;
|
|
322
|
+
self.translatesAutoresizingMaskIntoConstraints = YES;
|
|
323
|
+
}
|
|
324
|
+
return self;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// See the note in the header: React Native's coordinates are top-left.
|
|
328
|
+
- (BOOL)isFlipped {
|
|
329
|
+
return YES;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
- (void)setRnFrameX:(CGFloat)x y:(CGFloat)y width:(CGFloat)width height:(CGFloat)height {
|
|
333
|
+
self.frame = NSMakeRect(x, y, width, height);
|
|
334
|
+
// A mask layer is built against the bounds, so a resize invalidates it. The
|
|
335
|
+
// uniform case is a plain cornerRadius and needs nothing.
|
|
336
|
+
if (self.layer.mask != nil) {
|
|
337
|
+
[self rnUpdateRadiusMask];
|
|
338
|
+
}
|
|
339
|
+
// Borders are drawn along the bounds too.
|
|
340
|
+
if (_hasBorders) {
|
|
341
|
+
self.needsDisplay = YES;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
- (void)setRnBackgroundColorRed:(CGFloat)red
|
|
346
|
+
green:(CGFloat)green
|
|
347
|
+
blue:(CGFloat)blue
|
|
348
|
+
alpha:(CGFloat)alpha
|
|
349
|
+
hasColor:(BOOL)hasColor {
|
|
350
|
+
_hasBackgroundColor = hasColor;
|
|
351
|
+
_backgroundComponents[0] = red;
|
|
352
|
+
_backgroundComponents[1] = green;
|
|
353
|
+
_backgroundComponents[2] = blue;
|
|
354
|
+
_backgroundComponents[3] = alpha;
|
|
355
|
+
|
|
356
|
+
if (!hasColor) {
|
|
357
|
+
self.layer.backgroundColor = nil;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// sRGB explicitly. React Native's colour components are sRGB, and letting
|
|
361
|
+
// CoreGraphics pick a device space shifts every colour slightly on a
|
|
362
|
+
// wide-gamut display -- visible next to the same app on another platform,
|
|
363
|
+
// which is exactly what this project is trying not to be.
|
|
364
|
+
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
365
|
+
const CGFloat components[4] = {red, green, blue, alpha};
|
|
366
|
+
CGColorRef color = CGColorCreate(space, components);
|
|
367
|
+
self.layer.backgroundColor = color;
|
|
368
|
+
CGColorRelease(color);
|
|
369
|
+
CGColorSpaceRelease(space);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// --- Accessibility ---------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
// React Native's role vocabulary is mostly ARIA's; AppKit's is its own, older
|
|
375
|
+
// and smaller. Several entries below are the closest thing rather than an
|
|
376
|
+
// equivalent, and each says so -- an approximate role is better than none, and
|
|
377
|
+
// much better than a wrong one, which makes a control announce itself as
|
|
378
|
+
// something it is not.
|
|
379
|
+
static NSAccessibilityRole RnAccessibilityRoleFor(NSString *name) {
|
|
380
|
+
static NSDictionary<NSString *, NSAccessibilityRole> *roles = nil;
|
|
381
|
+
static dispatch_once_t once;
|
|
382
|
+
dispatch_once(&once, ^{
|
|
383
|
+
roles = @{
|
|
384
|
+
@"button" : NSAccessibilityButtonRole,
|
|
385
|
+
@"imagebutton" : NSAccessibilityButtonRole,
|
|
386
|
+
@"link" : NSAccessibilityLinkRole,
|
|
387
|
+
// A search field is a text field with a subrole on macOS, not a role of
|
|
388
|
+
// its own. Reporting the role loses the "this searches" part, which is
|
|
389
|
+
// what NSAccessibilitySearchFieldSubrole would carry -- a subrole needs a
|
|
390
|
+
// second property, and this does not set one yet.
|
|
391
|
+
@"search" : NSAccessibilityTextFieldRole,
|
|
392
|
+
@"image" : NSAccessibilityImageRole,
|
|
393
|
+
@"text" : NSAccessibilityStaticTextRole,
|
|
394
|
+
@"adjustable" : NSAccessibilitySliderRole,
|
|
395
|
+
@"checkbox" : NSAccessibilityCheckBoxRole,
|
|
396
|
+
@"combobox" : NSAccessibilityComboBoxRole,
|
|
397
|
+
@"menu" : NSAccessibilityMenuRole,
|
|
398
|
+
@"menubar" : NSAccessibilityMenuBarRole,
|
|
399
|
+
@"menuitem" : NSAccessibilityMenuItemRole,
|
|
400
|
+
@"progressbar" : NSAccessibilityProgressIndicatorRole,
|
|
401
|
+
@"radio" : NSAccessibilityRadioButtonRole,
|
|
402
|
+
@"radiogroup" : NSAccessibilityRadioGroupRole,
|
|
403
|
+
@"scrollbar" : NSAccessibilityScrollBarRole,
|
|
404
|
+
@"spinbutton" : NSAccessibilityIncrementorRole,
|
|
405
|
+
@"list" : NSAccessibilityListRole,
|
|
406
|
+
@"grid" : NSAccessibilityTableRole,
|
|
407
|
+
@"toolbar" : NSAccessibilityToolbarRole,
|
|
408
|
+
@"tooltip" : NSAccessibilityHelpTagRole,
|
|
409
|
+
// macOS has no toggle-button or switch role: VoiceOver announces both as
|
|
410
|
+
// checkboxes, which is also what AppKit's own NSSwitch reports.
|
|
411
|
+
@"togglebutton" : NSAccessibilityCheckBoxRole,
|
|
412
|
+
@"switch" : NSAccessibilityCheckBoxRole,
|
|
413
|
+
// A tab in an AX tab group is a radio button, which reads oddly and is
|
|
414
|
+
// what every Mac application does.
|
|
415
|
+
@"tab" : NSAccessibilityRadioButtonRole,
|
|
416
|
+
@"tablist" : NSAccessibilityTabGroupRole,
|
|
417
|
+
// No standalone header or alert role for a view. A group at least says
|
|
418
|
+
// "these belong together", which a static text would not.
|
|
419
|
+
@"header" : NSAccessibilityGroupRole,
|
|
420
|
+
@"alert" : NSAccessibilityGroupRole,
|
|
421
|
+
// Explicitly nothing: `none` and `presentation` mean "do not announce
|
|
422
|
+
// this", which is handled by isAccessibilityElement below.
|
|
423
|
+
@"none" : NSAccessibilityUnknownRole,
|
|
424
|
+
@"presentation" : NSAccessibilityUnknownRole,
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
NSAccessibilityRole role = roles[name];
|
|
428
|
+
// Anything unrecognised is a group rather than a guess: a wrong role is worse
|
|
429
|
+
// for a screen reader than a vague one, because it makes the view announce
|
|
430
|
+
// itself as something it is not.
|
|
431
|
+
return role != nil ? role : NSAccessibilityGroupRole;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
- (void)setRnAccessibleRole:(NSString *)role {
|
|
435
|
+
_roleName = role.length > 0 ? [role copy] : nil;
|
|
436
|
+
|
|
437
|
+
if (_roleName == nil) {
|
|
438
|
+
self.accessibilityRole = NSAccessibilityGroupRole;
|
|
439
|
+
// A plain <View> is scenery. Leaving every one of them in the tree would
|
|
440
|
+
// bury the handful that mean something under hundreds that do not.
|
|
441
|
+
self.accessibilityElement = NO;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const NSAccessibilityRole mapped = RnAccessibilityRoleFor(_roleName);
|
|
446
|
+
self.accessibilityRole = mapped;
|
|
447
|
+
self.accessibilityElement = mapped != NSAccessibilityUnknownRole;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
- (void)setRnAccessibleLabel:(NSString *)label hint:(NSString *)hint {
|
|
451
|
+
self.accessibilityLabel = label.length > 0 ? label : nil;
|
|
452
|
+
// `accessibilityHelp`, not `accessibilityValue`: React Native's hint is the
|
|
453
|
+
// supplementary description, which on a Mac is what a help tag carries.
|
|
454
|
+
self.accessibilityHelp = hint.length > 0 ? hint : nil;
|
|
455
|
+
|
|
456
|
+
// A label makes a view worth announcing even when it has no role -- which is
|
|
457
|
+
// the common case for an icon-only <Pressable> with an accessibilityLabel.
|
|
458
|
+
if (label.length > 0) {
|
|
459
|
+
self.accessibilityElement = YES;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
- (void)setRnAccessibleStateDisabled:(RnAppKitAccessibleFlag)disabled
|
|
464
|
+
checked:(RnAppKitAccessibleFlag)checked
|
|
465
|
+
selected:(RnAppKitAccessibleFlag)selected
|
|
466
|
+
expanded:(RnAppKitAccessibleFlag)expanded
|
|
467
|
+
busy:(RnAppKitAccessibleFlag)busy {
|
|
468
|
+
if (disabled != RnAppKitAccessibleUnset) {
|
|
469
|
+
self.accessibilityEnabled = disabled != RnAppKitAccessibleTrue;
|
|
470
|
+
}
|
|
471
|
+
if (checked != RnAppKitAccessibleUnset) {
|
|
472
|
+
// AppKit expresses checked as the element's value, the way a checkbox does,
|
|
473
|
+
// rather than as a state of its own.
|
|
474
|
+
self.accessibilityValue = @(checked == RnAppKitAccessibleTrue);
|
|
475
|
+
}
|
|
476
|
+
if (selected != RnAppKitAccessibleUnset) {
|
|
477
|
+
self.accessibilitySelected = selected == RnAppKitAccessibleTrue;
|
|
478
|
+
}
|
|
479
|
+
if (expanded != RnAppKitAccessibleUnset) {
|
|
480
|
+
self.accessibilityExpanded = expanded == RnAppKitAccessibleTrue;
|
|
481
|
+
}
|
|
482
|
+
// `busy` has no AppKit equivalent. React Native's meaning is "this is
|
|
483
|
+
// loading", which VoiceOver has no way to be told about a plain view; the
|
|
484
|
+
// nearest thing is an NSAccessibilityProgressIndicator, which would be a
|
|
485
|
+
// different role rather than a state. Left unreported rather than mapped onto
|
|
486
|
+
// something that means something else.
|
|
487
|
+
(void)busy;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
- (void)setRnAccessibleHidden:(BOOL)hidden {
|
|
491
|
+
if (hidden) {
|
|
492
|
+
self.accessibilityElement = NO;
|
|
493
|
+
}
|
|
494
|
+
// Not `else { YES }`: whether an unhidden view is an element is decided by
|
|
495
|
+
// its role and label above, and overriding that here would put every plain
|
|
496
|
+
// <View> back into the tree.
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
- (void)setRnScrollOffsetX:(CGFloat)x y:(CGFloat)y {
|
|
500
|
+
const NSRect bounds = self.bounds;
|
|
501
|
+
if (bounds.origin.x == x && bounds.origin.y == y) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
[self setBoundsOrigin:NSMakePoint(x, y)];
|
|
505
|
+
// setBoundsOrigin: moves subviews but does not repaint what this view draws
|
|
506
|
+
// itself, which matters the moment a ScrollView has a background or text.
|
|
507
|
+
self.needsDisplay = YES;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
- (NSPoint)rnScrollOffset {
|
|
511
|
+
return self.bounds.origin;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
- (void)setRnScrollIndicatorVerticalOffset:(CGFloat)verticalOffset
|
|
515
|
+
verticalLength:(CGFloat)verticalLength
|
|
516
|
+
horizontalOffset:(CGFloat)horizontalOffset
|
|
517
|
+
horizontalLength:(CGFloat)horizontalLength {
|
|
518
|
+
if (verticalLength <= 0.0 && horizontalLength <= 0.0) {
|
|
519
|
+
[_indicators removeFromSuperview];
|
|
520
|
+
_indicators = nil;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (_indicators == nil) {
|
|
525
|
+
_indicators = [[RnAppKitScrollIndicatorView alloc] initWithFrame:self.bounds];
|
|
526
|
+
[self addSubview:_indicators positioned:NSWindowAbove relativeTo:nil];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// `bounds` rather than a rect at the origin: a scrolled view's bounds origin
|
|
530
|
+
// is the scroll offset, so this pins the overlay to the viewport instead of
|
|
531
|
+
// letting it travel with the content.
|
|
532
|
+
_indicators.frame = self.bounds;
|
|
533
|
+
|
|
534
|
+
if (_indicators.rnVerticalOffset == verticalOffset &&
|
|
535
|
+
_indicators.rnVerticalLength == verticalLength &&
|
|
536
|
+
_indicators.rnHorizontalOffset == horizontalOffset &&
|
|
537
|
+
_indicators.rnHorizontalLength == horizontalLength) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
_indicators.rnVerticalOffset = verticalOffset;
|
|
541
|
+
_indicators.rnVerticalLength = verticalLength;
|
|
542
|
+
_indicators.rnHorizontalOffset = horizontalOffset;
|
|
543
|
+
_indicators.rnHorizontalLength = horizontalLength;
|
|
544
|
+
_indicators.needsDisplay = YES;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
static const char *RnAppKitImageFitName(RnAppKitImageFit fit) {
|
|
548
|
+
switch (fit) {
|
|
549
|
+
case RnAppKitImageFitContain:
|
|
550
|
+
return "contain";
|
|
551
|
+
case RnAppKitImageFitStretch:
|
|
552
|
+
return "stretch";
|
|
553
|
+
case RnAppKitImageFitCenter:
|
|
554
|
+
return "center";
|
|
555
|
+
case RnAppKitImageFitRepeat:
|
|
556
|
+
return "repeat";
|
|
557
|
+
case RnAppKitImageFitCover:
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
return "cover";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
- (void)setRnImageTint:(NSColor *)tint {
|
|
564
|
+
if (_imageTint == tint || [_imageTint isEqual:tint]) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
_imageTint = tint;
|
|
568
|
+
[self setNeedsDisplay:YES];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
- (void)setRnImage:(CGImageRef)image fit:(RnAppKitImageFit)fit {
|
|
572
|
+
if (_image == image && _imageFit == fit) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// Retained, not borrowed: the loader's cache owns one reference and this owns
|
|
576
|
+
// another, so a view outliving an eviction still has pixels to draw.
|
|
577
|
+
CGImageRef previous = _image;
|
|
578
|
+
_image = image != nullptr ? CGImageRetain(image) : nullptr;
|
|
579
|
+
CGImageRelease(previous);
|
|
580
|
+
_imageFit = fit;
|
|
581
|
+
self.needsDisplay = YES;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Where the image lands inside the frame.
|
|
585
|
+
//
|
|
586
|
+
// The same arithmetic as the GTK side's, deliberately -- two platforms
|
|
587
|
+
// disagreeing about what `resizeMode: 'contain'` means would be the sort of
|
|
588
|
+
// difference nobody thinks to check. Duplicated rather than shared because the
|
|
589
|
+
// view layers link no common code at all, which is what keeps them testable
|
|
590
|
+
// without React Native.
|
|
591
|
+
- (NSRect)rnImageRectForSize:(NSSize)size {
|
|
592
|
+
const CGFloat imageWidth = (CGFloat)CGImageGetWidth(_image);
|
|
593
|
+
const CGFloat imageHeight = (CGFloat)CGImageGetHeight(_image);
|
|
594
|
+
if (_imageFit == RnAppKitImageFitStretch || imageWidth <= 0 || imageHeight <= 0) {
|
|
595
|
+
return NSMakeRect(0, 0, size.width, size.height);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
CGFloat scale = 1.0;
|
|
599
|
+
switch (_imageFit) {
|
|
600
|
+
case RnAppKitImageFitContain:
|
|
601
|
+
scale = MIN(size.width / imageWidth, size.height / imageHeight);
|
|
602
|
+
break;
|
|
603
|
+
case RnAppKitImageFitCover:
|
|
604
|
+
scale = MAX(size.width / imageWidth, size.height / imageHeight);
|
|
605
|
+
break;
|
|
606
|
+
case RnAppKitImageFitCenter:
|
|
607
|
+
// Centre at natural size, but never larger than the frame -- which is
|
|
608
|
+
// what React Native's `center` does.
|
|
609
|
+
scale = MIN(1.0, MIN(size.width / imageWidth, size.height / imageHeight));
|
|
610
|
+
break;
|
|
611
|
+
case RnAppKitImageFitRepeat:
|
|
612
|
+
// One tile, at natural size. Where it is placed and how many there are
|
|
613
|
+
// is the draw's business, not this function's.
|
|
614
|
+
break;
|
|
615
|
+
case RnAppKitImageFitStretch:
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const CGFloat width = imageWidth * scale;
|
|
620
|
+
const CGFloat height = imageHeight * scale;
|
|
621
|
+
return NSMakeRect((size.width - width) / 2.0, (size.height - height) / 2.0, width, height);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
- (void)setRnTextLayout:(id)layout {
|
|
625
|
+
_textLayout = (RnTextLayout *)layout;
|
|
626
|
+
// A layer-backed view with a drawRect: gets its contents from that draw, and
|
|
627
|
+
// only redraws when told. Without this the first paragraph appears and no
|
|
628
|
+
// later one ever does.
|
|
629
|
+
self.needsDisplay = YES;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Only views with text draw anything; the rest are pure CALayer properties and
|
|
633
|
+
// this is never called for them.
|
|
634
|
+
//
|
|
635
|
+
// The background is *not* drawn here. It is a layer property, which means Core
|
|
636
|
+
// Animation paints it under this and a view can have both without either
|
|
637
|
+
// knowing about the other.
|
|
638
|
+
- (void)setRnHighlights:(const float *)rectangles
|
|
639
|
+
filled:(const bool *)filled
|
|
640
|
+
count:(NSInteger)count {
|
|
641
|
+
_highlights.clear();
|
|
642
|
+
_highlightFilled.clear();
|
|
643
|
+
if (rectangles != nullptr && count > 0) {
|
|
644
|
+
_highlights.assign(rectangles, rectangles + (count * 8));
|
|
645
|
+
_highlightFilled.assign(filled, filled + count);
|
|
646
|
+
}
|
|
647
|
+
self.needsDisplay = YES;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
651
|
+
(void)dirtyRect;
|
|
652
|
+
const BOOL ring = [self rnShowsFocusRing];
|
|
653
|
+
if (_textLayout == nil && _image == nullptr && !_hasBorders && !ring &&
|
|
654
|
+
_highlightFilled.empty()) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
CGContextRef context = [NSGraphicsContext currentContext].CGContext;
|
|
658
|
+
const NSSize size = self.bounds.size;
|
|
659
|
+
|
|
660
|
+
if (_image != nullptr) {
|
|
661
|
+
const NSRect destination = [self rnImageRectForSize:size];
|
|
662
|
+
|
|
663
|
+
CGContextSaveGState(context);
|
|
664
|
+
// cover, center and repeat can all put pixels outside the frame, and an
|
|
665
|
+
// <Image> never paints beyond its own box on iOS or Android.
|
|
666
|
+
if (_imageFit != RnAppKitImageFitContain && _imageFit != RnAppKitImageFitStretch) {
|
|
667
|
+
CGContextClipToRect(context, NSMakeRect(0, 0, size.width, size.height));
|
|
668
|
+
}
|
|
669
|
+
// CGImage draws bottom-up and this view is flipped, so without this every
|
|
670
|
+
// photograph comes out upside down -- which reads as a broken decoder
|
|
671
|
+
// rather than a coordinate system.
|
|
672
|
+
CGContextTranslateCTM(context, 0, size.height);
|
|
673
|
+
CGContextScaleCTM(context, 1, -1);
|
|
674
|
+
const NSRect drawn = NSMakeRect(destination.origin.x,
|
|
675
|
+
size.height - destination.origin.y - destination.size.height,
|
|
676
|
+
destination.size.width,
|
|
677
|
+
destination.size.height);
|
|
678
|
+
if (_imageFit == RnAppKitImageFitRepeat) {
|
|
679
|
+
// Tiled from the top left at natural size. `CGContextDrawTiledImage`
|
|
680
|
+
// takes the *first* tile's rect and repeats it across the clip, which is
|
|
681
|
+
// why the clip above is what stops it painting the whole window.
|
|
682
|
+
//
|
|
683
|
+
// The first tile is at the top of the view, which in this flipped
|
|
684
|
+
// context is `size.height - tileHeight` -- so a box that is not a whole
|
|
685
|
+
// number of tiles high has its partial tile at the bottom, as CSS
|
|
686
|
+
// `repeat` does, rather than at the top.
|
|
687
|
+
const NSRect tile = NSMakeRect(0,
|
|
688
|
+
size.height - destination.size.height,
|
|
689
|
+
destination.size.width,
|
|
690
|
+
destination.size.height);
|
|
691
|
+
if (_imageTint != nil) {
|
|
692
|
+
CGContextClipToMask(context, tile, _image);
|
|
693
|
+
CGContextSetFillColorWithColor(context, _imageTint.CGColor);
|
|
694
|
+
CGContextFillRect(context, NSMakeRect(0, 0, size.width, size.height));
|
|
695
|
+
} else {
|
|
696
|
+
CGContextDrawTiledImage(context, tile, _image);
|
|
697
|
+
}
|
|
698
|
+
} else if (_imageTint != nil) {
|
|
699
|
+
// The image becomes a stencil and the colour is what is drawn. Clipping
|
|
700
|
+
// to the mask uses the image's alpha, which is what `tintColor` means:
|
|
701
|
+
// recolour the silhouette rather than blend with the pixels.
|
|
702
|
+
CGContextClipToMask(context, drawn, _image);
|
|
703
|
+
CGContextSetFillColorWithColor(context, _imageTint.CGColor);
|
|
704
|
+
CGContextFillRect(context, drawn);
|
|
705
|
+
} else {
|
|
706
|
+
CGContextDrawImage(context, drawn, _image);
|
|
707
|
+
}
|
|
708
|
+
CGContextRestoreGState(context);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Text sits above the image and below any children, which is the order the
|
|
712
|
+
// GTK side paints in too.
|
|
713
|
+
if (_textLayout != nil) {
|
|
714
|
+
[_textLayout drawInContext:context size:size];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Borders paint over the content, as they do on every other platform.
|
|
718
|
+
if (_hasBorders) {
|
|
719
|
+
[self rnDrawBordersInContext:context size:size];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// The focus ring, over everything including the border, because it is the
|
|
723
|
+
// answer to "where am I" and must not be hidden by what it is drawn on.
|
|
724
|
+
//
|
|
725
|
+
// Inside the bounds rather than outside: drawRect: is clipped to them, and a
|
|
726
|
+
// ring that is inset here and outset on Linux would be a difference an app
|
|
727
|
+
// did not ask for. It follows the view's own corner radii, so it hugs a
|
|
728
|
+
// rounded button.
|
|
729
|
+
// React DevTools' overlay, over everything including the border. Above the
|
|
730
|
+
// app on purpose: it is not part of it, and an inspected element half hidden
|
|
731
|
+
// behind a card would be pointing at the wrong thing.
|
|
732
|
+
for (size_t i = 0; i < _highlightFilled.size(); i++) {
|
|
733
|
+
const float *rectangle = _highlights.data() + (i * 8);
|
|
734
|
+
const NSRect area = NSMakeRect(rectangle[0], rectangle[1], rectangle[2], rectangle[3]);
|
|
735
|
+
if (_highlightFilled[i]) {
|
|
736
|
+
CGContextSetRGBFillColor(context, rectangle[4], rectangle[5], rectangle[6], rectangle[7]);
|
|
737
|
+
CGContextFillRect(context, area);
|
|
738
|
+
}
|
|
739
|
+
// Opaque on the outline even when the fill is not, so the edge of an
|
|
740
|
+
// inspected element is a line rather than a suggestion. Stroked down the
|
|
741
|
+
// middle, so the path is inset by half the width to keep it inside.
|
|
742
|
+
CGContextSetRGBStrokeColor(context, rectangle[4], rectangle[5], rectangle[6], 1.0);
|
|
743
|
+
CGContextSetLineWidth(context, basalt::kHighlightBorderWidth);
|
|
744
|
+
const CGFloat inset = basalt::kHighlightBorderWidth / 2.0;
|
|
745
|
+
CGContextStrokeRect(context, NSInsetRect(area, inset, inset));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (ring) {
|
|
749
|
+
[self rnDrawFocusRingInContext:context size:size];
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
- (void)rnDrawFocusRingInContext:(CGContextRef)context size:(NSSize)size {
|
|
754
|
+
const CGFloat width = basalt::kFocusRingWidth;
|
|
755
|
+
// Stroked down the middle of the line, so the path is inset by half of it to
|
|
756
|
+
// keep the whole ring inside the view.
|
|
757
|
+
const NSRect rect = NSMakeRect(width / 2.0, width / 2.0,
|
|
758
|
+
MAX(0.0, size.width - width),
|
|
759
|
+
MAX(0.0, size.height - width));
|
|
760
|
+
CGFloat radii[8];
|
|
761
|
+
for (int i = 0; i < 8; i++) {
|
|
762
|
+
radii[i] = _hasBorderRadii ? MAX(0.0, _borderRadii[i] - width / 2.0) : 0.0;
|
|
763
|
+
}
|
|
764
|
+
CGPathRef path = RnAppKitCreateRoundedPath(rect, radii);
|
|
765
|
+
CGContextSaveGState(context);
|
|
766
|
+
CGContextSetRGBStrokeColor(context,
|
|
767
|
+
basalt::kFocusRingRed,
|
|
768
|
+
basalt::kFocusRingGreen,
|
|
769
|
+
basalt::kFocusRingBlue,
|
|
770
|
+
basalt::kFocusRingAlpha);
|
|
771
|
+
CGContextSetLineWidth(context, width);
|
|
772
|
+
CGContextAddPath(context, path);
|
|
773
|
+
CGContextStrokePath(context);
|
|
774
|
+
CGContextRestoreGState(context);
|
|
775
|
+
CGPathRelease(path);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// The four edges, each its own width and colour.
|
|
779
|
+
//
|
|
780
|
+
// This is CSS's own algorithm rather than a stroke: an edge is the region
|
|
781
|
+
// between the outer and inner rounded rectangles, clipped to a wedge whose
|
|
782
|
+
// sides are the diagonals from the outer corner to the inner one. That is what
|
|
783
|
+
// mitres two edges of different colours against each other, and a stroke
|
|
784
|
+
// cannot do it -- which matters as soon as one edge differs, and is invisible
|
|
785
|
+
// until then.
|
|
786
|
+
- (void)rnDrawBordersInContext:(CGContextRef)context size:(NSSize)size {
|
|
787
|
+
const CGFloat w = size.width;
|
|
788
|
+
const CGFloat h = size.height;
|
|
789
|
+
const CGFloat top = _borderWidths[0];
|
|
790
|
+
const CGFloat right = _borderWidths[1];
|
|
791
|
+
const CGFloat bottom = _borderWidths[2];
|
|
792
|
+
const CGFloat left = _borderWidths[3];
|
|
793
|
+
|
|
794
|
+
CGPathRef outer = RnAppKitCreateRoundedPath(CGRectMake(0, 0, w, h), _borderRadii);
|
|
795
|
+
|
|
796
|
+
// The inner radii shrink by the width of the edges that meet at the corner.
|
|
797
|
+
// A corner whose radius is smaller than its border is square on the inside,
|
|
798
|
+
// which the clamp in the path builder takes care of.
|
|
799
|
+
const CGFloat innerRadii[8] = {
|
|
800
|
+
_borderRadii[0] - left, _borderRadii[1] - top,
|
|
801
|
+
_borderRadii[2] - right, _borderRadii[3] - top,
|
|
802
|
+
_borderRadii[4] - right, _borderRadii[5] - bottom,
|
|
803
|
+
_borderRadii[6] - left, _borderRadii[7] - bottom,
|
|
804
|
+
};
|
|
805
|
+
const CGFloat innerWidth = w - left - right;
|
|
806
|
+
const CGFloat innerHeight = h - top - bottom;
|
|
807
|
+
CGPathRef inner = nullptr;
|
|
808
|
+
if (innerWidth > 0 && innerHeight > 0) {
|
|
809
|
+
inner = RnAppKitCreateRoundedPath(
|
|
810
|
+
CGRectMake(left, top, innerWidth, innerHeight), innerRadii);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// The diagonal at each corner, as a point and a direction, and a point known
|
|
814
|
+
// to be inside each edge's own band.
|
|
815
|
+
//
|
|
816
|
+
// Deliberately not four quadrilaterals stopping at the inner rectangle. That
|
|
817
|
+
// is right only while the corners are square: a radius pushes the border band
|
|
818
|
+
// outside those quads, and the part of the arc beyond them is then inside no
|
|
819
|
+
// wedge at all and never painted. What that looks like is not a missing
|
|
820
|
+
// sliver -- it is a corner whose colour stops early, which reads as a chamfer
|
|
821
|
+
// rather than as a bug.
|
|
822
|
+
//
|
|
823
|
+
// Two half-plane clips per edge instead. Successive clips intersect, so the
|
|
824
|
+
// pair is the sector the edge owns, and a sector is unbounded: it covers the
|
|
825
|
+
// whole arc however large the radius. It also stays correct when the two
|
|
826
|
+
// diagonals cross inside the view, which a quadrilateral cannot -- that
|
|
827
|
+
// happens whenever the border is thick relative to the frame, and it turns a
|
|
828
|
+
// quad into a bowtie.
|
|
829
|
+
const CGPoint corners[4] = {{0, 0}, {w, 0}, {w, h}, {0, h}};
|
|
830
|
+
const CGPoint directions[4] = {
|
|
831
|
+
{left, top}, {-right, top}, {-right, -bottom}, {left, -bottom}};
|
|
832
|
+
// Edge e is bounded by the diagonals at corner e and corner e+1.
|
|
833
|
+
const CGPoint insides[4] = {
|
|
834
|
+
{w / 2, top / 2},
|
|
835
|
+
{w - right / 2, h / 2},
|
|
836
|
+
{w / 2, h - bottom / 2},
|
|
837
|
+
{left / 2, h / 2},
|
|
838
|
+
};
|
|
839
|
+
const CGFloat extent = (w + h) * 4;
|
|
840
|
+
|
|
841
|
+
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
842
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
843
|
+
if (_borderWidths[edge] <= 0 || _borderColors[edge * 4 + 3] <= 0) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
CGContextSaveGState(context);
|
|
847
|
+
|
|
848
|
+
RnAppKitClipToHalfPlane(context, corners[edge], directions[edge], insides[edge], extent);
|
|
849
|
+
RnAppKitClipToHalfPlane(
|
|
850
|
+
context, corners[(edge + 1) % 4], directions[(edge + 1) % 4], insides[edge], extent);
|
|
851
|
+
|
|
852
|
+
// Outer minus inner, as an even-odd fill of the two subpaths.
|
|
853
|
+
CGContextBeginPath(context);
|
|
854
|
+
CGContextAddPath(context, outer);
|
|
855
|
+
if (inner != nullptr) {
|
|
856
|
+
CGContextAddPath(context, inner);
|
|
857
|
+
}
|
|
858
|
+
const CGFloat components[4] = {
|
|
859
|
+
_borderColors[edge * 4 + 0], _borderColors[edge * 4 + 1],
|
|
860
|
+
_borderColors[edge * 4 + 2], _borderColors[edge * 4 + 3]};
|
|
861
|
+
CGColorRef color = CGColorCreate(space, components);
|
|
862
|
+
CGContextSetFillColorWithColor(context, color);
|
|
863
|
+
CGContextEOFillPath(context);
|
|
864
|
+
CGColorRelease(color);
|
|
865
|
+
|
|
866
|
+
CGContextRestoreGState(context);
|
|
867
|
+
}
|
|
868
|
+
CGColorSpaceRelease(space);
|
|
869
|
+
CGPathRelease(outer);
|
|
870
|
+
if (inner != nullptr) {
|
|
871
|
+
CGPathRelease(inner);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
- (void)setRnOpacity:(CGFloat)opacity {
|
|
876
|
+
_opacity = opacity;
|
|
877
|
+
self.layer.opacity = (float)opacity;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
- (void)setRnHidesBackFace:(BOOL)hides {
|
|
881
|
+
if (_hidesBackFace == hides) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
_hidesBackFace = hides;
|
|
885
|
+
[self rnUpdateBackFaceVisibility];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Hidden through `self.hidden` rather than by skipping the draw: it takes the
|
|
889
|
+
// children with it, which is what a back face should do, and it takes hit
|
|
890
|
+
// testing with it too -- the back of a card is not clickable.
|
|
891
|
+
//
|
|
892
|
+
// The two reasons are kept apart and combined here. `self.hidden` is one flag
|
|
893
|
+
// and `display: none` writes it too, so whichever of the two spoke last used
|
|
894
|
+
// to answer for both -- and layout metrics are applied after props, so a card
|
|
895
|
+
// turned away from the viewer was reliably un-hidden a moment later.
|
|
896
|
+
- (void)rnApplyVisibility {
|
|
897
|
+
self.hidden = (_hiddenByApp || _hiddenByBackFace) ? YES : NO;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
- (void)setRnHidden:(BOOL)hidden {
|
|
901
|
+
_hiddenByApp = hidden;
|
|
902
|
+
[self rnApplyVisibility];
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
- (void)rnUpdateBackFaceVisibility {
|
|
906
|
+
BOOL away = NO;
|
|
907
|
+
if (_hidesBackFace) {
|
|
908
|
+
float matrix[16];
|
|
909
|
+
const CGFloat *fields = (const CGFloat *)&_transform;
|
|
910
|
+
for (int index = 0; index < 16; index++) {
|
|
911
|
+
matrix[index] = (float)fields[index];
|
|
912
|
+
}
|
|
913
|
+
away = basalt::facesAway(matrix) ? YES : NO;
|
|
914
|
+
}
|
|
915
|
+
// Recomputed rather than returned early on, so that turning the prop off
|
|
916
|
+
// shows a view it had hidden instead of leaving it hidden forever.
|
|
917
|
+
_hiddenByBackFace = away;
|
|
918
|
+
[self rnApplyVisibility];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
- (void)setRnTransform:(nullable const float *)matrix {
|
|
922
|
+
if (matrix == nullptr) {
|
|
923
|
+
_hasTransform = NO;
|
|
924
|
+
_transform = CATransform3DIdentity;
|
|
925
|
+
self.layer.transform = CATransform3DIdentity;
|
|
926
|
+
[self rnUpdateBackFaceVisibility];
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Field by field rather than a memcpy: CATransform3D's members are CGFloat,
|
|
931
|
+
// which is not guaranteed to be double, and the layout is only documented as
|
|
932
|
+
// "m11 through m44 in this order".
|
|
933
|
+
CATransform3D transform;
|
|
934
|
+
CGFloat *fields = (CGFloat *)&transform;
|
|
935
|
+
for (int index = 0; index < 16; index++) {
|
|
936
|
+
fields[index] = (CGFloat)matrix[index];
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
_transform = transform;
|
|
940
|
+
_hasTransform = !CATransform3DIsIdentity(transform);
|
|
941
|
+
[self rnUpdateBackFaceVisibility];
|
|
942
|
+
self.layer.transform = transform;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
- (void)setRnClipsChildren:(BOOL)clips {
|
|
946
|
+
_clipsChildren = clips;
|
|
947
|
+
self.layer.masksToBounds = clips;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
- (void)setRnZIndex:(NSInteger)zIndex {
|
|
951
|
+
_zIndex = zIndex;
|
|
952
|
+
// Core Animation composites sibling layers by zPosition, so painting reorders
|
|
953
|
+
// without the subviews array moving -- and the subviews array is the child
|
|
954
|
+
// list Fabric indexes into. Reordering it to paint would corrupt the next
|
|
955
|
+
// Insert.
|
|
956
|
+
self.layer.zPosition = (CGFloat)zIndex;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
- (NSInteger)rnZIndex {
|
|
960
|
+
return _zIndex;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
- (CGAffineTransform)rnLocalToParent {
|
|
964
|
+
const NSRect frame = self.frame;
|
|
965
|
+
const CGAffineTransform translation =
|
|
966
|
+
CGAffineTransformMakeTranslation(frame.origin.x, frame.origin.y);
|
|
967
|
+
|
|
968
|
+
// `CATransform3DIsAffine` is false for a perspective transform, which has no
|
|
969
|
+
// 2D inverse to hit test with. React Native can express one; nothing here
|
|
970
|
+
// draws it yet -- see docs/backlog/correctness.md -- and answering with the
|
|
971
|
+
// translation keeps such a view clickable at its untransformed place rather
|
|
972
|
+
// than nowhere at all.
|
|
973
|
+
if (!_hasTransform || !CATransform3DIsAffine(_transform)) {
|
|
974
|
+
return translation;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Anchored at the centre, because that is where CALayer's default anchor
|
|
978
|
+
// point is and so where the drawn transform turns. Written as move the
|
|
979
|
+
// centre to the origin, transform, move it back, then place the frame --
|
|
980
|
+
// the same four steps, in the same order, as Win32's `localToParent`.
|
|
981
|
+
const CGFloat centreX = frame.size.width / 2.0;
|
|
982
|
+
const CGFloat centreY = frame.size.height / 2.0;
|
|
983
|
+
|
|
984
|
+
CGAffineTransform local = CGAffineTransformMakeTranslation(-centreX, -centreY);
|
|
985
|
+
local = CGAffineTransformConcat(local, CATransform3DGetAffineTransform(_transform));
|
|
986
|
+
local = CGAffineTransformConcat(local, CGAffineTransformMakeTranslation(centreX, centreY));
|
|
987
|
+
return CGAffineTransformConcat(local, translation);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
- (BOOL)rnPageToLocal:(NSPoint)page fromRoot:(RnAppKitView *)root into:(NSPoint *)out {
|
|
991
|
+
CGAffineTransform toPage = CGAffineTransformIdentity;
|
|
992
|
+
|
|
993
|
+
for (RnAppKitView *node = self; node != nil && node != root;) {
|
|
994
|
+
toPage = CGAffineTransformConcat(toPage, [node rnLocalToParent]);
|
|
995
|
+
|
|
996
|
+
RnAppKitView *parent =
|
|
997
|
+
[node.superview isKindOfClass:[RnAppKitView class]] ? (RnAppKitView *)node.superview : nil;
|
|
998
|
+
if (parent == nil) {
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
// A scrolled ancestor moves its children by its bounds origin, which is
|
|
1002
|
+
// not part of any child's own placement -- the same offset the hit test
|
|
1003
|
+
// adds back on the way down.
|
|
1004
|
+
const NSPoint scroll = parent.bounds.origin;
|
|
1005
|
+
toPage =
|
|
1006
|
+
CGAffineTransformConcat(toPage, CGAffineTransformMakeTranslation(-scroll.x, -scroll.y));
|
|
1007
|
+
node = parent;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const CGFloat determinant = toPage.a * toPage.d - toPage.b * toPage.c;
|
|
1011
|
+
if (determinant == 0.0) {
|
|
1012
|
+
return NO;
|
|
1013
|
+
}
|
|
1014
|
+
if (out != NULL) {
|
|
1015
|
+
*out = CGPointApplyAffineTransform(page, CGAffineTransformInvert(toPage));
|
|
1016
|
+
}
|
|
1017
|
+
return YES;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
- (NSArray<RnAppKitView *> *)rnChildrenInPaintOrder {
|
|
1021
|
+
NSMutableArray<RnAppKitView *> *ordered = [NSMutableArray array];
|
|
1022
|
+
for (NSView *child in self.subviews) {
|
|
1023
|
+
if ([child isKindOfClass:[RnAppKitView class]]) {
|
|
1024
|
+
[ordered addObject:(RnAppKitView *)child];
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// Only when something asked for it. The overwhelmingly common view has no
|
|
1028
|
+
// zIndex anywhere among its children, and sorting every tree on every hit
|
|
1029
|
+
// test to discover that would be paid on every press.
|
|
1030
|
+
BOOL any = NO;
|
|
1031
|
+
for (RnAppKitView *child in ordered) {
|
|
1032
|
+
if (child.rnZIndex != 0) {
|
|
1033
|
+
any = YES;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (!any) {
|
|
1038
|
+
return ordered;
|
|
1039
|
+
}
|
|
1040
|
+
// Stable, so equal zIndex keeps document order.
|
|
1041
|
+
return [ordered sortedArrayWithOptions:NSSortStable
|
|
1042
|
+
usingComparator:^NSComparisonResult(RnAppKitView *a, RnAppKitView *b) {
|
|
1043
|
+
if (a.rnZIndex < b.rnZIndex) return NSOrderedAscending;
|
|
1044
|
+
if (a.rnZIndex > b.rnZIndex) return NSOrderedDescending;
|
|
1045
|
+
return NSOrderedSame;
|
|
1046
|
+
}];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
- (void)setRnCornerRadius:(CGFloat)radius {
|
|
1050
|
+
const CGFloat radii[8] = {radius, radius, radius, radius,
|
|
1051
|
+
radius, radius, radius, radius};
|
|
1052
|
+
[self setRnBorderRadii:radii];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
- (void)setRnBorderRadii:(nullable const CGFloat *)radii {
|
|
1056
|
+
BOOL any = NO;
|
|
1057
|
+
for (int i = 0; i < 8; i++) {
|
|
1058
|
+
_borderRadii[i] = radii != nullptr && radii[i] > 0 ? radii[i] : 0;
|
|
1059
|
+
if (_borderRadii[i] > 0) {
|
|
1060
|
+
any = YES;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
_hasBorderRadii = any;
|
|
1064
|
+
|
|
1065
|
+
// A single circular radius is what CALayer can express directly, and is the
|
|
1066
|
+
// overwhelmingly common case -- one `borderRadius` in a stylesheet. Keeping
|
|
1067
|
+
// it on cornerRadius rather than on a mask means the usual view stays a
|
|
1068
|
+
// plain layer, and means `overflow: 'visible'` still lets children escape.
|
|
1069
|
+
BOOL uniform = YES;
|
|
1070
|
+
for (int i = 1; i < 8; i++) {
|
|
1071
|
+
if (_borderRadii[i] != _borderRadii[0]) {
|
|
1072
|
+
uniform = NO;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (uniform) {
|
|
1078
|
+
_cornerRadius = _borderRadii[0];
|
|
1079
|
+
self.layer.cornerRadius = _borderRadii[0];
|
|
1080
|
+
self.layer.mask = nil;
|
|
1081
|
+
} else {
|
|
1082
|
+
// Anything else needs a mask layer, which is what UIKit's own RCTView does
|
|
1083
|
+
// for the same reason. The cost is that a mask clips children whatever
|
|
1084
|
+
// `overflow` says -- CALayer offers no way to round a background without
|
|
1085
|
+
// clipping what sits on it.
|
|
1086
|
+
_cornerRadius = 0;
|
|
1087
|
+
self.layer.cornerRadius = 0;
|
|
1088
|
+
[self rnUpdateRadiusMask];
|
|
1089
|
+
}
|
|
1090
|
+
// The border is drawn along these radii.
|
|
1091
|
+
self.needsDisplay = YES;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
- (void)rnUpdateRadiusMask {
|
|
1095
|
+
if (!_hasBorderRadii) {
|
|
1096
|
+
self.layer.mask = nil;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
CAShapeLayer *mask = [CAShapeLayer layer];
|
|
1100
|
+
mask.frame = self.bounds;
|
|
1101
|
+
CGPathRef path = RnAppKitCreateRoundedPath(self.bounds, _borderRadii);
|
|
1102
|
+
mask.path = path;
|
|
1103
|
+
CGPathRelease(path);
|
|
1104
|
+
self.layer.mask = mask;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
- (void)setRnBorderWidths:(nullable const CGFloat *)widths
|
|
1108
|
+
colors:(nullable const CGFloat *)colors {
|
|
1109
|
+
BOOL any = NO;
|
|
1110
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
1111
|
+
_borderWidths[edge] = widths != nullptr && widths[edge] > 0 ? widths[edge] : 0;
|
|
1112
|
+
for (int c = 0; c < 4; c++) {
|
|
1113
|
+
_borderColors[edge * 4 + c] = colors != nullptr ? colors[edge * 4 + c] : 0;
|
|
1114
|
+
}
|
|
1115
|
+
// A width with a transparent colour paints nothing, and the GTK side makes
|
|
1116
|
+
// the same judgement, so the two agree on whether a view has a border at
|
|
1117
|
+
// all rather than only on what it looks like.
|
|
1118
|
+
if (_borderWidths[edge] > 0 && _borderColors[edge * 4 + 3] > 0) {
|
|
1119
|
+
any = YES;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
_hasBorders = any;
|
|
1123
|
+
self.needsDisplay = YES;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
- (void)insertRnChild:(RnAppKitView *)child atIndex:(NSInteger)index {
|
|
1127
|
+
NSArray<NSView *> *children = self.subviews;
|
|
1128
|
+
if (index >= (NSInteger)children.count) {
|
|
1129
|
+
[self addSubview:child];
|
|
1130
|
+
[self rnRaiseIndicators];
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
[self addSubview:child positioned:NSWindowBelow relativeTo:children[(NSUInteger)index]];
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// A child added at or past the end lands above the overlay, so put it back on
|
|
1137
|
+
// top. Cheap, and the alternative -- teaching every insert about a view that is
|
|
1138
|
+
// not part of the tree -- puts the overlay into code that has nothing to do
|
|
1139
|
+
// with it.
|
|
1140
|
+
- (void)rnRaiseIndicators {
|
|
1141
|
+
if (_indicators != nil) {
|
|
1142
|
+
[self addSubview:_indicators positioned:NSWindowAbove relativeTo:nil];
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
- (void)removeRnChild:(RnAppKitView *)child {
|
|
1147
|
+
// Removed, not destroyed: a Delete mutation for the same tag follows
|
|
1148
|
+
// separately, and until then the view may be inserted somewhere else.
|
|
1149
|
+
if (child.superview == self) {
|
|
1150
|
+
[child removeFromSuperview];
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
- (void)describeInto:(NSMutableString *)out depth:(NSInteger)depth {
|
|
1155
|
+
for (NSInteger i = 0; i < depth; i++) {
|
|
1156
|
+
[out appendString:@" "];
|
|
1157
|
+
}
|
|
1158
|
+
const NSRect frame = self.frame;
|
|
1159
|
+
[out appendFormat:@"view tag=%ld frame=(%g,%g %gx%g)",
|
|
1160
|
+
(long)self.rnTag,
|
|
1161
|
+
frame.origin.x,
|
|
1162
|
+
frame.origin.y,
|
|
1163
|
+
frame.size.width,
|
|
1164
|
+
frame.size.height];
|
|
1165
|
+
|
|
1166
|
+
if (_hasBackgroundColor) {
|
|
1167
|
+
[out appendFormat:@" bg=#%02x%02x%02x%02x",
|
|
1168
|
+
(unsigned)(_backgroundComponents[0] * 255.0 + 0.5),
|
|
1169
|
+
(unsigned)(_backgroundComponents[1] * 255.0 + 0.5),
|
|
1170
|
+
(unsigned)(_backgroundComponents[2] * 255.0 + 0.5),
|
|
1171
|
+
(unsigned)(_backgroundComponents[3] * 255.0 + 0.5)];
|
|
1172
|
+
}
|
|
1173
|
+
// Field order matches the GTK side exactly -- bg, opacity, clip, then text --
|
|
1174
|
+
// because scripts/compare_hosts.sh diffs the two dumps line by line and a
|
|
1175
|
+
// reordering would read as every line differing.
|
|
1176
|
+
if (_opacity < 1.0) {
|
|
1177
|
+
[out appendFormat:@" opacity=%g", _opacity];
|
|
1178
|
+
}
|
|
1179
|
+
if (_clipsChildren) {
|
|
1180
|
+
[out appendString:@" clip"];
|
|
1181
|
+
}
|
|
1182
|
+
// Whether anything is drawn at all. Without this a view hidden by
|
|
1183
|
+
// `display: none` or by a back face turned away reads exactly like a visible
|
|
1184
|
+
// one, and the only prop in this dump that removes a view entirely was the
|
|
1185
|
+
// only one it could not show.
|
|
1186
|
+
if (self.hidden) {
|
|
1187
|
+
[out appendString:@" hidden"];
|
|
1188
|
+
}
|
|
1189
|
+
// Per-corner radii and per-edge borders, in the same fields and the same
|
|
1190
|
+
// order the GTK and Win32 sides print. They are here for the same reason
|
|
1191
|
+
// `transform=` is, and the comment there names the precedent: a frame cannot
|
|
1192
|
+
// show a border, so a bordered view and a bare one read as identical in this
|
|
1193
|
+
// dump -- which is how this host went until phase 47 applying neither.
|
|
1194
|
+
if (_hasBorderRadii) {
|
|
1195
|
+
[out appendFormat:@" radii=(%g,%g,%g,%g,%g,%g,%g,%g)",
|
|
1196
|
+
(double)_borderRadii[0], (double)_borderRadii[1],
|
|
1197
|
+
(double)_borderRadii[2], (double)_borderRadii[3],
|
|
1198
|
+
(double)_borderRadii[4], (double)_borderRadii[5],
|
|
1199
|
+
(double)_borderRadii[6], (double)_borderRadii[7]];
|
|
1200
|
+
}
|
|
1201
|
+
if (_hasBorders) {
|
|
1202
|
+
[out appendFormat:@" borderw=(%g,%g,%g,%g)",
|
|
1203
|
+
(double)_borderWidths[0], (double)_borderWidths[1],
|
|
1204
|
+
(double)_borderWidths[2], (double)_borderWidths[3]];
|
|
1205
|
+
[out appendString:@" borderc=("];
|
|
1206
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
1207
|
+
[out appendFormat:@"%s#%02x%02x%02x%02x",
|
|
1208
|
+
edge == 0 ? "" : ",",
|
|
1209
|
+
(unsigned)(_borderColors[edge * 4 + 0] * 255.0 + 0.5),
|
|
1210
|
+
(unsigned)(_borderColors[edge * 4 + 1] * 255.0 + 0.5),
|
|
1211
|
+
(unsigned)(_borderColors[edge * 4 + 2] * 255.0 + 0.5),
|
|
1212
|
+
(unsigned)(_borderColors[edge * 4 + 3] * 255.0 + 0.5)];
|
|
1213
|
+
}
|
|
1214
|
+
[out appendString:@")"];
|
|
1215
|
+
}
|
|
1216
|
+
if (_hasTransform) {
|
|
1217
|
+
// The 2D affine part, which is all either platform draws, in the order
|
|
1218
|
+
// CSS writes a matrix(): a, b, c, d, tx, ty. The GTK side prints the same
|
|
1219
|
+
// six from its own matrix, so a transform is comparable across the two --
|
|
1220
|
+
// without which a view that is rotated on one desktop and not on the other
|
|
1221
|
+
// looks identical in this dump. Which is exactly how the macOS host went
|
|
1222
|
+
// this long without applying transforms at all.
|
|
1223
|
+
[out appendFormat:@" transform=(%g,%g,%g,%g,%g,%g)",
|
|
1224
|
+
(double)_transform.m11,
|
|
1225
|
+
(double)_transform.m12,
|
|
1226
|
+
(double)_transform.m21,
|
|
1227
|
+
(double)_transform.m22,
|
|
1228
|
+
(double)_transform.m41,
|
|
1229
|
+
(double)_transform.m42];
|
|
1230
|
+
}
|
|
1231
|
+
const NSPoint scroll = self.bounds.origin;
|
|
1232
|
+
if (scroll.x != 0 || scroll.y != 0) {
|
|
1233
|
+
[out appendFormat:@" scroll=(%g,%g)", scroll.x, scroll.y];
|
|
1234
|
+
}
|
|
1235
|
+
// The overlay scrollbars, which are otherwise pure paint and so invisible to
|
|
1236
|
+
// every test this project has. Printed only when there is one, so a view that
|
|
1237
|
+
// does not scroll stays as short as it was.
|
|
1238
|
+
if (_indicators != nil && _indicators.rnVerticalLength > 0) {
|
|
1239
|
+
[out appendFormat:@" scrollbar-v=(%g,%g)",
|
|
1240
|
+
_indicators.rnVerticalOffset,
|
|
1241
|
+
_indicators.rnVerticalLength];
|
|
1242
|
+
}
|
|
1243
|
+
if (_indicators != nil && _indicators.rnHorizontalLength > 0) {
|
|
1244
|
+
[out appendFormat:@" scrollbar-h=(%g,%g)",
|
|
1245
|
+
_indicators.rnHorizontalOffset,
|
|
1246
|
+
_indicators.rnHorizontalLength];
|
|
1247
|
+
}
|
|
1248
|
+
// Printed only when it is not the default, like every other field here.
|
|
1249
|
+
// Worth printing at all because it is invisible: a view with
|
|
1250
|
+
// `pointerEvents: none` is drawn exactly like one without, and the only way
|
|
1251
|
+
// the cross-host diff can say the prop arrived on all three is if each one
|
|
1252
|
+
// reports it.
|
|
1253
|
+
if (self.rnPointerEvents != RnAppKitPointerEventsAuto) {
|
|
1254
|
+
[out appendFormat:@" pe=%s", RnAppKitPointerEventsName(self.rnPointerEvents)];
|
|
1255
|
+
}
|
|
1256
|
+
if (_image != nullptr) {
|
|
1257
|
+
// The same `texture=WxH fit=<name>` the GTK side emits, so an <Image> shows
|
|
1258
|
+
// up in the cross-platform diff and the end-to-end suite can assert on it.
|
|
1259
|
+
// The fit is here because it is the only thing about a drawn image that a
|
|
1260
|
+
// frame cannot show: two views the same size holding the same picture are
|
|
1261
|
+
// identical in every other line of this dump and different on screen.
|
|
1262
|
+
[out appendFormat:@" texture=%zux%zu", CGImageGetWidth(_image), CGImageGetHeight(_image)];
|
|
1263
|
+
[out appendFormat:@" fit=%s", RnAppKitImageFitName(_imageFit)];
|
|
1264
|
+
// Printed for the same reason the fit is: a tinted image and an untinted
|
|
1265
|
+
// one are identical in every other line of this dump and different on
|
|
1266
|
+
// screen. Formatted exactly as GTK formats a colour, so the cross-host diff
|
|
1267
|
+
// can compare them.
|
|
1268
|
+
if (_imageTint != nil) {
|
|
1269
|
+
NSColor *rgb = [_imageTint colorUsingColorSpace:NSColorSpace.sRGBColorSpace] ?: _imageTint;
|
|
1270
|
+
[out appendFormat:@" tint=#%02x%02x%02x%02x",
|
|
1271
|
+
(unsigned)(rgb.redComponent * 255.0 + 0.5),
|
|
1272
|
+
(unsigned)(rgb.greenComponent * 255.0 + 0.5),
|
|
1273
|
+
(unsigned)(rgb.blueComponent * 255.0 + 0.5),
|
|
1274
|
+
(unsigned)(rgb.alphaComponent * 255.0 + 0.5)];
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (_textLayout != nil) {
|
|
1278
|
+
NSString *text = _textLayout.attributedString.string;
|
|
1279
|
+
if (text.length > 0) {
|
|
1280
|
+
// Escaped like g_strescape's output on the other side, so a string with a
|
|
1281
|
+
// quote or a newline in it stays one line and stays comparable.
|
|
1282
|
+
NSMutableString *escaped = [text mutableCopy];
|
|
1283
|
+
[escaped replaceOccurrencesOfString:@"\\" withString:@"\\\\"
|
|
1284
|
+
options:0 range:NSMakeRange(0, escaped.length)];
|
|
1285
|
+
[escaped replaceOccurrencesOfString:@"\"" withString:@"\\\""
|
|
1286
|
+
options:0 range:NSMakeRange(0, escaped.length)];
|
|
1287
|
+
[escaped replaceOccurrencesOfString:@"\n" withString:@"\\n"
|
|
1288
|
+
options:0 range:NSMakeRange(0, escaped.length)];
|
|
1289
|
+
[out appendFormat:@" text=\"%@\"", escaped];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// A text field's content lives in its NSTextField peer, not in anything this
|
|
1294
|
+
// view draws, so it would otherwise be invisible to every test that reads
|
|
1295
|
+
// this tree. Same spelling as the GTK side's.
|
|
1296
|
+
if (self.rnEditable != nil) {
|
|
1297
|
+
NSString *value = RnPeerText(self.rnEditable);
|
|
1298
|
+
NSMutableString *escaped = [value mutableCopy];
|
|
1299
|
+
[escaped replaceOccurrencesOfString:@"\\" withString:@"\\\\"
|
|
1300
|
+
options:0 range:NSMakeRange(0, escaped.length)];
|
|
1301
|
+
[escaped replaceOccurrencesOfString:@"\"" withString:@"\\\""
|
|
1302
|
+
options:0 range:NSMakeRange(0, escaped.length)];
|
|
1303
|
+
[out appendFormat:@" editable=\"%@\"", escaped];
|
|
1304
|
+
|
|
1305
|
+
// First responder is the field's *field editor* while it is being edited,
|
|
1306
|
+
// not the field itself, so asking the window whether it is editing this one
|
|
1307
|
+
// is the question that actually has the right answer.
|
|
1308
|
+
NSWindow *window = self.window;
|
|
1309
|
+
// A multiline peer *is* the editor, so it is the first responder itself;
|
|
1310
|
+
// a single-line one is being edited through a field editor whose delegate
|
|
1311
|
+
// is the field. RnPeerEditor answers for both.
|
|
1312
|
+
const BOOL focused = window != nil && window.firstResponder != nil &&
|
|
1313
|
+
window.firstResponder == RnPeerEditor(self.rnEditable);
|
|
1314
|
+
if (focused) {
|
|
1315
|
+
[out appendString:@" focused"];
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// How many DevTools highlights this view is drawing. In the dump because they
|
|
1320
|
+
// are otherwise invisible to everything but a screenshot, and because a
|
|
1321
|
+
// command that arrived and drew nothing is exactly the failure worth
|
|
1322
|
+
// catching.
|
|
1323
|
+
if (!_highlightFilled.empty()) {
|
|
1324
|
+
[out appendFormat:@" highlights=%lu", (unsigned long)_highlightFilled.size()];
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// What kind of control this view is, and what state it is in. Written by
|
|
1328
|
+
// core/DesktopControls.h rather than formatted here, for the same reason the
|
|
1329
|
+
// role name below is React Native's vocabulary and not AppKit's.
|
|
1330
|
+
if (_rnControlDescription != nil) {
|
|
1331
|
+
[out appendFormat:@" control=%@", _rnControlDescription];
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// React Native's role name, not AppKit's. The GTK side reports the same
|
|
1335
|
+
// string for the same reason: this dump is compared line by line across two
|
|
1336
|
+
// platforms, and each reporting its own toolkit's vocabulary would make every
|
|
1337
|
+
// accessible view look like a difference. That the *platform* role was really
|
|
1338
|
+
// applied is asserted in each platform's unit tests instead.
|
|
1339
|
+
if (_roleName != nil) {
|
|
1340
|
+
[out appendFormat:@" role=%@", _roleName];
|
|
1341
|
+
}
|
|
1342
|
+
// Whether Tab stops here, after `role=` because that is where the GTK side
|
|
1343
|
+
// prints it and this dump is diffed line by line. Whether it is focused *now*
|
|
1344
|
+
// is deliberately not printed: that depends on what the window server did
|
|
1345
|
+
// when the window opened, which is not a property of the platform and would
|
|
1346
|
+
// make this dump differ between two machines running the same app.
|
|
1347
|
+
if (self.rnFocusable) {
|
|
1348
|
+
[out appendString:@" focusable"];
|
|
1349
|
+
}
|
|
1350
|
+
[out appendString:@"\n"];
|
|
1351
|
+
|
|
1352
|
+
for (NSView *child in self.subviews) {
|
|
1353
|
+
if ([child isKindOfClass:[RnAppKitView class]]) {
|
|
1354
|
+
[(RnAppKitView *)child describeInto:out depth:depth + 1];
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// --- Input ----------------------------------------------------------------
|
|
1360
|
+
//
|
|
1361
|
+
// AppKit delivers a mouse event to the view its own hit testing picked, which
|
|
1362
|
+
// is some view in this tree; React Native wants it against the surface root,
|
|
1363
|
+
// hit-tested React Native's way. So every view forwards, and the root is where
|
|
1364
|
+
// the forwarding stops.
|
|
1365
|
+
//
|
|
1366
|
+
// Not overriding hitTest: to make the root swallow everything instead: that
|
|
1367
|
+
// would also swallow the cursor rectangles, tooltips and tracking areas any
|
|
1368
|
+
// later component needs, and AppKit's own hit testing is doing no harm here.
|
|
1369
|
+
|
|
1370
|
+
// Keyboard focus.
|
|
1371
|
+
//
|
|
1372
|
+
// AppKit's key-view loop is the chain: a view that accepts first responder
|
|
1373
|
+
// status is reached by Tab, and `nextValidKeyView` walks the view hierarchy, so
|
|
1374
|
+
// the order is tree order without this project deciding what "next" means. The
|
|
1375
|
+
// GTK host gets the same for the same reason; the Win32 host has no such loop
|
|
1376
|
+
// and builds one. See AppKitFocus.h.
|
|
1377
|
+
|
|
1378
|
+
- (BOOL)acceptsFirstResponder {
|
|
1379
|
+
return _rnFocusable;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
- (BOOL)canBecomeKeyView {
|
|
1383
|
+
// Not inherited: NSView's default also consults the Full Keyboard Access
|
|
1384
|
+
// setting for some views, and a React Native app's buttons are not AppKit
|
|
1385
|
+
// controls that a user has opted into tabbing to -- they are the whole
|
|
1386
|
+
// interface.
|
|
1387
|
+
return _rnFocusable && !self.isHiddenOrHasHiddenAncestor;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
- (nullable id<RnAppKitFocusHandler>)rnFocusHandlerForTree {
|
|
1391
|
+
NSView *view = self;
|
|
1392
|
+
while (view != nil) {
|
|
1393
|
+
if ([view isKindOfClass:[RnAppKitView class]]) {
|
|
1394
|
+
id<RnAppKitFocusHandler> handler = ((RnAppKitView *)view).rnFocusHandler;
|
|
1395
|
+
if (handler != nil) {
|
|
1396
|
+
return handler;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
view = view.superview;
|
|
1400
|
+
}
|
|
1401
|
+
return nil;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
- (BOOL)becomeFirstResponder {
|
|
1405
|
+
if (![super becomeFirstResponder]) {
|
|
1406
|
+
return NO;
|
|
1407
|
+
}
|
|
1408
|
+
// The ring is drawn by this view, so it has to redraw when focus arrives.
|
|
1409
|
+
self.needsDisplay = YES;
|
|
1410
|
+
[[self rnFocusHandlerForTree] rnView:self didChangeFocus:YES];
|
|
1411
|
+
return YES;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
- (BOOL)resignFirstResponder {
|
|
1415
|
+
if (![super resignFirstResponder]) {
|
|
1416
|
+
return NO;
|
|
1417
|
+
}
|
|
1418
|
+
self.needsDisplay = YES;
|
|
1419
|
+
[[self rnFocusHandlerForTree] rnView:self didChangeFocus:NO];
|
|
1420
|
+
return YES;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
- (void)keyDown:(NSEvent *)event {
|
|
1424
|
+
// An app's own declared shortcuts first, before activation and before Tab.
|
|
1425
|
+
//
|
|
1426
|
+
// Deliberately first: an app that binds Enter or Tab means it, and a platform
|
|
1427
|
+
// default that won over an explicit declaration would be impossible to
|
|
1428
|
+
// override. The cost is that binding Tab breaks focus movement inside that
|
|
1429
|
+
// view, which is the app's business and is what a browser does too.
|
|
1430
|
+
if ([[self rnFocusHandlerForTree] rnView:self handlesKey:event]) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
// Return, enter and space, which are the two keys that activate a control on
|
|
1434
|
+
// every desktop and in the browser. Anything else goes on, so a view with
|
|
1435
|
+
// focus does not swallow the window's own shortcuts.
|
|
1436
|
+
const unichar first = event.charactersIgnoringModifiers.length > 0
|
|
1437
|
+
? [event.charactersIgnoringModifiers characterAtIndex:0]
|
|
1438
|
+
: 0;
|
|
1439
|
+
const BOOL activates = first == NSCarriageReturnCharacter ||
|
|
1440
|
+
first == NSEnterCharacter || first == ' ';
|
|
1441
|
+
if (activates && [[self rnFocusHandlerForTree] rnActivateView:self]) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
// Tab, which on this platform is this project's own: AppKit's key-view loop
|
|
1445
|
+
// does nothing for a window built without a nib. Shift-Tab arrives as the
|
|
1446
|
+
// same character with the shift modifier set.
|
|
1447
|
+
if (first == NSTabCharacter || first == NSBackTabCharacter) {
|
|
1448
|
+
const BOOL forward = first == NSTabCharacter &&
|
|
1449
|
+
(event.modifierFlags & NSEventModifierFlagShift) == 0;
|
|
1450
|
+
if ([[self rnFocusHandlerForTree] rnMoveFocusForward:forward]) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
[super keyDown:event];
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Whether to paint the ring: focused, and focused by this window rather than by
|
|
1458
|
+
// a window that is not in front.
|
|
1459
|
+
- (BOOL)rnShowsFocusRing {
|
|
1460
|
+
NSWindow *window = self.window;
|
|
1461
|
+
return _rnFocusable && window != nil && window.firstResponder == self;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
- (nullable id<RnAppKitInputHandler>)rnHandler:(NSView **)outRoot {
|
|
1465
|
+
NSView *view = self;
|
|
1466
|
+
while (view != nil) {
|
|
1467
|
+
if ([view isKindOfClass:[RnAppKitView class]]) {
|
|
1468
|
+
id<RnAppKitInputHandler> handler = ((RnAppKitView *)view).rnInputHandler;
|
|
1469
|
+
if (handler != nil) {
|
|
1470
|
+
*outRoot = view;
|
|
1471
|
+
return handler;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
view = view.superview;
|
|
1475
|
+
}
|
|
1476
|
+
return nil;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
- (void)forwardMouse:(NSEvent *)event kind:(char)kind {
|
|
1480
|
+
NSView *root = nil;
|
|
1481
|
+
id<RnAppKitInputHandler> handler = [self rnHandler:&root];
|
|
1482
|
+
if (handler == nil) {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const NSPoint point = [root convertPoint:event.locationInWindow fromView:nil];
|
|
1486
|
+
// AppKit's `buttonNumber` is 0 for left, 1 for right, 2 for the wheel. W3C
|
|
1487
|
+
// has the middle one and the right one the other way round, which is exactly
|
|
1488
|
+
// the kind of thing worth converting once rather than comparing twice.
|
|
1489
|
+
int button = 0;
|
|
1490
|
+
switch (event.buttonNumber) {
|
|
1491
|
+
case 0: button = (int)basalt::PointerButton::Primary; break;
|
|
1492
|
+
case 1: button = (int)basalt::PointerButton::Secondary; break;
|
|
1493
|
+
default: button = (int)basalt::PointerButton::Middle; break;
|
|
1494
|
+
}
|
|
1495
|
+
switch (kind) {
|
|
1496
|
+
case 'd': [handler rnMouseDownAt:point button:button]; break;
|
|
1497
|
+
case 'm': [handler rnMouseDraggedTo:point]; break;
|
|
1498
|
+
case 'u': [handler rnMouseUpAt:point button:button]; break;
|
|
1499
|
+
case 'h': [handler rnMouseMovedTo:point]; break;
|
|
1500
|
+
default: break;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Hover.
|
|
1505
|
+
//
|
|
1506
|
+
// A view gets `mouseMoved:` only if something asked for it, and the tracking
|
|
1507
|
+
// area is that ask. Only the surface root has an input handler, and the area
|
|
1508
|
+
// covers its whole visible rect, so one area serves every view in the tree --
|
|
1509
|
+
// which is the same arrangement the GTK side gets by attaching its motion
|
|
1510
|
+
// controller to the root rather than to each widget.
|
|
1511
|
+
//
|
|
1512
|
+
// NSTrackingInVisibleRect leaves the area's geometry to AppKit, so a window
|
|
1513
|
+
// resize does not need this recomputed; `updateTrackingAreas` still has to
|
|
1514
|
+
// exist, because that is where AppKit expects an area to be re-added.
|
|
1515
|
+
- (void)updateTrackingAreas {
|
|
1516
|
+
[super updateTrackingAreas];
|
|
1517
|
+
if (self.rnInputHandler == nil) {
|
|
1518
|
+
if (_rnHoverTrackingArea != nil) {
|
|
1519
|
+
[self removeTrackingArea:_rnHoverTrackingArea];
|
|
1520
|
+
_rnHoverTrackingArea = nil;
|
|
1521
|
+
}
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (_rnHoverTrackingArea != nil) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
_rnHoverTrackingArea = [[NSTrackingArea alloc]
|
|
1528
|
+
initWithRect:NSZeroRect
|
|
1529
|
+
options:NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingInVisibleRect |
|
|
1530
|
+
NSTrackingActiveInKeyWindow
|
|
1531
|
+
owner:self
|
|
1532
|
+
userInfo:nil];
|
|
1533
|
+
[self addTrackingArea:_rnHoverTrackingArea];
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
- (void)mouseMoved:(NSEvent *)event {
|
|
1537
|
+
[self forwardMouse:event kind:'h'];
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
- (void)mouseExited:(NSEvent *)event {
|
|
1541
|
+
NSView *root = nil;
|
|
1542
|
+
id<RnAppKitInputHandler> handler = [self rnHandler:&root];
|
|
1543
|
+
if (handler != nil) {
|
|
1544
|
+
[handler rnMouseExited];
|
|
1545
|
+
}
|
|
1546
|
+
[super mouseExited:event];
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Wheel and touchpad. Unhandled scrolls go to super, which walks the responder
|
|
1550
|
+
// chain -- so a wheel over a plain view inside a list reaches the list.
|
|
1551
|
+
- (void)scrollWheel:(NSEvent *)event {
|
|
1552
|
+
id<RnAppKitScrollHandler> handler = self.rnScrollHandler;
|
|
1553
|
+
if (handler != nil) {
|
|
1554
|
+
const NSPoint delta = NSMakePoint(event.scrollingDeltaX, event.scrollingDeltaY);
|
|
1555
|
+
// `hasPreciseScrollingDeltas` is the only thing that can tell a touchpad's
|
|
1556
|
+
// pixels from a wheel's line counts -- both arrive as small numbers, so the
|
|
1557
|
+
// deltas alone cannot. Passed on rather than resolved here: how many pixels
|
|
1558
|
+
// a line is worth is a cross-platform decision, and it is made next to the
|
|
1559
|
+
// GTK side's copy of it.
|
|
1560
|
+
if ([handler rnScrollView:self
|
|
1561
|
+
by:delta
|
|
1562
|
+
precise:event.hasPreciseScrollingDeltas
|
|
1563
|
+
phase:event.phase
|
|
1564
|
+
momentum:event.momentumPhase]) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
[super scrollWheel:event];
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
- (void)mouseDown:(NSEvent *)event {
|
|
1572
|
+
[self forwardMouse:event kind:'d'];
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
- (void)mouseDragged:(NSEvent *)event {
|
|
1576
|
+
[self forwardMouse:event kind:'m'];
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
- (void)mouseUp:(NSEvent *)event {
|
|
1580
|
+
[self forwardMouse:event kind:'u'];
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// Every button, and which one it was now travels with it. React Native's touch
|
|
1584
|
+
// model still has no concept of one -- that belongs to pointer events -- so a
|
|
1585
|
+
// secondary click arrives as a pointer event and presses nothing, which is what
|
|
1586
|
+
// it means on every desktop. See core/PointerButtons.h.
|
|
1587
|
+
- (void)rightMouseDown:(NSEvent *)event {
|
|
1588
|
+
[self forwardMouse:event kind:'d'];
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
- (void)rightMouseDragged:(NSEvent *)event {
|
|
1592
|
+
[self forwardMouse:event kind:'m'];
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
- (void)rightMouseUp:(NSEvent *)event {
|
|
1596
|
+
[self forwardMouse:event kind:'u'];
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
- (NSString *)describeTree {
|
|
1600
|
+
NSMutableString *out = [NSMutableString string];
|
|
1601
|
+
[self describeInto:out depth:0];
|
|
1602
|
+
return out;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
@end
|