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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runMacos.js +33 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +298 -0
  5. package/native/appkit/AppKitAnimationChoreographer.h +54 -0
  6. package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
  7. package/native/appkit/AppKitDropTarget.h +34 -0
  8. package/native/appkit/AppKitDropTarget.mm +292 -0
  9. package/native/appkit/AppKitFocus.h +120 -0
  10. package/native/appkit/AppKitFocus.mm +293 -0
  11. package/native/appkit/AppKitImageLoader.h +80 -0
  12. package/native/appkit/AppKitImageLoader.mm +156 -0
  13. package/native/appkit/AppKitKeyEvents.h +26 -0
  14. package/native/appkit/AppKitKeyEvents.mm +97 -0
  15. package/native/appkit/AppKitMenuBar.mm +424 -0
  16. package/native/appkit/AppKitMountingManager.h +135 -0
  17. package/native/appkit/AppKitMountingManager.mm +946 -0
  18. package/native/appkit/AppKitRunLoopObserver.h +34 -0
  19. package/native/appkit/AppKitRunLoopObserver.mm +42 -0
  20. package/native/appkit/AppKitScrollView.h +168 -0
  21. package/native/appkit/AppKitScrollView.mm +550 -0
  22. package/native/appkit/AppKitSkiaContext.h +33 -0
  23. package/native/appkit/AppKitSkiaContext.mm +255 -0
  24. package/native/appkit/AppKitSkiaModule.h +53 -0
  25. package/native/appkit/AppKitSkiaModule.mm +81 -0
  26. package/native/appkit/AppKitSkiaPeer.h +37 -0
  27. package/native/appkit/AppKitSkiaPeer.mm +123 -0
  28. package/native/appkit/AppKitSkiaView.h +62 -0
  29. package/native/appkit/AppKitSnapshot.h +21 -0
  30. package/native/appkit/AppKitSnapshot.mm +91 -0
  31. package/native/appkit/AppKitTextInput.h +140 -0
  32. package/native/appkit/AppKitTextInput.mm +588 -0
  33. package/native/appkit/AppKitTextPeer.h +74 -0
  34. package/native/appkit/AppKitTextPeer.mm +353 -0
  35. package/native/appkit/AppKitTitleBar.h +90 -0
  36. package/native/appkit/AppKitTitleBar.mm +186 -0
  37. package/native/appkit/AppKitTouchDispatcher.h +138 -0
  38. package/native/appkit/AppKitTouchDispatcher.mm +514 -0
  39. package/native/appkit/AppKitWindowControl.mm +222 -0
  40. package/native/appkit/AppKitWindowModule.h +62 -0
  41. package/native/appkit/AppKitWindowModule.mm +194 -0
  42. package/native/appkit/ColorSchemeAppKit.mm +71 -0
  43. package/native/appkit/ComponentRegistryAppKit.mm +121 -0
  44. package/native/appkit/CoreTextLayout.h +43 -0
  45. package/native/appkit/CoreTextLayout.mm +252 -0
  46. package/native/appkit/CoreTextLayoutManager.mm +138 -0
  47. package/native/appkit/FontRegistryCoreText.mm +148 -0
  48. package/native/appkit/PlatformServicesAppKit.mm +540 -0
  49. package/native/appkit/RnAppKitView.h +417 -0
  50. package/native/appkit/RnAppKitView.mm +1605 -0
  51. package/native/appkit/RnTextLayout.h +38 -0
  52. package/native/appkit/RnTextLayout.mm +168 -0
  53. package/native/appkit/demo_layout_appkit.mm +93 -0
  54. package/native/appkit/main_appkit.mm +1860 -0
  55. package/native/appkit/mount_harness_appkit.mm +197 -0
  56. package/native/tests/test_appkit_accessibility.mm +170 -0
  57. package/native/tests/test_appkit_image.mm +237 -0
  58. package/native/tests/test_appkit_input.mm +852 -0
  59. package/native/tests/test_appkit_key_events.mm +150 -0
  60. package/native/tests/test_appkit_mounting.mm +430 -0
  61. package/native/tests/test_appkit_scrollbar.mm +132 -0
  62. package/native/tests/test_appkit_services.mm +57 -0
  63. package/native/tests/test_appkit_skia.mm +212 -0
  64. package/native/tests/test_appkit_text.mm +180 -0
  65. package/native/tests/test_appkit_textinput.mm +748 -0
  66. package/native/tests/test_appkit_titlebar.mm +107 -0
  67. package/native/tests/test_appkit_view.mm +237 -0
  68. package/package.json +26 -0
  69. 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
+ }