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,38 @@
|
|
|
1
|
+
// A laid-out paragraph, and nothing that knows what React Native is.
|
|
2
|
+
//
|
|
3
|
+
// Deliberately separate from CoreTextLayout.h, which builds one of these out of
|
|
4
|
+
// an AttributedString. The view layer draws paragraphs and must not gain a
|
|
5
|
+
// React Native dependency to do it -- basalt_appkit_view builds and is tested
|
|
6
|
+
// on a Mac with nothing else installed, and that is worth keeping.
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#ifdef __OBJC__
|
|
11
|
+
#import <Cocoa/Cocoa.h>
|
|
12
|
+
|
|
13
|
+
@interface RnTextLayout : NSObject
|
|
14
|
+
|
|
15
|
+
@property(nonatomic, readonly) NSAttributedString *attributedString;
|
|
16
|
+
// 0 means no limit, matching ParagraphAttributes::maximumNumberOfLines.
|
|
17
|
+
@property(nonatomic, readonly) NSInteger maximumNumberOfLines;
|
|
18
|
+
@property(nonatomic, readonly) CTLineTruncationType truncationType;
|
|
19
|
+
@property(nonatomic, readonly) BOOL truncates;
|
|
20
|
+
|
|
21
|
+
+ (instancetype)layoutWithAttributedString:(NSAttributedString *)attributedString
|
|
22
|
+
maximumNumberOfLines:(NSInteger)maximumNumberOfLines
|
|
23
|
+
truncationType:(CTLineTruncationType)truncationType
|
|
24
|
+
truncates:(BOOL)truncates;
|
|
25
|
+
|
|
26
|
+
// The size this paragraph needs at `maxWidth`. Pass a negative width for
|
|
27
|
+
// unconstrained. In React Native's logical points, like everything else here.
|
|
28
|
+
//
|
|
29
|
+
// Width is not baked in: a PangoLayout has to be built against a width and
|
|
30
|
+
// rebuilt when it changes, and a Core Text framesetter does not. One of the few
|
|
31
|
+
// places the two platforms differ in shape rather than in spelling.
|
|
32
|
+
- (CGSize)sizeForWidth:(CGFloat)maxWidth;
|
|
33
|
+
|
|
34
|
+
// Draws into `context`, top-left origin, in a box `size` wide and tall.
|
|
35
|
+
- (void)drawInContext:(CGContextRef)context size:(CGSize)size;
|
|
36
|
+
|
|
37
|
+
@end
|
|
38
|
+
#endif
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#import "RnTextLayout.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
|
|
5
|
+
@implementation RnTextLayout {
|
|
6
|
+
CTFramesetterRef _framesetter;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
+ (instancetype)layoutWithAttributedString:(NSAttributedString *)attributedString
|
|
10
|
+
maximumNumberOfLines:(NSInteger)maximumNumberOfLines
|
|
11
|
+
truncationType:(CTLineTruncationType)truncationType
|
|
12
|
+
truncates:(BOOL)truncates {
|
|
13
|
+
RnTextLayout *layout = [[RnTextLayout alloc] init];
|
|
14
|
+
layout->_attributedString = attributedString;
|
|
15
|
+
layout->_maximumNumberOfLines = maximumNumberOfLines;
|
|
16
|
+
layout->_truncationType = truncationType;
|
|
17
|
+
layout->_truncates = truncates;
|
|
18
|
+
layout->_framesetter =
|
|
19
|
+
CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString);
|
|
20
|
+
return layout;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (void)dealloc {
|
|
24
|
+
if (_framesetter != nullptr) {
|
|
25
|
+
CFRelease(_framesetter);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The lines this paragraph breaks into at `maxWidth`, already limited to
|
|
30
|
+
// maximumNumberOfLines and with the last one truncated if it had to be.
|
|
31
|
+
//
|
|
32
|
+
// Core Text has no "maximum number of lines": a framesetter breaks as many
|
|
33
|
+
// lines as the text needs and a frame holds as many as fit in its path. So the
|
|
34
|
+
// limit is applied here, by asking for an unbounded frame and keeping the first
|
|
35
|
+
// N -- which is also the only way to put the ellipsis in the right place, since
|
|
36
|
+
// the line that gets truncated is the last one *kept*, not the last one there
|
|
37
|
+
// is.
|
|
38
|
+
- (NSArray *)linesForWidth:(CGFloat)maxWidth outLines:(NSMutableArray *)collected {
|
|
39
|
+
const CGFloat width = maxWidth < 0 ? CGFLOAT_MAX : maxWidth;
|
|
40
|
+
|
|
41
|
+
CGMutablePathRef path = CGPathCreateMutable();
|
|
42
|
+
CGPathAddRect(path, nullptr, CGRectMake(0, 0, width, CGFLOAT_MAX));
|
|
43
|
+
CTFrameRef frame = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), path, nullptr);
|
|
44
|
+
CGPathRelease(path);
|
|
45
|
+
if (frame == nullptr) {
|
|
46
|
+
return collected;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
50
|
+
const NSInteger limit =
|
|
51
|
+
_maximumNumberOfLines > 0 ? MIN(_maximumNumberOfLines, (NSInteger)lines.count)
|
|
52
|
+
: (NSInteger)lines.count;
|
|
53
|
+
|
|
54
|
+
for (NSInteger i = 0; i < limit; i++) {
|
|
55
|
+
CTLineRef line = (__bridge CTLineRef)lines[(NSUInteger)i];
|
|
56
|
+
const BOOL isLastKept = (i == limit - 1);
|
|
57
|
+
const BOOL droppedSomething = limit < (NSInteger)lines.count;
|
|
58
|
+
|
|
59
|
+
if (_truncates && isLastKept && droppedSomething && width != CGFLOAT_MAX) {
|
|
60
|
+
// The truncated line has to be built from everything that would have
|
|
61
|
+
// followed, not from this line alone -- otherwise the ellipsis lands
|
|
62
|
+
// after text that already fit and nothing looks omitted.
|
|
63
|
+
const CFRange lineRange = CTLineGetStringRange(line);
|
|
64
|
+
const CFIndex from = lineRange.location;
|
|
65
|
+
NSAttributedString *rest =
|
|
66
|
+
[_attributedString attributedSubstringFromRange:NSMakeRange(
|
|
67
|
+
(NSUInteger)from, _attributedString.length - (NSUInteger)from)];
|
|
68
|
+
|
|
69
|
+
// The token inherits the attributes of the text it replaces, so an
|
|
70
|
+
// ellipsis in 40pt bold does not come out 14pt regular.
|
|
71
|
+
NSDictionary *attributes = _attributedString.length > 0
|
|
72
|
+
? [_attributedString attributesAtIndex:(NSUInteger)MAX((CFIndex)0, from) effectiveRange:nullptr]
|
|
73
|
+
: @{};
|
|
74
|
+
NSAttributedString *ellipsis = [[NSAttributedString alloc] initWithString:@"…"
|
|
75
|
+
attributes:attributes];
|
|
76
|
+
CTLineRef token = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)ellipsis);
|
|
77
|
+
CTLineRef full = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)rest);
|
|
78
|
+
CTLineRef truncated = CTLineCreateTruncatedLine(full, width, _truncationType, token);
|
|
79
|
+
CFRelease(token);
|
|
80
|
+
CFRelease(full);
|
|
81
|
+
if (truncated != nullptr) {
|
|
82
|
+
[collected addObject:(__bridge_transfer id)truncated];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[collected addObject:(__bridge id)line];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
CFRelease(frame);
|
|
91
|
+
return collected;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Line metrics, summed. Not CTFramesetterSuggestFrameSizeWithConstraints: that
|
|
95
|
+
// measures the whole text and cannot be told about a line limit, so a
|
|
96
|
+
// numberOfLines={2} paragraph would be laid out two lines tall and measured
|
|
97
|
+
// twelve.
|
|
98
|
+
- (CGSize)sizeForWidth:(CGFloat)maxWidth {
|
|
99
|
+
NSMutableArray *lines = [NSMutableArray array];
|
|
100
|
+
[self linesForWidth:maxWidth outLines:lines];
|
|
101
|
+
|
|
102
|
+
CGFloat width = 0;
|
|
103
|
+
CGFloat height = 0;
|
|
104
|
+
for (id item in lines) {
|
|
105
|
+
CTLineRef line = (__bridge CTLineRef)item;
|
|
106
|
+
CGFloat ascent = 0, descent = 0, leading = 0;
|
|
107
|
+
const double lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
108
|
+
width = MAX(width, (CGFloat)lineWidth);
|
|
109
|
+
height += std::ceil(ascent + descent + leading);
|
|
110
|
+
}
|
|
111
|
+
return CGSizeMake(std::ceil(width), height);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// How far along the line the text sits: 0 left, 0.5 centred, 1 right.
|
|
115
|
+
//
|
|
116
|
+
// Taken from the first fragment, because React Native resolves alignment onto
|
|
117
|
+
// every fragment from the <Text> that owns them -- the Pango side reads it the
|
|
118
|
+
// same way. Core Text puts alignment in the *frame*, and this draws lines one
|
|
119
|
+
// at a time so it can honour numberOfLines, so the frame's own alignment never
|
|
120
|
+
// applies and the offset has to be computed here. Without this every paragraph
|
|
121
|
+
// draws flush left and `textAlign` silently does nothing.
|
|
122
|
+
- (CGFloat)flushFactor {
|
|
123
|
+
if (_attributedString.length == 0) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
NSParagraphStyle *style = [_attributedString attribute:NSParagraphStyleAttributeName
|
|
127
|
+
atIndex:0
|
|
128
|
+
effectiveRange:nullptr];
|
|
129
|
+
switch (style.alignment) {
|
|
130
|
+
case NSTextAlignmentCenter:
|
|
131
|
+
return 0.5;
|
|
132
|
+
case NSTextAlignmentRight:
|
|
133
|
+
return 1.0;
|
|
134
|
+
default:
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
- (void)drawInContext:(CGContextRef)context size:(CGSize)size {
|
|
140
|
+
NSMutableArray *lines = [NSMutableArray array];
|
|
141
|
+
[self linesForWidth:size.width outLines:lines];
|
|
142
|
+
|
|
143
|
+
const CGFloat flush = [self flushFactor];
|
|
144
|
+
|
|
145
|
+
CGContextSaveGState(context);
|
|
146
|
+
// Core Text draws with the origin at the baseline and y increasing upward.
|
|
147
|
+
// The view above is flipped, so the context arrives y-down; this puts it back
|
|
148
|
+
// for the duration of the draw and leaves it as it was found.
|
|
149
|
+
CGContextTranslateCTM(context, 0, size.height);
|
|
150
|
+
CGContextScaleCTM(context, 1, -1);
|
|
151
|
+
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
|
|
152
|
+
|
|
153
|
+
CGFloat y = size.height;
|
|
154
|
+
for (id item in lines) {
|
|
155
|
+
CTLineRef line = (__bridge CTLineRef)item;
|
|
156
|
+
CGFloat ascent = 0, descent = 0, leading = 0;
|
|
157
|
+
CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
158
|
+
|
|
159
|
+
y -= std::ceil(ascent + descent + leading);
|
|
160
|
+
const CGFloat x = flush == 0 ? 0 : (CGFloat)CTLineGetPenOffsetForFlush(line, flush, size.width);
|
|
161
|
+
CGContextSetTextPosition(context, x, y + std::ceil(descent + leading));
|
|
162
|
+
CTLineDraw(line, context);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
CGContextRestoreGState(context);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The macOS view layer, driven by hand-written frames.
|
|
2
|
+
//
|
|
3
|
+
// The same standing-in-for-Fabric exercise as gtk/demo_layout_gtk.cpp, with the
|
|
4
|
+
// same boxes at the same coordinates, so the two platforms can be compared
|
|
5
|
+
// directly rather than by eye. Builds and runs without React Native's C++ core,
|
|
6
|
+
// which keeps the paint and placement path verifiable on its own.
|
|
7
|
+
//
|
|
8
|
+
// Three modes. BASALT_DUMP_TREE prints the tree and exits. BASALT_SNAPSHOT=<png>
|
|
9
|
+
// renders the hierarchy offscreen and writes it, which is the mode that proves
|
|
10
|
+
// anything: the tree dump would look identical whether the flip works or not,
|
|
11
|
+
// because it prints the frames that were set rather than where they landed.
|
|
12
|
+
// Neither needs a window. Without either, it opens one.
|
|
13
|
+
|
|
14
|
+
#import "AppKitSnapshot.h"
|
|
15
|
+
#import "RnAppKitView.h"
|
|
16
|
+
|
|
17
|
+
#include <cstdio>
|
|
18
|
+
#include <cstdlib>
|
|
19
|
+
|
|
20
|
+
static RnAppKitView *makeBox(NSInteger tag,
|
|
21
|
+
CGFloat x,
|
|
22
|
+
CGFloat y,
|
|
23
|
+
CGFloat w,
|
|
24
|
+
CGFloat h,
|
|
25
|
+
CGFloat r,
|
|
26
|
+
CGFloat g,
|
|
27
|
+
CGFloat b,
|
|
28
|
+
CGFloat a) {
|
|
29
|
+
RnAppKitView *view = [RnAppKitView viewWithTag:tag];
|
|
30
|
+
[view setRnFrameX:x y:y width:w height:h];
|
|
31
|
+
[view setRnBackgroundColorRed:r green:g blue:b alpha:a hasColor:YES];
|
|
32
|
+
return view;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static RnAppKitView *buildTree(void) {
|
|
36
|
+
RnAppKitView *root = makeBox(1, 0, 0, 640, 420, 0.12, 0.13, 0.16, 1.0);
|
|
37
|
+
|
|
38
|
+
// A row of children, absolutely placed the way Yoga would have resolved them.
|
|
39
|
+
RnAppKitView *a = makeBox(2, 32, 32, 240, 160, 0.30, 0.55, 0.95, 1.0);
|
|
40
|
+
RnAppKitView *b = makeBox(3, 296, 32, 240, 160, 0.95, 0.45, 0.35, 1.0);
|
|
41
|
+
RnAppKitView *c = makeBox(4, 32, 224, 504, 140, 0.35, 0.80, 0.55, 1.0);
|
|
42
|
+
|
|
43
|
+
[root insertRnChild:a atIndex:0];
|
|
44
|
+
[root insertRnChild:b atIndex:1];
|
|
45
|
+
[root insertRnChild:c atIndex:2];
|
|
46
|
+
|
|
47
|
+
// Nested child, to prove coordinates are parent-relative. On a flipped view
|
|
48
|
+
// this lands 24pt below `a`'s top edge; on an unflipped one it would land
|
|
49
|
+
// 24pt above its bottom, which is the bug the flip exists to prevent.
|
|
50
|
+
RnAppKitView *nested = makeBox(5, 24, 24, 120, 90, 1.0, 1.0, 1.0, 0.85);
|
|
51
|
+
[a insertRnChild:nested atIndex:0];
|
|
52
|
+
|
|
53
|
+
// Half-opacity child, to prove the opacity layer. Distinct from the alpha
|
|
54
|
+
// above: that one is a translucent colour, this one is a translucent view.
|
|
55
|
+
RnAppKitView *faded = makeBox(6, 24, 24, 160, 90, 0.1, 0.1, 0.1, 1.0);
|
|
56
|
+
[faded setRnOpacity:0.4];
|
|
57
|
+
[c insertRnChild:faded atIndex:0];
|
|
58
|
+
|
|
59
|
+
return root;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
int main(int argc, const char *argv[]) {
|
|
63
|
+
(void)argc;
|
|
64
|
+
(void)argv;
|
|
65
|
+
@autoreleasepool {
|
|
66
|
+
RnAppKitView *root = buildTree();
|
|
67
|
+
|
|
68
|
+
if (getenv("BASALT_DUMP_TREE") != nullptr) {
|
|
69
|
+
std::fputs([[root describeTree] UTF8String], stdout);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (const char *snapshotPath = getenv("BASALT_SNAPSHOT")) {
|
|
74
|
+
return RnAppKitWriteSnapshot(root, [NSString stringWithUTF8String:snapshotPath]) ? 0 : 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[NSApplication sharedApplication];
|
|
78
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
79
|
+
|
|
80
|
+
NSWindow *window =
|
|
81
|
+
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 640, 420)
|
|
82
|
+
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable
|
|
83
|
+
backing:NSBackingStoreBuffered
|
|
84
|
+
defer:NO];
|
|
85
|
+
window.title = @"basalt-core — macOS mounting skeleton";
|
|
86
|
+
window.contentView = root;
|
|
87
|
+
[window center];
|
|
88
|
+
[window makeKeyAndOrderFront:nil];
|
|
89
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
90
|
+
[NSApp run];
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
}
|