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,150 @@
1
+ // An NSEvent as a key combination.
2
+ //
3
+ // The mapping is the part of key handling that is this host's alone -- the
4
+ // registry and the matching are core's, and tested there -- and it is where the
5
+ // silent failures live. A key named "Arrowleft" matches nothing and reports
6
+ // nothing, and an app that bound "ArrowLeft" simply finds its shortcut dead.
7
+
8
+ #include "TestHarness.h"
9
+
10
+ #import "AppKitKeyEvents.h"
11
+
12
+ #import <AppKit/AppKit.h>
13
+
14
+ #include <sstream>
15
+ #include <string>
16
+
17
+ namespace {
18
+
19
+ // AppKit does not let you make an NSEvent from nothing, but it does let you
20
+ // describe one. The characters are what a real press would carry: `characters`
21
+ // with modifiers applied, `charactersIgnoringModifiers` without.
22
+ NSEvent *keyPress(NSString *characters, NSString *bare, NSEventModifierFlags flags) {
23
+ return [NSEvent keyEventWithType:NSEventTypeKeyDown
24
+ location:NSZeroPoint
25
+ modifierFlags:flags
26
+ timestamp:0
27
+ windowNumber:0
28
+ context:nil
29
+ characters:characters
30
+ charactersIgnoringModifiers:bare
31
+ isARepeat:NO
32
+ keyCode:0];
33
+ }
34
+
35
+ std::string keyOf(NSEvent *event) {
36
+ const auto combination = basalt::keyCombinationFrom(event);
37
+ return combination.has_value() ? combination->key : std::string("(none)");
38
+ }
39
+
40
+ basalt::KeyModifiers modifiersOf(NSEvent *event) {
41
+ const auto combination = basalt::keyCombinationFrom(event);
42
+ return combination.has_value() ? combination->modifiers : basalt::KeyModifiers{};
43
+ }
44
+
45
+ // A character in the private-use area, which is how AppKit reports the arrows.
46
+ NSString *functionKey(unichar code) {
47
+ return [NSString stringWithCharacters:&code length:1];
48
+ }
49
+
50
+ } // namespace
51
+
52
+ TEST(appkit_keys_a_plain_letter_is_itself) {
53
+ EXPECT_EQ(keyOf(keyPress(@"a", @"a", 0)), std::string("a"));
54
+ EXPECT(modifiersOf(keyPress(@"a", @"a", 0)) == basalt::KeyModifiers{});
55
+ }
56
+
57
+ TEST(appkit_keys_shift_changes_the_character_and_sets_the_modifier) {
58
+ // Both, as a browser reports both: the key is "A" and shiftKey is true. An app
59
+ // may bind either and neither is wrong -- which is only true if the character
60
+ // comes from `characters` rather than `charactersIgnoringModifiers`.
61
+ NSEvent *event = keyPress(@"A", @"a", NSEventModifierFlagShift);
62
+ EXPECT_EQ(keyOf(event), std::string("A"));
63
+ EXPECT(modifiersOf(event).shift);
64
+ }
65
+
66
+ TEST(appkit_keys_command_is_meta) {
67
+ // As in a browser on this platform, and as react-native-macos spells it: an
68
+ // app binding Cmd+Z writes metaKey. Calling it `cmd` would have been clearer
69
+ // here and wrong everywhere an app is shared.
70
+ NSEvent *event = keyPress(@"z", @"z", NSEventModifierFlagCommand);
71
+ EXPECT_EQ(keyOf(event), std::string("z"));
72
+ EXPECT(modifiersOf(event).meta);
73
+ EXPECT(!modifiersOf(event).ctrl);
74
+ }
75
+
76
+ TEST(appkit_keys_control_reports_the_letter_not_the_control_character) {
77
+ // Ctrl+A arrives with `characters` as 0x01, which is not a name anybody binds.
78
+ // The unmodified character is what the app means.
79
+ NSEvent *event = keyPress(@"\x01", @"a", NSEventModifierFlagControl);
80
+ EXPECT_EQ(keyOf(event), std::string("a"));
81
+ EXPECT(modifiersOf(event).ctrl);
82
+ }
83
+
84
+ TEST(appkit_keys_the_arrows_have_browser_names) {
85
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSLeftArrowFunctionKey),
86
+ functionKey(NSLeftArrowFunctionKey), 0)),
87
+ std::string("ArrowLeft"));
88
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSRightArrowFunctionKey),
89
+ functionKey(NSRightArrowFunctionKey), 0)),
90
+ std::string("ArrowRight"));
91
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSUpArrowFunctionKey),
92
+ functionKey(NSUpArrowFunctionKey), 0)),
93
+ std::string("ArrowUp"));
94
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSDownArrowFunctionKey),
95
+ functionKey(NSDownArrowFunctionKey), 0)),
96
+ std::string("ArrowDown"));
97
+ }
98
+
99
+ TEST(appkit_keys_the_navigation_block_has_browser_names) {
100
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSPageUpFunctionKey),
101
+ functionKey(NSPageUpFunctionKey), 0)),
102
+ std::string("PageUp"));
103
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSHomeFunctionKey),
104
+ functionKey(NSHomeFunctionKey), 0)),
105
+ std::string("Home"));
106
+ // Forward delete is "Delete"; the key most keyboards label delete is
107
+ // backspace, and conflating them is the mistake this asserts against.
108
+ EXPECT_EQ(keyOf(keyPress(functionKey(NSDeleteFunctionKey),
109
+ functionKey(NSDeleteFunctionKey), 0)),
110
+ std::string("Delete"));
111
+ EXPECT_EQ(keyOf(keyPress(@"\x7F", @"\x7F", 0)), std::string("Backspace"));
112
+ }
113
+
114
+ TEST(appkit_keys_escape_enter_tab_and_space) {
115
+ EXPECT_EQ(keyOf(keyPress(@"\x1B", @"\x1B", 0)), std::string("Escape"));
116
+ EXPECT_EQ(keyOf(keyPress(@"\r", @"\r", 0)), std::string("Enter"));
117
+ EXPECT_EQ(keyOf(keyPress(@"\t", @"\t", 0)), std::string("Tab"));
118
+ // A space is a character and is named " ", not "Space". The one that looks
119
+ // like a mistake.
120
+ EXPECT_EQ(keyOf(keyPress(@" ", @" ", 0)), std::string(" "));
121
+ }
122
+
123
+ TEST(appkit_keys_shift_tab_is_tab_with_shift) {
124
+ // AppKit sends back-tab as its own character rather than Tab with a modifier.
125
+ // Reported as Tab plus shift, because that is what a browser reports and what
126
+ // an app will have bound.
127
+ NSEvent *event = keyPress(@"\x19", @"\x19", NSEventModifierFlagShift);
128
+ EXPECT_EQ(keyOf(event), std::string("Tab"));
129
+ EXPECT(modifiersOf(event).shift);
130
+ }
131
+
132
+ TEST(appkit_keys_a_bare_modifier_is_not_a_combination) {
133
+ // Holding Command produces an event with no characters. Offering that to an
134
+ // app as a combination would fire a shortcut on the modifier alone.
135
+ EXPECT_EQ(keyOf(keyPress(@"", @"", NSEventModifierFlagCommand)), std::string("(none)"));
136
+ EXPECT_EQ(keyOf(nil), std::string("(none)"));
137
+ }
138
+
139
+ TEST(appkit_keys_several_modifiers_at_once) {
140
+ NSEvent *event = keyPress(@"z", @"z",
141
+ NSEventModifierFlagCommand | NSEventModifierFlagShift);
142
+ const basalt::KeyModifiers modifiers = modifiersOf(event);
143
+ EXPECT(modifiers.meta);
144
+ EXPECT(modifiers.shift);
145
+ EXPECT(!modifiers.alt);
146
+ EXPECT(!modifiers.ctrl);
147
+ // And it is a different combination from Cmd+Z, which is why redo does not
148
+ // fire undo. core/KeyEvents.h asserts the matching half of this.
149
+ EXPECT_EQ(keyOf(event), std::string("z"));
150
+ }
@@ -0,0 +1,430 @@
1
+ // Tests for AppKitMountingManager: does a stream of ShadowViewMutations produce
2
+ // the view tree it describes?
3
+ //
4
+ // Deliberately the same seven questions `tests/test_mounting.cpp` asks of the
5
+ // GTK one, in the same order, with the same tags and frames. The two managers
6
+ // share their walk now, so most of this is asking whether the sharing actually
7
+ // holds -- and the day it stops holding, these fail here rather than in an app.
8
+
9
+ #include "TestHarness.h"
10
+ #include "TreeDump.h"
11
+
12
+ #import "AppKitMountingManager.h"
13
+
14
+ #include <react/renderer/components/view/ViewProps.h>
15
+ #include <react/renderer/graphics/Color.h>
16
+
17
+ #include <sstream>
18
+ #include <vector>
19
+
20
+ using facebook::react::ColorComponents;
21
+ using facebook::react::LayoutMetrics;
22
+ using facebook::react::MountingTransaction;
23
+ using facebook::react::ShadowView;
24
+ using facebook::react::ShadowViewMutation;
25
+ using facebook::react::ShadowViewMutationList;
26
+ using facebook::react::SurfaceId;
27
+ using facebook::react::Tag;
28
+ using facebook::react::TransactionTelemetry;
29
+ using facebook::react::ViewProps;
30
+
31
+ namespace {
32
+
33
+ constexpr SurfaceId kSurfaceId = 1;
34
+
35
+ ShadowView makeView(Tag tag,
36
+ float x,
37
+ float y,
38
+ float width,
39
+ float height,
40
+ float red = 0.0F,
41
+ float green = 0.0F,
42
+ float blue = 0.0F) {
43
+ auto props = std::make_shared<ViewProps>();
44
+ props->backgroundColor = facebook::react::colorFromComponents(
45
+ ColorComponents{.red = red, .green = green, .blue = blue, .alpha = 1.0F});
46
+
47
+ LayoutMetrics metrics;
48
+ metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
49
+
50
+ ShadowView view;
51
+ view.componentName = "View";
52
+ view.surfaceId = kSurfaceId;
53
+ view.tag = tag;
54
+ view.props = props;
55
+ view.layoutMetrics = metrics;
56
+ return view;
57
+ }
58
+
59
+ // Applies mutations synchronously. executeMount would queue onto the main
60
+ // queue, which a test has no reason to spin.
61
+ void apply(basalt::AppKitMountingManager &manager, ShadowViewMutationList &&mutations) {
62
+ manager.applyTransaction(
63
+ kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
64
+ }
65
+
66
+ NSInteger childCount(RnAppKitView *view) {
67
+ return (NSInteger)view.subviews.count;
68
+ }
69
+
70
+ // The tag of the nth child, or -1.
71
+ NSInteger childTagAt(RnAppKitView *view, NSInteger index) {
72
+ if (index < 0 || index >= (NSInteger)view.subviews.count) {
73
+ return -1;
74
+ }
75
+ return ((RnAppKitView *)view.subviews[(NSUInteger)index]).rnTag;
76
+ }
77
+
78
+ } // namespace
79
+
80
+ TEST(appkit_create_and_insert_builds_the_tree) {
81
+ @autoreleasepool {
82
+ basalt::AppKitMountingManager manager;
83
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
84
+
85
+ ShadowViewMutationList mutations;
86
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(10, 0, 0, 100, 50)));
87
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(11, 0, 60, 100, 50)));
88
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(10, 0, 0, 100, 50), 0));
89
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(11, 0, 60, 100, 50), 1));
90
+ apply(manager, std::move(mutations));
91
+
92
+ EXPECT_EQ((long)childCount(root), 2L);
93
+ EXPECT_EQ((long)childTagAt(root, 0), 10L);
94
+ EXPECT_EQ((long)childTagAt(root, 1), 11L);
95
+
96
+ manager.destroySurfaceRoot(kSurfaceId);
97
+ }
98
+ }
99
+
100
+ TEST(appkit_insert_honours_the_index) {
101
+ @autoreleasepool {
102
+ basalt::AppKitMountingManager manager;
103
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
104
+
105
+ ShadowViewMutationList first;
106
+ first.push_back(ShadowViewMutation::CreateMutation(makeView(20, 0, 0, 10, 10)));
107
+ first.push_back(ShadowViewMutation::CreateMutation(makeView(21, 0, 0, 10, 10)));
108
+ first.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(20, 0, 0, 10, 10), 0));
109
+ first.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(21, 0, 0, 10, 10), 1));
110
+ apply(manager, std::move(first));
111
+
112
+ // Fabric numbers inserts by their position in the parent's final child
113
+ // list, so a later mutation can land between two existing children. On
114
+ // AppKit that means addSubview:positioned:relativeTo: rather than a plain
115
+ // append, and getting it wrong reorders the z-order too.
116
+ ShadowViewMutationList second;
117
+ second.push_back(ShadowViewMutation::CreateMutation(makeView(22, 0, 0, 10, 10)));
118
+ second.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(22, 0, 0, 10, 10), 1));
119
+ apply(manager, std::move(second));
120
+
121
+ EXPECT_EQ((long)childCount(root), 3L);
122
+ EXPECT_EQ((long)childTagAt(root, 0), 20L);
123
+ EXPECT_EQ((long)childTagAt(root, 1), 22L);
124
+ EXPECT_EQ((long)childTagAt(root, 2), 21L);
125
+
126
+ manager.destroySurfaceRoot(kSurfaceId);
127
+ }
128
+ }
129
+
130
+ TEST(appkit_remove_detaches_but_does_not_destroy) {
131
+ @autoreleasepool {
132
+ basalt::AppKitMountingManager manager;
133
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
134
+
135
+ ShadowViewMutationList mutations;
136
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(30, 0, 0, 10, 10)));
137
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
138
+ apply(manager, std::move(mutations));
139
+
140
+ ShadowViewMutationList removal;
141
+ removal.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
142
+ apply(manager, std::move(removal));
143
+
144
+ // Detached from the parent...
145
+ EXPECT_EQ((long)childCount(root), 0L);
146
+
147
+ // ...but still alive, because a re-Insert may follow before the Delete.
148
+ // Under ARC that invariant rests on the registry's map entry rather than on
149
+ // an explicit ref, which is exactly the sort of difference that would go
150
+ // unnoticed until a view vanished mid-reparent.
151
+ ShadowViewMutationList reinsert;
152
+ reinsert.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
153
+ apply(manager, std::move(reinsert));
154
+
155
+ EXPECT_EQ((long)childCount(root), 1L);
156
+ EXPECT_EQ((long)childTagAt(root, 0), 30L);
157
+
158
+ manager.destroySurfaceRoot(kSurfaceId);
159
+ }
160
+ }
161
+
162
+ TEST(appkit_update_changes_frame_without_reparenting) {
163
+ @autoreleasepool {
164
+ basalt::AppKitMountingManager manager;
165
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
166
+
167
+ ShadowViewMutationList mutations;
168
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(40, 5, 6, 100, 50)));
169
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(40, 5, 6, 100, 50), 0));
170
+ apply(manager, std::move(mutations));
171
+
172
+ ShadowViewMutationList update;
173
+ update.push_back(ShadowViewMutation::UpdateMutation(
174
+ makeView(40, 5, 6, 100, 50), makeView(40, 7, 8, 200, 75), kSurfaceId));
175
+ apply(manager, std::move(update));
176
+
177
+ EXPECT_EQ((long)childCount(root), 1L);
178
+
179
+ const NSRect frame = ((RnAppKitView *)root.subviews[0]).frame;
180
+ EXPECT_NEAR(frame.origin.x, 7.0, 0.001);
181
+ EXPECT_NEAR(frame.origin.y, 8.0, 0.001);
182
+ EXPECT_NEAR(frame.size.width, 200.0, 0.001);
183
+ EXPECT_NEAR(frame.size.height, 75.0, 0.001);
184
+
185
+ manager.destroySurfaceRoot(kSurfaceId);
186
+ }
187
+ }
188
+
189
+ TEST(appkit_delete_removes_the_view_from_the_registry) {
190
+ @autoreleasepool {
191
+ basalt::AppKitMountingManager manager;
192
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
193
+
194
+ ShadowViewMutationList mutations;
195
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(50, 0, 0, 10, 10)));
196
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
197
+ apply(manager, std::move(mutations));
198
+
199
+ ShadowViewMutationList teardown;
200
+ teardown.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
201
+ teardown.push_back(ShadowViewMutation::DeleteMutation(makeView(50, 0, 0, 10, 10)));
202
+ apply(manager, std::move(teardown));
203
+
204
+ EXPECT_EQ((long)childCount(root), 0L);
205
+ EXPECT(manager.viewForTag(50) == nil);
206
+
207
+ // A stray Insert for a deleted tag must be ignored rather than crash, since
208
+ // that is what a bug upstream would look like from here.
209
+ ShadowViewMutationList stray;
210
+ stray.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
211
+ apply(manager, std::move(stray));
212
+ EXPECT_EQ((long)childCount(root), 0L);
213
+
214
+ manager.destroySurfaceRoot(kSurfaceId);
215
+ }
216
+ }
217
+
218
+ TEST(appkit_nested_children_are_parented_to_their_own_parent) {
219
+ @autoreleasepool {
220
+ basalt::AppKitMountingManager manager;
221
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
222
+
223
+ ShadowViewMutationList mutations;
224
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(60, 0, 0, 200, 200)));
225
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(61, 10, 10, 50, 50)));
226
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(60, 0, 0, 200, 200), 0));
227
+ // parentTag 60, not the surface root.
228
+ mutations.push_back(ShadowViewMutation::InsertMutation(60, makeView(61, 10, 10, 50, 50), 0));
229
+ apply(manager, std::move(mutations));
230
+
231
+ EXPECT_EQ((long)childCount(root), 1L);
232
+ RnAppKitView *outer = (RnAppKitView *)root.subviews[0];
233
+ EXPECT_EQ((long)outer.rnTag, 60L);
234
+ EXPECT_EQ((long)childCount(outer), 1L);
235
+ EXPECT_EQ((long)childTagAt(outer, 0), 61L);
236
+
237
+ manager.destroySurfaceRoot(kSurfaceId);
238
+ }
239
+ }
240
+
241
+ TEST(appkit_surface_root_is_reused_not_recreated) {
242
+ @autoreleasepool {
243
+ basalt::AppKitMountingManager manager;
244
+ RnAppKitView *first = manager.createSurfaceRoot(kSurfaceId);
245
+ RnAppKitView *again = manager.createSurfaceRoot(kSurfaceId);
246
+
247
+ // Fabric emits no Create for a root, so asking twice must not produce two.
248
+ EXPECT(first == again);
249
+ EXPECT(manager.getSurfaceRoot(kSurfaceId) == first);
250
+
251
+ // In Fabric a SurfaceId *is* the root's tag.
252
+ EXPECT_EQ((long)first.rnTag, (long)kSurfaceId);
253
+
254
+ manager.destroySurfaceRoot(kSurfaceId);
255
+ EXPECT(manager.getSurfaceRoot(kSurfaceId) == nil);
256
+ }
257
+ }
258
+
259
+ // Props travel the same path as the tree does, so a mutation that carries only
260
+ // new props has to land on the layer without anything else changing.
261
+ TEST(appkit_props_reach_the_layer_through_a_mutation) {
262
+ @autoreleasepool {
263
+ basalt::AppKitMountingManager manager;
264
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
265
+
266
+ ShadowViewMutationList mutations;
267
+ mutations.push_back(
268
+ ShadowViewMutation::CreateMutation(makeView(70, 0, 0, 10, 10, 0.25F, 0.5F, 0.75F)));
269
+ mutations.push_back(ShadowViewMutation::InsertMutation(
270
+ kSurfaceId, makeView(70, 0, 0, 10, 10, 0.25F, 0.5F, 0.75F), 0));
271
+ apply(manager, std::move(mutations));
272
+
273
+ RnAppKitView *view = (RnAppKitView *)root.subviews[0];
274
+ const CGFloat *components = CGColorGetComponents(view.layer.backgroundColor);
275
+ EXPECT_NEAR(components[0], 0.25, 0.01);
276
+ EXPECT_NEAR(components[1], 0.5, 0.01);
277
+ EXPECT_NEAR(components[2], 0.75, 0.01);
278
+
279
+ manager.destroySurfaceRoot(kSurfaceId);
280
+ }
281
+ }
282
+
283
+ // What this platform claims, stated as one list rather than as assertions
284
+ // added a component at a time -- this test went stale three milestones running,
285
+ // because "everything not named here is missing" is the fact worth pinning and
286
+ // a pile of individual `!hasComponent` lines is not.
287
+ TEST(appkit_has_component_matches_the_registered_descriptors) {
288
+ @autoreleasepool {
289
+ basalt::AppKitMountingManager manager;
290
+
291
+ // Paragraph is the mountable half of <Text>; Text and RawText exist only in
292
+ // the shadow tree, folded into the Paragraph's AttributedString.
293
+ // ScrollView's content child arrives as "ScrollContentView", which the
294
+ // registry rewrites to "View" before it reaches here.
295
+ const std::vector<std::string> supported = {
296
+ "View", "RootView", "Paragraph", "ScrollView", "Image", "TextInput",
297
+ // The controls, each of which is a real AppKit control rather than a
298
+ // box this host draws -- plus UnimplementedNativeView, which is what
299
+ // React Native substitutes for a component nobody registered.
300
+ "ActivityIndicatorView", "Switch", "ModalHostView", "PullToRefreshView",
301
+ "UnimplementedNativeView",
302
+ };
303
+ for (const auto &name : supported) {
304
+ if (!manager.hasComponent(name)) {
305
+ ::basalt::testing::recordFailure(std::string(__FILE__) + ":" + std::to_string(__LINE__),
306
+ "expected to support " + name);
307
+ }
308
+ }
309
+
310
+ // Claiming a component without an AppKit peer is worse than admitting the
311
+ // gap: the registry would build shadow nodes nothing can mount, and the app
312
+ // would render blank rectangles rather than fail.
313
+ // `ActivityIndicator` and `Modal` are here rather than above on purpose:
314
+ // the components React Native actually mounts are `ActivityIndicatorView`
315
+ // and `ModalHostView`, and answering to the names a reader expects would be
316
+ // answering to names nothing sends.
317
+ const std::vector<std::string> unsupported = {
318
+ "Slider", "Modal", "ActivityIndicator", "SomethingNobodyHasHeardOf",
319
+ };
320
+ for (const auto &name : unsupported) {
321
+ if (manager.hasComponent(name)) {
322
+ ::basalt::testing::recordFailure(std::string(__FILE__) + ":" + std::to_string(__LINE__),
323
+ "claims " + name + " without a peer");
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ // --- transformOrigin --------------------------------------------------------
330
+ //
331
+ // `resolveTransform` folds the origin into the matrix, and all three hosts
332
+ // call it -- so this has been "passed through but never exercised" since it
333
+ // was written. What makes it checkable without a display is that the anchor
334
+ // is a claim about a point: scaling about the top-left must leave the
335
+ // top-left corner exactly where it was.
336
+ //
337
+ // The hosts anchor a transform at the view's centre, which is what an
338
+ // untouched `transformOrigin` means, so the matrix is in centre-relative
339
+ // coordinates and the top-left corner is at (-w/2, -h/2).
340
+ //
341
+ // The same two tests are in the other two suites, with the same tags, for the
342
+ // same reason the ones above are.
343
+
344
+ namespace {
345
+
346
+ ShadowView makeTransformed(Tag tag,
347
+ float width,
348
+ float height,
349
+ facebook::react::Transform transform,
350
+ facebook::react::TransformOrigin origin = {}) {
351
+ // Fresh props rather than a copy of makeView's: BaseViewProps has a deleted
352
+ // copy constructor at React Native 0.86, which is what a current Expo app
353
+ // installs. Copying compiled against main and broke the version an app
354
+ // actually has -- found by building a real create-expo-app, not here.
355
+ ShadowView view = makeView(tag, 0.0F, 0.0F, width, height);
356
+ auto props = std::make_shared<ViewProps>();
357
+ props->transform = transform;
358
+ props->transformOrigin = origin;
359
+ view.props = props;
360
+ return view;
361
+ }
362
+
363
+ } // namespace
364
+
365
+ TEST(transform_origin_anchors_the_corner_it_names) {
366
+ @autoreleasepool {
367
+ basalt::AppKitMountingManager manager;
368
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
369
+
370
+ using facebook::react::Transform;
371
+ using facebook::react::TransformOrigin;
372
+ using facebook::react::UnitType;
373
+ using facebook::react::ValueUnit;
374
+
375
+ // Doubled about the top-left of a 100x50 view.
376
+ TransformOrigin topLeft;
377
+ topLeft.xy = {ValueUnit(0.0F, UnitType::Point), ValueUnit(0.0F, UnitType::Point)};
378
+
379
+ ShadowViewMutationList mutations;
380
+ mutations.push_back(ShadowViewMutation::CreateMutation(
381
+ makeTransformed(30, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F), topLeft)));
382
+ mutations.push_back(ShadowViewMutation::InsertMutation(
383
+ kSurfaceId,
384
+ makeTransformed(30, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F), topLeft),
385
+ 0));
386
+ apply(manager, std::move(mutations));
387
+
388
+ const auto matrix = basalt::testing::transformIn(std::string([root describeTree].UTF8String), 30);
389
+ EXPECT_EQ(matrix[0], 2.0);
390
+ EXPECT_EQ(matrix[3], 2.0);
391
+
392
+ // The corner the origin names, in the centre-relative coordinates the
393
+ // matrix is written in, has to come back to itself: (-50,-25) scaled by two
394
+ // is (-100,-50), so the translation has to put 50 and 25 back.
395
+ const double cornerX = matrix[0] * -50.0 + matrix[2] * -25.0 + matrix[4];
396
+ const double cornerY = matrix[1] * -50.0 + matrix[3] * -25.0 + matrix[5];
397
+ EXPECT_EQ(cornerX, -50.0);
398
+ EXPECT_EQ(cornerY, -25.0);
399
+
400
+ manager.destroySurfaceRoot(kSurfaceId);
401
+ }
402
+ }
403
+
404
+ // And with no origin set, the centre is the anchor -- which is the default
405
+ // every host relies on, and the reason an unset origin must not be folded in
406
+ // as (0,0).
407
+ TEST(no_transform_origin_anchors_the_centre) {
408
+ @autoreleasepool {
409
+ basalt::AppKitMountingManager manager;
410
+ RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
411
+
412
+ using facebook::react::Transform;
413
+
414
+ ShadowViewMutationList mutations;
415
+ mutations.push_back(ShadowViewMutation::CreateMutation(
416
+ makeTransformed(31, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F))));
417
+ mutations.push_back(ShadowViewMutation::InsertMutation(
418
+ kSurfaceId, makeTransformed(31, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F)), 0));
419
+ apply(manager, std::move(mutations));
420
+
421
+ const auto matrix = basalt::testing::transformIn(std::string([root describeTree].UTF8String), 31);
422
+ EXPECT_EQ(matrix[0], 2.0);
423
+ EXPECT_EQ(matrix[3], 2.0);
424
+ // No translation at all: the centre is already the anchor.
425
+ EXPECT_EQ(matrix[4], 0.0);
426
+ EXPECT_EQ(matrix[5], 0.0);
427
+
428
+ manager.destroySurfaceRoot(kSurfaceId);
429
+ }
430
+ }