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,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
|
+
}
|