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,107 @@
1
+ // The title bar's metrics, which are the only part of it a test can reach.
2
+ //
3
+ // What the two styles *look* like is in the screenshots that went with phase
4
+ // 58; what is assertable is the arithmetic an app lays itself out against, and
5
+ // that arithmetic is the whole contract src/TitleBar.js has with the host.
6
+ //
7
+ // A real NSWindow, never ordered front. Its metrics are a property of the
8
+ // window rather than of anything on screen, so nothing here has to be shown.
9
+
10
+ #include "TestHarness.h"
11
+
12
+ #include <sstream>
13
+
14
+ #import "AppKitTitleBar.h"
15
+
16
+ TEST(titlebar_native_style_costs_the_app_nothing) {
17
+ @autoreleasepool {
18
+ NSWindow *window = [[NSWindow alloc]
19
+ initWithContentRect:NSMakeRect(0, 0, 400, 300)
20
+ styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
21
+ NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
22
+ backing:NSBackingStoreBuffered
23
+ defer:NO];
24
+ basalt::titleBar().attach(window);
25
+ basalt::titleBar().setStyle(basalt::TitleBarStyle::Native);
26
+
27
+ // The system's title bar sits outside the content view, so it takes
28
+ // nothing from the app and there is nothing to leave clear. Windows
29
+ // reports the same zeroes for the same reason.
30
+ const auto metrics = basalt::titleBar().metrics();
31
+ EXPECT(metrics.style == basalt::TitleBarStyle::Native);
32
+ EXPECT_EQ(metrics.height, 0.0);
33
+ EXPECT_EQ(metrics.buttonsWidth, 0.0);
34
+ }
35
+ }
36
+
37
+ TEST(titlebar_hidden_style_reports_what_to_leave_clear) {
38
+ @autoreleasepool {
39
+ NSWindow *window = [[NSWindow alloc]
40
+ initWithContentRect:NSMakeRect(0, 0, 400, 300)
41
+ styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
42
+ NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
43
+ backing:NSBackingStoreBuffered
44
+ defer:NO];
45
+ basalt::titleBar().attach(window);
46
+ basalt::titleBar().setStyle(basalt::TitleBarStyle::Hidden);
47
+
48
+ const auto metrics = basalt::titleBar().metrics();
49
+ EXPECT(metrics.style == basalt::TitleBarStyle::Hidden);
50
+ // A real height rather than a guess: the content view now extends under
51
+ // the title bar, and this is how much of it the app must not fill.
52
+ EXPECT(metrics.height > 0.0);
53
+ // And the traffic lights, which the system keeps drawing over the app's
54
+ // own header -- the counterpart of the caption buttons on Windows.
55
+ EXPECT(metrics.buttonsWidth > 0.0);
56
+
57
+ // The window really did take the style, rather than the metrics being
58
+ // computed from a flag nothing acted on.
59
+ EXPECT((window.styleMask & NSWindowStyleMaskFullSizeContentView) != 0);
60
+ EXPECT(window.titlebarAppearsTransparent);
61
+
62
+ // And back, so a request that is unmounted restores the window.
63
+ basalt::titleBar().setStyle(basalt::TitleBarStyle::Native);
64
+ EXPECT((window.styleMask & NSWindowStyleMaskFullSizeContentView) == 0);
65
+ EXPECT_EQ(basalt::titleBar().metrics().height, 0.0);
66
+ }
67
+ }
68
+
69
+ TEST(titlebar_takes_a_title_and_a_colour) {
70
+ @autoreleasepool {
71
+ NSWindow *window = [[NSWindow alloc]
72
+ initWithContentRect:NSMakeRect(0, 0, 400, 300)
73
+ styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable
74
+ backing:NSBackingStoreBuffered
75
+ defer:NO];
76
+ basalt::titleBar().attach(window);
77
+ basalt::titleBar().setStyle(basalt::TitleBarStyle::Native);
78
+ basalt::titleBar().setTitle(std::string("Inbox"));
79
+ EXPECT([window.title isEqualToString:@"Inbox"]);
80
+
81
+ // A dark caption asks for the dark appearance, because AppKit has no
82
+ // caption text colour of its own -- the appearance is what decides it.
83
+ basalt::titleBar().setColors(0xFF1A1E28u, std::nullopt, std::nullopt);
84
+ EXPECT(window.titlebarAppearsTransparent);
85
+ EXPECT([window.appearance.name isEqualToString:NSAppearanceNameDarkAqua]);
86
+
87
+ // And a light one asks for the light appearance.
88
+ basalt::titleBar().setColors(0xFFF5F5F5u, std::nullopt, std::nullopt);
89
+ EXPECT([window.appearance.name isEqualToString:NSAppearanceNameAqua]);
90
+ }
91
+ }
92
+
93
+ // The caption buttons' functions. What they do was checked live -- a probe
94
+ // drove the window from 900x700 to maximised and back, then minimised and
95
+ // closed it. What a test pins is the half with no window on the other end: the
96
+ // module outlives the window at shutdown, and a call arriving then must not
97
+ // take the process with it.
98
+ TEST(titlebar_actions_are_safe_with_no_window) {
99
+ @autoreleasepool {
100
+ basalt::titleBar().attach(nil);
101
+ basalt::titleBar().minimize();
102
+ basalt::titleBar().toggleMaximize();
103
+ basalt::titleBar().close();
104
+ basalt::titleBar().startDrag();
105
+ EXPECT(true);
106
+ }
107
+ }
@@ -0,0 +1,237 @@
1
+ // Tests for the macOS view layer.
2
+ //
3
+ // RnAppKitView has no React Native dependency, so these need no Fabric machinery
4
+ // and no window. They share the toolkit-free harness the GTK tests use, which
5
+ // is the first small piece of evidence that a second platform can reuse what is
6
+ // here rather than reimplement it.
7
+
8
+ #include "TestHarness.h"
9
+
10
+ #import "RnAppKitView.h"
11
+
12
+ #include <sstream>
13
+
14
+ namespace {
15
+
16
+ // Components of a layer's background colour, or all -1 if it has none.
17
+ struct Background {
18
+ CGFloat r = -1, g = -1, b = -1, a = -1;
19
+ bool present = false;
20
+ };
21
+
22
+ Background backgroundOf(RnAppKitView *view) {
23
+ Background result;
24
+ CGColorRef color = view.layer.backgroundColor;
25
+ if (color == nullptr) {
26
+ return result;
27
+ }
28
+ const CGFloat *components = CGColorGetComponents(color);
29
+ result.present = true;
30
+ result.r = components[0];
31
+ result.g = components[1];
32
+ result.b = components[2];
33
+ result.a = components[3];
34
+ return result;
35
+ }
36
+
37
+ RnAppKitView *box(NSInteger tag, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
38
+ RnAppKitView *view = [RnAppKitView viewWithTag:tag];
39
+ [view setRnFrameX:x y:y width:w height:h];
40
+ return view;
41
+ }
42
+
43
+ NSInteger tagOfSubview(RnAppKitView *parent, NSUInteger index) {
44
+ return ((RnAppKitView *)parent.subviews[index]).rnTag;
45
+ }
46
+
47
+ } // namespace
48
+
49
+ TEST(appkit_view_stores_its_tag_and_frame) {
50
+ @autoreleasepool {
51
+ RnAppKitView *view = box(7, 1.5, 2.5, 30, 40);
52
+
53
+ EXPECT_EQ((long)view.rnTag, 7L);
54
+ EXPECT_NEAR(view.frame.origin.x, 1.5, 0.001);
55
+ EXPECT_NEAR(view.frame.origin.y, 2.5, 0.001);
56
+ EXPECT_NEAR(view.frame.size.width, 30.0, 0.001);
57
+ EXPECT_NEAR(view.frame.size.height, 40.0, 0.001);
58
+ }
59
+ }
60
+
61
+ // The reason the view exists at all. AppKit's origin is bottom-left; every
62
+ // frame Fabric produces assumes top-left, so a child at y=32 in a 420-tall
63
+ // parent must sit 32 from the top and not 32 from the bottom.
64
+ TEST(appkit_view_is_flipped_so_frames_are_top_left) {
65
+ @autoreleasepool {
66
+ RnAppKitView *parent = box(1, 0, 0, 640, 420);
67
+ RnAppKitView *child = box(2, 32, 32, 240, 160);
68
+ [parent insertRnChild:child atIndex:0];
69
+
70
+ EXPECT(parent.isFlipped);
71
+
72
+ // The child's own origin, expressed in the parent. Unflipped this would be
73
+ // 228 -- 420 - 32 - 160 -- which is the bug in numbers.
74
+ const NSPoint origin = [child convertPoint:NSZeroPoint toView:parent];
75
+ EXPECT_NEAR(origin.x, 32.0, 0.001);
76
+ EXPECT_NEAR(origin.y, 32.0, 0.001);
77
+ }
78
+ }
79
+
80
+ TEST(appkit_view_children_insert_at_the_requested_index) {
81
+ @autoreleasepool {
82
+ RnAppKitView *parent = box(1, 0, 0, 100, 100);
83
+ RnAppKitView *a = box(10, 0, 0, 10, 10);
84
+ RnAppKitView *b = box(11, 0, 0, 10, 10);
85
+ RnAppKitView *c = box(12, 0, 0, 10, 10);
86
+
87
+ [parent insertRnChild:a atIndex:0];
88
+ [parent insertRnChild:b atIndex:1];
89
+ // Into the middle, which is the case an append-only implementation passes
90
+ // every other test without handling.
91
+ [parent insertRnChild:c atIndex:1];
92
+
93
+ EXPECT_EQ((long)parent.subviews.count, 3L);
94
+ EXPECT_EQ((long)tagOfSubview(parent, 0), 10L);
95
+ EXPECT_EQ((long)tagOfSubview(parent, 1), 12L);
96
+ EXPECT_EQ((long)tagOfSubview(parent, 2), 11L);
97
+
98
+ [parent removeRnChild:c];
99
+ EXPECT_EQ((long)parent.subviews.count, 2L);
100
+ EXPECT_EQ((long)tagOfSubview(parent, 0), 10L);
101
+ EXPECT_EQ((long)tagOfSubview(parent, 1), 11L);
102
+
103
+ // Removed, not destroyed: a reinsertion elsewhere has to still work.
104
+ EXPECT(c.superview == nil);
105
+ RnAppKitView *other = box(2, 0, 0, 100, 100);
106
+ [other insertRnChild:c atIndex:0];
107
+ EXPECT_EQ((long)tagOfSubview(other, 0), 12L);
108
+ }
109
+ }
110
+
111
+ // Removing a view that is not ours must not detach it from whoever owns it.
112
+ TEST(appkit_view_removes_only_its_own_child) {
113
+ @autoreleasepool {
114
+ RnAppKitView *owner = box(1, 0, 0, 100, 100);
115
+ RnAppKitView *stranger = box(2, 0, 0, 100, 100);
116
+ RnAppKitView *child = box(10, 0, 0, 10, 10);
117
+
118
+ [owner insertRnChild:child atIndex:0];
119
+ [stranger removeRnChild:child];
120
+
121
+ EXPECT_EQ((long)owner.subviews.count, 1L);
122
+ EXPECT(child.superview == owner);
123
+ }
124
+ }
125
+
126
+ TEST(appkit_view_background_reaches_the_layer) {
127
+ @autoreleasepool {
128
+ RnAppKitView *view = box(1, 0, 0, 10, 10);
129
+ EXPECT(!backgroundOf(view).present);
130
+
131
+ [view setRnBackgroundColorRed:0.25 green:0.5 blue:0.75 alpha:0.5 hasColor:YES];
132
+ const Background set = backgroundOf(view);
133
+ EXPECT(set.present);
134
+ EXPECT_NEAR(set.r, 0.25, 0.001);
135
+ EXPECT_NEAR(set.g, 0.5, 0.001);
136
+ EXPECT_NEAR(set.b, 0.75, 0.001);
137
+ EXPECT_NEAR(set.a, 0.5, 0.001);
138
+
139
+ // sRGB, not the display's space. The same colour has to come out the same
140
+ // on a wide-gamut Mac as it does on Linux, or "consistent across platforms"
141
+ // stops being true in the one place nobody thinks to check.
142
+ CGColorSpaceRef space = CGColorGetColorSpace(view.layer.backgroundColor);
143
+ CFStringRef name = CGColorSpaceCopyName(space);
144
+ EXPECT(name != nullptr && CFEqual(name, kCGColorSpaceSRGB));
145
+ if (name != nullptr) {
146
+ CFRelease(name);
147
+ }
148
+
149
+ // No background is not transparent black: the layer must have no colour at
150
+ // all, so whatever is behind it shows through unmodified.
151
+ [view setRnBackgroundColorRed:0.25 green:0.5 blue:0.75 alpha:0.5 hasColor:NO];
152
+ EXPECT(!backgroundOf(view).present);
153
+ }
154
+ }
155
+
156
+ TEST(appkit_view_opacity_clipping_and_radius_reach_the_layer) {
157
+ @autoreleasepool {
158
+ RnAppKitView *view = box(1, 0, 0, 10, 10);
159
+
160
+ EXPECT_NEAR(view.layer.opacity, 1.0, 0.001);
161
+ EXPECT(!view.layer.masksToBounds);
162
+ EXPECT_NEAR(view.layer.cornerRadius, 0.0, 0.001);
163
+
164
+ [view setRnOpacity:0.4];
165
+ [view setRnClipsChildren:YES];
166
+ [view setRnCornerRadius:8];
167
+
168
+ EXPECT_NEAR(view.layer.opacity, 0.4, 0.001);
169
+ EXPECT(view.layer.masksToBounds);
170
+ EXPECT_NEAR(view.layer.cornerRadius, 8.0, 0.001);
171
+ }
172
+ }
173
+
174
+ // A scroll offset moves the children and leaves their frames alone. Through
175
+ // bounds.origin, so AppKit shifts drawing, clipping and hit testing together --
176
+ // an implementation that moved each child instead would have to undo it on
177
+ // every mutation, and would fight the frames Yoga produced.
178
+ TEST(appkit_scroll_offset_moves_children_without_moving_their_frames) {
179
+ @autoreleasepool {
180
+ RnAppKitView *page = box(99, 0, 0, 400, 400);
181
+ RnAppKitView *scroller = box(1, 0, 0, 200, 100);
182
+ RnAppKitView *content = box(2, 0, 0, 200, 600);
183
+ [page insertRnChild:scroller atIndex:0];
184
+ [scroller insertRnChild:content atIndex:0];
185
+
186
+ // Measured against the *page*, not the scroller. A view's own coordinate
187
+ // system is its bounds space, so converting into the scroller would report
188
+ // the frame back unchanged however far it had scrolled -- which is exactly
189
+ // the wrong question, and the first version of this test asked it.
190
+ EXPECT_NEAR(scroller.rnScrollOffset.y, 0.0, 0.001);
191
+ EXPECT_NEAR([content convertPoint:NSZeroPoint toView:page].y, 0.0, 0.001);
192
+
193
+ [scroller setRnScrollOffsetX:0 y:150];
194
+
195
+ EXPECT_NEAR(scroller.rnScrollOffset.y, 150.0, 0.001);
196
+ // The frame is untouched: it is still what the shadow tree assigned.
197
+ EXPECT_NEAR(content.frame.origin.y, 0.0, 0.001);
198
+ // But it now sits 150 above the scroller's visible top.
199
+ EXPECT_NEAR([content convertPoint:NSZeroPoint toView:page].y, -150.0, 0.001);
200
+ }
201
+ }
202
+
203
+ TEST(appkit_scroll_offset_is_reported_in_the_tree) {
204
+ @autoreleasepool {
205
+ RnAppKitView *scroller = box(1, 0, 0, 200, 100);
206
+ [scroller setRnScrollOffsetX:0 y:40];
207
+
208
+ const std::string described = [scroller describeTree].UTF8String;
209
+ // The same `scroll=(x,y)` the GTK side emits, so a scrolled list shows up
210
+ // in the cross-platform diff.
211
+ EXPECT(described.find("scroll=(0,40)") != std::string::npos);
212
+ }
213
+ }
214
+
215
+ // The same format the GTK side emits, character for character, so the two
216
+ // platforms can be compared without reading two renderers.
217
+ TEST(appkit_view_describes_its_tree_like_the_gtk_one) {
218
+ @autoreleasepool {
219
+ RnAppKitView *root = box(1, 0, 0, 640, 420);
220
+ [root setRnBackgroundColorRed:0.12 green:0.13 blue:0.16 alpha:1.0 hasColor:YES];
221
+
222
+ RnAppKitView *child = box(2, 32, 32, 240, 160);
223
+ [child setRnOpacity:0.4];
224
+ [child setRnClipsChildren:YES];
225
+ [root insertRnChild:child atIndex:0];
226
+
227
+ const std::string described = [root describeTree].UTF8String;
228
+ const std::string expected =
229
+ "view tag=1 frame=(0,0 640x420) bg=#1f2129ff\n"
230
+ " view tag=2 frame=(32,32 240x160) opacity=0.4 clip\n";
231
+ EXPECT_EQ(described, expected);
232
+ }
233
+ }
234
+
235
+ int main(int argc, char **argv) {
236
+ return basalt::testing::runAllTests(argc, argv);
237
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "basalt-appkit",
3
+ "version": "0.0.0-rc5",
4
+ "description": "The macOS view layer for basalt-core: an AppKit view layer, a Fabric mounting manager, and a host. Everything behind them is in basalt-core.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vincentvella/basalt-core.git",
9
+ "directory": "packages/basalt-appkit"
10
+ },
11
+ "exports": {
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "cli",
16
+ "native",
17
+ "react-native.config.js"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": "*",
21
+ "react-native": "*"
22
+ },
23
+ "dependencies": {
24
+ "basalt-core": "0.0.0-rc5"
25
+ }
26
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What React Native's CLI reads out of this package.
3
+ *
4
+ * Just the `run-macos` command. The platform *declarations* -- which make
5
+ * `--platform` accept this one at all -- are in basalt-core, because
6
+ * they are the same on every desktop and an app targeting only one of them
7
+ * still needs them.
8
+ *
9
+ * The CLI reads a react-native.config.js out of every dependency and merges
10
+ * them, so an app with more than one of these packages installed gets the
11
+ * platforms from the shared one and a command from each of the others, without
12
+ * any of them knowing about the rest.
13
+ *
14
+ * @format
15
+ */
16
+
17
+ // @ts-check
18
+ 'use strict';
19
+
20
+ module.exports = {
21
+ commands: [require('./cli/runMacos')],
22
+ };