basalt-win32 0.0.0-stage → 0.2.0
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/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +429 -0
- package/native/tests/test_win32_accessibility.cpp +585 -0
- package/native/tests/test_win32_animated_image.cpp +351 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +750 -0
- package/native/tests/test_win32_image.cpp +370 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +511 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +757 -0
- package/native/tests/test_win32_paint.cpp +1799 -0
- package/native/tests/test_win32_scrollview.cpp +647 -0
- package/native/tests/test_win32_selection.cpp +215 -0
- package/native/tests/test_win32_text.cpp +1456 -0
- package/native/tests/test_win32_textinput.cpp +1594 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +156 -0
- package/native/win32/DirectWriteLayout.cpp +485 -0
- package/native/win32/DirectWriteLayout.h +77 -0
- package/native/win32/DirectWriteLayoutManager.cpp +154 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +744 -0
- package/native/win32/RnWin32Accessible.cpp +343 -0
- package/native/win32/RnWin32Accessible.h +130 -0
- package/native/win32/RnWin32Image.cpp +588 -0
- package/native/win32/RnWin32Image.h +139 -0
- package/native/win32/RnWin32TextLayout.cpp +1379 -0
- package/native/win32/RnWin32TextLayout.h +390 -0
- package/native/win32/RnWin32View.cpp +2956 -0
- package/native/win32/RnWin32View.h +926 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +209 -0
- package/native/win32/Win32Cursors.cpp +309 -0
- package/native/win32/Win32Cursors.h +121 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +199 -0
- package/native/win32/Win32ImageLoader.h +133 -0
- package/native/win32/Win32InputScopes.h +92 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +382 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +1282 -0
- package/native/win32/Win32MountingManager.h +243 -0
- package/native/win32/Win32Offscreen.h +53 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +718 -0
- package/native/win32/Win32ScrollView.h +256 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +1296 -0
- package/native/win32/Win32TextInput.h +341 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +577 -0
- package/native/win32/Win32TouchDispatcher.h +201 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2722 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +25 -4
- package/react-native.config.js +22 -0
- package/README.md +0 -3
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
// Tests for Win32MountingManager: does a stream of ShadowViewMutations produce
|
|
2
|
+
// the view tree it describes?
|
|
3
|
+
//
|
|
4
|
+
// Deliberately the same nine questions `tests/test_mounting.cpp` asks of GTK
|
|
5
|
+
// and `tests/test_appkit_mounting.mm` asks of AppKit, in the same order, with
|
|
6
|
+
// the same tags and frames. All three managers share their walk, so most of
|
|
7
|
+
// this is asking whether the sharing actually holds -- and the day it stops,
|
|
8
|
+
// these fail here rather than in an app.
|
|
9
|
+
//
|
|
10
|
+
// One is new and is about this platform specifically:
|
|
11
|
+
// `win32_delete_frees_the_view` exists because Windows is the first of the
|
|
12
|
+
// three where the registry's ownership is written out rather than delegated to
|
|
13
|
+
// a refcount or to ARC.
|
|
14
|
+
|
|
15
|
+
#include "TestHarness.h"
|
|
16
|
+
#include "TreeDump.h"
|
|
17
|
+
|
|
18
|
+
#include "DirectWriteLayout.h"
|
|
19
|
+
#include "Win32MountingManager.h"
|
|
20
|
+
|
|
21
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
22
|
+
#include <react/renderer/graphics/Color.h>
|
|
23
|
+
|
|
24
|
+
#include <cstdint>
|
|
25
|
+
#include <cstdio>
|
|
26
|
+
#include <sstream>
|
|
27
|
+
#include <vector>
|
|
28
|
+
|
|
29
|
+
using basalt::Win32MountingManager;
|
|
30
|
+
using basalt::win32::RnWin32View;
|
|
31
|
+
using facebook::react::ColorComponents;
|
|
32
|
+
using facebook::react::LayoutMetrics;
|
|
33
|
+
using facebook::react::MountingTransaction;
|
|
34
|
+
using facebook::react::ShadowView;
|
|
35
|
+
using facebook::react::ShadowViewMutation;
|
|
36
|
+
using facebook::react::ShadowViewMutationList;
|
|
37
|
+
using facebook::react::SurfaceId;
|
|
38
|
+
using facebook::react::Tag;
|
|
39
|
+
using facebook::react::TransactionTelemetry;
|
|
40
|
+
using facebook::react::ViewProps;
|
|
41
|
+
|
|
42
|
+
namespace {
|
|
43
|
+
|
|
44
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
45
|
+
|
|
46
|
+
ShadowView makeView(Tag tag,
|
|
47
|
+
float x,
|
|
48
|
+
float y,
|
|
49
|
+
float width,
|
|
50
|
+
float height,
|
|
51
|
+
float red = 0.0F,
|
|
52
|
+
float green = 0.0F,
|
|
53
|
+
float blue = 0.0F) {
|
|
54
|
+
auto props = std::make_shared<ViewProps>();
|
|
55
|
+
props->backgroundColor = facebook::react::colorFromComponents(
|
|
56
|
+
ColorComponents{.red = red, .green = green, .blue = blue, .alpha = 1.0F});
|
|
57
|
+
|
|
58
|
+
LayoutMetrics metrics;
|
|
59
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
60
|
+
|
|
61
|
+
ShadowView view;
|
|
62
|
+
view.componentName = "View";
|
|
63
|
+
view.surfaceId = kSurfaceId;
|
|
64
|
+
view.tag = tag;
|
|
65
|
+
view.props = props;
|
|
66
|
+
view.layoutMetrics = metrics;
|
|
67
|
+
return view;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Applies mutations synchronously. executeMount would post to the UI thread,
|
|
71
|
+
// which a test has no message loop to pump -- though with no host installed
|
|
72
|
+
// postToUiThread runs inline anyway, so this is about being explicit rather
|
|
73
|
+
// than about avoiding a hang.
|
|
74
|
+
void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
|
|
75
|
+
manager.applyTransaction(
|
|
76
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
size_t childCount(const RnWin32View *view) {
|
|
80
|
+
return view->children().size();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The tag of the nth child, or -1.
|
|
84
|
+
int32_t childTagAt(const RnWin32View *view, size_t index) {
|
|
85
|
+
if (index >= view->children().size()) {
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
return view->children()[index]->tag();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
} // namespace
|
|
92
|
+
|
|
93
|
+
TEST(win32_create_and_insert_builds_the_tree) {
|
|
94
|
+
Win32MountingManager manager;
|
|
95
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
96
|
+
|
|
97
|
+
ShadowViewMutationList mutations;
|
|
98
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(10, 0, 0, 100, 50)));
|
|
99
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(11, 0, 60, 100, 50)));
|
|
100
|
+
mutations.push_back(
|
|
101
|
+
ShadowViewMutation::InsertMutation(kSurfaceId, makeView(10, 0, 0, 100, 50), 0));
|
|
102
|
+
mutations.push_back(
|
|
103
|
+
ShadowViewMutation::InsertMutation(kSurfaceId, makeView(11, 0, 60, 100, 50), 1));
|
|
104
|
+
apply(manager, std::move(mutations));
|
|
105
|
+
|
|
106
|
+
EXPECT_EQ(childCount(root), size_t{2});
|
|
107
|
+
EXPECT_EQ(childTagAt(root, 0), 10);
|
|
108
|
+
EXPECT_EQ(childTagAt(root, 1), 11);
|
|
109
|
+
|
|
110
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
TEST(win32_insert_honours_the_index) {
|
|
114
|
+
Win32MountingManager manager;
|
|
115
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
116
|
+
|
|
117
|
+
ShadowViewMutationList first;
|
|
118
|
+
first.push_back(ShadowViewMutation::CreateMutation(makeView(20, 0, 0, 10, 10)));
|
|
119
|
+
first.push_back(ShadowViewMutation::CreateMutation(makeView(21, 0, 0, 10, 10)));
|
|
120
|
+
first.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(20, 0, 0, 10, 10), 0));
|
|
121
|
+
first.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(21, 0, 0, 10, 10), 1));
|
|
122
|
+
apply(manager, std::move(first));
|
|
123
|
+
|
|
124
|
+
// An Insert's index counts positions in the parent's *final* child list, so
|
|
125
|
+
// this lands between the two. An append-only implementation passes every
|
|
126
|
+
// other test in this file.
|
|
127
|
+
ShadowViewMutationList second;
|
|
128
|
+
second.push_back(ShadowViewMutation::CreateMutation(makeView(22, 0, 0, 10, 10)));
|
|
129
|
+
second.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(22, 0, 0, 10, 10), 1));
|
|
130
|
+
apply(manager, std::move(second));
|
|
131
|
+
|
|
132
|
+
EXPECT_EQ(childCount(root), size_t{3});
|
|
133
|
+
EXPECT_EQ(childTagAt(root, 0), 20);
|
|
134
|
+
EXPECT_EQ(childTagAt(root, 1), 22);
|
|
135
|
+
EXPECT_EQ(childTagAt(root, 2), 21);
|
|
136
|
+
|
|
137
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
TEST(win32_remove_detaches_but_does_not_destroy) {
|
|
141
|
+
Win32MountingManager manager;
|
|
142
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
143
|
+
|
|
144
|
+
ShadowViewMutationList mutations;
|
|
145
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(30, 0, 0, 10, 10)));
|
|
146
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
|
|
147
|
+
apply(manager, std::move(mutations));
|
|
148
|
+
|
|
149
|
+
ShadowViewMutationList removal;
|
|
150
|
+
removal.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
|
|
151
|
+
apply(manager, std::move(removal));
|
|
152
|
+
|
|
153
|
+
// A reparent is a Remove and an Insert with no Delete between them, so the
|
|
154
|
+
// view has to survive the gap -- and the registry is the only thing holding
|
|
155
|
+
// it. Asking the registry is the assertion: if Remove had destroyed it, this
|
|
156
|
+
// would be null or a dangling pointer.
|
|
157
|
+
EXPECT_EQ(childCount(root), size_t{0});
|
|
158
|
+
EXPECT(manager.viewForTag(30) != nullptr);
|
|
159
|
+
|
|
160
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
TEST(win32_update_changes_frame_without_reparenting) {
|
|
164
|
+
Win32MountingManager manager;
|
|
165
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
166
|
+
|
|
167
|
+
ShadowViewMutationList mutations;
|
|
168
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(40, 0, 0, 100, 50)));
|
|
169
|
+
mutations.push_back(
|
|
170
|
+
ShadowViewMutation::InsertMutation(kSurfaceId, makeView(40, 0, 0, 100, 50), 0));
|
|
171
|
+
apply(manager, std::move(mutations));
|
|
172
|
+
|
|
173
|
+
ShadowViewMutationList update;
|
|
174
|
+
update.push_back(ShadowViewMutation::UpdateMutation(
|
|
175
|
+
makeView(40, 0, 0, 100, 50), makeView(40, 5, 6, 70, 30), kSurfaceId));
|
|
176
|
+
apply(manager, std::move(update));
|
|
177
|
+
|
|
178
|
+
RnWin32View *view = manager.viewForTag(40);
|
|
179
|
+
EXPECT(view != nullptr);
|
|
180
|
+
EXPECT_EQ(childCount(root), size_t{1});
|
|
181
|
+
EXPECT_NEAR(view->frame().x, 5.0, 0.001);
|
|
182
|
+
EXPECT_NEAR(view->frame().y, 6.0, 0.001);
|
|
183
|
+
EXPECT_NEAR(view->frame().width, 70.0, 0.001);
|
|
184
|
+
EXPECT_NEAR(view->frame().height, 30.0, 0.001);
|
|
185
|
+
|
|
186
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
TEST(win32_delete_removes_the_view_from_the_registry) {
|
|
190
|
+
Win32MountingManager manager;
|
|
191
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
192
|
+
|
|
193
|
+
ShadowViewMutationList mutations;
|
|
194
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(50, 0, 0, 10, 10)));
|
|
195
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
|
|
196
|
+
apply(manager, std::move(mutations));
|
|
197
|
+
EXPECT(manager.viewForTag(50) != nullptr);
|
|
198
|
+
|
|
199
|
+
// Fabric guarantees the Remove before the Delete, and this is that order.
|
|
200
|
+
ShadowViewMutationList teardown;
|
|
201
|
+
teardown.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
|
|
202
|
+
teardown.push_back(ShadowViewMutation::DeleteMutation(makeView(50, 0, 0, 10, 10)));
|
|
203
|
+
apply(manager, std::move(teardown));
|
|
204
|
+
|
|
205
|
+
EXPECT(manager.viewForTag(50) == nullptr);
|
|
206
|
+
|
|
207
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
TEST(win32_nested_children_are_parented_to_their_own_parent) {
|
|
211
|
+
Win32MountingManager manager;
|
|
212
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
213
|
+
|
|
214
|
+
ShadowViewMutationList mutations;
|
|
215
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(60, 0, 0, 100, 100)));
|
|
216
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(61, 10, 10, 20, 20)));
|
|
217
|
+
mutations.push_back(
|
|
218
|
+
ShadowViewMutation::InsertMutation(kSurfaceId, makeView(60, 0, 0, 100, 100), 0));
|
|
219
|
+
// Parented to 60, not to the surface root.
|
|
220
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(60, makeView(61, 10, 10, 20, 20), 0));
|
|
221
|
+
apply(manager, std::move(mutations));
|
|
222
|
+
|
|
223
|
+
EXPECT_EQ(childCount(root), size_t{1});
|
|
224
|
+
RnWin32View *middle = manager.viewForTag(60);
|
|
225
|
+
EXPECT(middle != nullptr);
|
|
226
|
+
EXPECT_EQ(childCount(middle), size_t{1});
|
|
227
|
+
EXPECT_EQ(childTagAt(middle, 0), 61);
|
|
228
|
+
|
|
229
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
TEST(win32_surface_root_is_reused_not_recreated) {
|
|
233
|
+
Win32MountingManager manager;
|
|
234
|
+
RnWin32View *first = manager.createSurfaceRoot(kSurfaceId);
|
|
235
|
+
RnWin32View *second = manager.createSurfaceRoot(kSurfaceId);
|
|
236
|
+
|
|
237
|
+
// In Fabric a SurfaceId *is* the root node's tag, and Fabric emits no Create
|
|
238
|
+
// for it -- the root shadow node is the base of every diff, so it must
|
|
239
|
+
// already exist when the first transaction arrives. Asking twice must not
|
|
240
|
+
// make two.
|
|
241
|
+
EXPECT(first == second);
|
|
242
|
+
|
|
243
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
TEST(win32_a_mutation_for_an_unknown_tag_is_survivable) {
|
|
247
|
+
Win32MountingManager manager;
|
|
248
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
249
|
+
|
|
250
|
+
// What a bug upstream, or a transaction racing a surface teardown, looks like
|
|
251
|
+
// from down here. It must not be fatal.
|
|
252
|
+
ShadowViewMutationList mutations;
|
|
253
|
+
mutations.push_back(
|
|
254
|
+
ShadowViewMutation::InsertMutation(kSurfaceId, makeView(999, 0, 0, 10, 10), 0));
|
|
255
|
+
mutations.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(998, 0, 0, 10, 10), 0));
|
|
256
|
+
mutations.push_back(ShadowViewMutation::DeleteMutation(makeView(997, 0, 0, 10, 10)));
|
|
257
|
+
apply(manager, std::move(mutations));
|
|
258
|
+
|
|
259
|
+
EXPECT_EQ(childCount(root), size_t{0});
|
|
260
|
+
|
|
261
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
TEST(win32_props_reach_the_view_through_a_mutation) {
|
|
265
|
+
Win32MountingManager manager;
|
|
266
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
267
|
+
|
|
268
|
+
ShadowViewMutationList mutations;
|
|
269
|
+
mutations.push_back(
|
|
270
|
+
ShadowViewMutation::CreateMutation(makeView(70, 0, 0, 64, 32, 1.0F, 0.0F, 0.0F)));
|
|
271
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(
|
|
272
|
+
kSurfaceId, makeView(70, 0, 0, 64, 32, 1.0F, 0.0F, 0.0F), 0));
|
|
273
|
+
apply(manager, std::move(mutations));
|
|
274
|
+
|
|
275
|
+
// The whole trip: a ViewProps built here, through the walk, into the view's
|
|
276
|
+
// own state and out again as the dump the three hosts compare.
|
|
277
|
+
RnWin32View *view = manager.viewForTag(70);
|
|
278
|
+
EXPECT(view != nullptr);
|
|
279
|
+
EXPECT(view->hasBackgroundColor());
|
|
280
|
+
EXPECT_EQ(view->describeTree(), std::string("view tag=70 frame=(0,0 64x32) bg=#ff0000ff\n"));
|
|
281
|
+
|
|
282
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
TEST(win32_has_component_matches_the_registered_descriptors) {
|
|
286
|
+
Win32MountingManager manager;
|
|
287
|
+
|
|
288
|
+
// Two statements of one fact, and when they disagree the registry wins:
|
|
289
|
+
// Fabric builds shadow nodes nothing can mount and the app renders blank
|
|
290
|
+
// rectangles rather than reporting anything. See ComponentRegistryWin32.cpp.
|
|
291
|
+
EXPECT(manager.hasComponent("View"));
|
|
292
|
+
EXPECT(manager.hasComponent("RootView"));
|
|
293
|
+
EXPECT(manager.hasComponent("Paragraph"));
|
|
294
|
+
EXPECT(manager.hasComponent("Image"));
|
|
295
|
+
EXPECT(manager.hasComponent("ScrollView"));
|
|
296
|
+
EXPECT(manager.hasComponent("TextInput"));
|
|
297
|
+
EXPECT(manager.hasComponent("ActivityIndicatorView"));
|
|
298
|
+
EXPECT(manager.hasComponent("Switch"));
|
|
299
|
+
EXPECT(manager.hasComponent("ModalHostView"));
|
|
300
|
+
EXPECT(manager.hasComponent("PullToRefreshView"));
|
|
301
|
+
// What React Native substitutes for a component nobody registered. Mounting
|
|
302
|
+
// it is what turns "nothing happens" into a view the tree dump can show.
|
|
303
|
+
EXPECT(manager.hasComponent("UnimplementedNativeView"));
|
|
304
|
+
|
|
305
|
+
// Claiming a component this host cannot mount is worse than admitting the
|
|
306
|
+
// gap. `ActivityIndicator` is here rather than above on purpose: the
|
|
307
|
+
// component React Native actually mounts is `ActivityIndicatorView`, and
|
|
308
|
+
// answering to the name a reader expects would be answering to a name
|
|
309
|
+
// nothing sends.
|
|
310
|
+
EXPECT(!manager.hasComponent("Slider"));
|
|
311
|
+
EXPECT(!manager.hasComponent("ActivityIndicator"));
|
|
312
|
+
EXPECT(!manager.hasComponent("SomethingNobodyHasHeardOf"));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- transformOrigin --------------------------------------------------------
|
|
316
|
+
//
|
|
317
|
+
// `resolveTransform` folds the origin into the matrix, and all three hosts
|
|
318
|
+
// call it -- so this has been "passed through but never exercised" since it
|
|
319
|
+
// was written. What makes it checkable without a display is that the anchor
|
|
320
|
+
// is a claim about a point: scaling about the top-left must leave the
|
|
321
|
+
// top-left corner exactly where it was.
|
|
322
|
+
//
|
|
323
|
+
// The hosts anchor a transform at the view's centre, which is what an
|
|
324
|
+
// untouched `transformOrigin` means, so the matrix is in centre-relative
|
|
325
|
+
// coordinates and the top-left corner is at (-w/2, -h/2).
|
|
326
|
+
//
|
|
327
|
+
// The same two tests are in the other two suites, with the same tags, for the
|
|
328
|
+
// same reason the ones above are.
|
|
329
|
+
|
|
330
|
+
namespace {
|
|
331
|
+
|
|
332
|
+
ShadowView makeTransformed(Tag tag,
|
|
333
|
+
float width,
|
|
334
|
+
float height,
|
|
335
|
+
facebook::react::Transform transform,
|
|
336
|
+
facebook::react::TransformOrigin origin = {}) {
|
|
337
|
+
// Fresh props rather than a copy of makeView's: BaseViewProps has a deleted
|
|
338
|
+
// copy constructor at React Native 0.86, which is what a current Expo app
|
|
339
|
+
// installs. Copying compiled against main and broke the version an app
|
|
340
|
+
// actually has -- found by building a real create-expo-app, not here.
|
|
341
|
+
ShadowView view = makeView(tag, 0.0F, 0.0F, width, height);
|
|
342
|
+
auto props = std::make_shared<ViewProps>();
|
|
343
|
+
props->transform = transform;
|
|
344
|
+
props->transformOrigin = origin;
|
|
345
|
+
view.props = props;
|
|
346
|
+
return view;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
} // namespace
|
|
350
|
+
|
|
351
|
+
TEST(transform_origin_anchors_the_corner_it_names) {
|
|
352
|
+
Win32MountingManager manager;
|
|
353
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
354
|
+
|
|
355
|
+
using facebook::react::Transform;
|
|
356
|
+
using facebook::react::TransformOrigin;
|
|
357
|
+
using facebook::react::UnitType;
|
|
358
|
+
using facebook::react::ValueUnit;
|
|
359
|
+
|
|
360
|
+
// Doubled about the top-left of a 100x50 view.
|
|
361
|
+
TransformOrigin topLeft;
|
|
362
|
+
topLeft.xy = {ValueUnit(0.0F, UnitType::Point), ValueUnit(0.0F, UnitType::Point)};
|
|
363
|
+
|
|
364
|
+
ShadowViewMutationList mutations;
|
|
365
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(
|
|
366
|
+
makeTransformed(30, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F), topLeft)));
|
|
367
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(
|
|
368
|
+
kSurfaceId,
|
|
369
|
+
makeTransformed(30, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F), topLeft),
|
|
370
|
+
0));
|
|
371
|
+
apply(manager, std::move(mutations));
|
|
372
|
+
|
|
373
|
+
const auto matrix = basalt::testing::transformIn(root->describeTree(), 30);
|
|
374
|
+
EXPECT_EQ(matrix[0], 2.0);
|
|
375
|
+
EXPECT_EQ(matrix[3], 2.0);
|
|
376
|
+
|
|
377
|
+
// The corner the origin names, in the centre-relative coordinates the
|
|
378
|
+
// matrix is written in, has to come back to itself: (-50,-25) scaled by two
|
|
379
|
+
// is (-100,-50), so the translation has to put 50 and 25 back.
|
|
380
|
+
const double cornerX = matrix[0] * -50.0 + matrix[2] * -25.0 + matrix[4];
|
|
381
|
+
const double cornerY = matrix[1] * -50.0 + matrix[3] * -25.0 + matrix[5];
|
|
382
|
+
EXPECT_EQ(cornerX, -50.0);
|
|
383
|
+
EXPECT_EQ(cornerY, -25.0);
|
|
384
|
+
|
|
385
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// And with no origin set, the centre is the anchor -- which is the default
|
|
389
|
+
// every host relies on, and the reason an unset origin must not be folded in
|
|
390
|
+
// as (0,0).
|
|
391
|
+
TEST(no_transform_origin_anchors_the_centre) {
|
|
392
|
+
Win32MountingManager manager;
|
|
393
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
394
|
+
|
|
395
|
+
using facebook::react::Transform;
|
|
396
|
+
|
|
397
|
+
ShadowViewMutationList mutations;
|
|
398
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(
|
|
399
|
+
makeTransformed(31, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F))));
|
|
400
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(
|
|
401
|
+
kSurfaceId, makeTransformed(31, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F)), 0));
|
|
402
|
+
apply(manager, std::move(mutations));
|
|
403
|
+
|
|
404
|
+
const auto matrix = basalt::testing::transformIn(root->describeTree(), 31);
|
|
405
|
+
EXPECT_EQ(matrix[0], 2.0);
|
|
406
|
+
EXPECT_EQ(matrix[3], 2.0);
|
|
407
|
+
// No translation at all: the centre is already the anchor.
|
|
408
|
+
EXPECT_EQ(matrix[4], 0.0);
|
|
409
|
+
EXPECT_EQ(matrix[5], 0.0);
|
|
410
|
+
|
|
411
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// What a TextAttributes becomes on this platform: the case mapping and the
|
|
416
|
+
// decoration flags.
|
|
417
|
+
//
|
|
418
|
+
// Here rather than in tests/test_win32_text.cpp because both need React
|
|
419
|
+
// Native's TextAttributes, and that file is compiled into the toolkit-only
|
|
420
|
+
// build as well. The pixels are its half; this is the translation.
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
namespace {
|
|
423
|
+
|
|
424
|
+
facebook::react::TextAttributes withTransform(facebook::react::TextTransform transform) {
|
|
425
|
+
facebook::react::TextAttributes attributes;
|
|
426
|
+
attributes.textTransform = transform;
|
|
427
|
+
return attributes;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
std::string transformed(facebook::react::TextTransform transform, const std::string &text) {
|
|
431
|
+
return basalt::win32::transformedFragmentText(withTransform(transform), text);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
} // namespace
|
|
435
|
+
|
|
436
|
+
TEST(win32_text_transform_maps_case) {
|
|
437
|
+
using facebook::react::TextTransform;
|
|
438
|
+
|
|
439
|
+
EXPECT_EQ(transformed(TextTransform::Uppercase, "hello there"), std::string("HELLO THERE"));
|
|
440
|
+
EXPECT_EQ(transformed(TextTransform::Lowercase, "HELLO THERE"), std::string("hello there"));
|
|
441
|
+
|
|
442
|
+
// Through LCMapStringEx rather than through `std::toupper`, which is what
|
|
443
|
+
// makes this work for anything but ASCII: a byte-wise map leaves an accented
|
|
444
|
+
// letter alone and looks right in English.
|
|
445
|
+
// Written as UTF-8 escapes rather than as the letters themselves, because
|
|
446
|
+
// this file carries no encoding declaration and MSVC without /utf-8 reads a
|
|
447
|
+
// literal's bytes as the machine's code page. The string is "creme brulee"
|
|
448
|
+
// with a grave, a circumflex and an acute on it.
|
|
449
|
+
EXPECT_EQ(transformed(TextTransform::Uppercase, "cr\xc3\xa8me br\xc3\xbbl\xc3\xa9" "e"),
|
|
450
|
+
std::string("CR\xc3\x88ME BR\xc3\x9bL\xc3\x89" "E"));
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
// React Native's own capitalize rule, from RCTAttributedTextUtils.mm and
|
|
454
|
+
// copied on all three hosts: split on single spaces, lowercase the word, and
|
|
455
|
+
// uppercase its first character unless it is a digit. So "iOS" becomes "Ios",
|
|
456
|
+
// which is surprising and is what the other platforms do.
|
|
457
|
+
EXPECT_EQ(transformed(TextTransform::Capitalize, "hello iOS world"),
|
|
458
|
+
std::string("Hello Ios World"));
|
|
459
|
+
EXPECT_EQ(transformed(TextTransform::Capitalize, "3rd place"), std::string("3rd Place"));
|
|
460
|
+
// Double spaces are kept, which falls out of splitting on single ones.
|
|
461
|
+
EXPECT_EQ(transformed(TextTransform::Capitalize, "a b"), std::string("A B"));
|
|
462
|
+
|
|
463
|
+
// No transform, and the empty string, both come back untouched.
|
|
464
|
+
facebook::react::TextAttributes plain;
|
|
465
|
+
EXPECT_EQ(basalt::win32::transformedFragmentText(plain, "Left Alone"),
|
|
466
|
+
std::string("Left Alone"));
|
|
467
|
+
EXPECT_EQ(transformed(TextTransform::Uppercase, ""), std::string(""));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// **Windows keeps the German sharp s where the other two hosts expand it.**
|
|
471
|
+
// Measured on CI on 2026-10-09, which is the only Windows this repository has:
|
|
472
|
+
// `LCMapStringEx` with `LCMAP_UPPERCASE | LCMAP_LINGUISTIC_CASING` maps it to
|
|
473
|
+
// itself, so "Strasse" written with it uppercases to "STRA" + the sharp s +
|
|
474
|
+
// "E", seven bytes. GLib's `g_utf8_strup` and NSString's `uppercaseString`
|
|
475
|
+
// both answer "STRASSE", and so does every browser on Windows, because they go
|
|
476
|
+
// through ICU rather than through the locale API.
|
|
477
|
+
//
|
|
478
|
+
// Asserted rather than tolerated, so that the day this host is changed the test
|
|
479
|
+
// says so and the record gets updated with it.
|
|
480
|
+
// backlog/platform-windows.md names the call that would close the gap:
|
|
481
|
+
// `u_strToUpper`, from the ICU that Windows 10 1703 and later ship, which does
|
|
482
|
+
// Unicode's full case mapping.
|
|
483
|
+
TEST(win32_text_transform_keeps_the_sharp_s) {
|
|
484
|
+
const std::string mapped =
|
|
485
|
+
transformed(facebook::react::TextTransform::Uppercase, "Stra\xc3\x9f" "e");
|
|
486
|
+
EXPECT_EQ(mapped, std::string("STRA\xc3\x9f" "E"));
|
|
487
|
+
// Which is one byte longer than "STRASSE" is, and a byte shorter than the
|
|
488
|
+
// input: the sharp s is two bytes in UTF-8 and did not change.
|
|
489
|
+
EXPECT_EQ((int)mapped.size(), 7);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
TEST(win32_a_decoration_reaches_the_style) {
|
|
493
|
+
using facebook::react::TextDecorationLineType;
|
|
494
|
+
|
|
495
|
+
facebook::react::TextAttributes attributes;
|
|
496
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).underline);
|
|
497
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).strikethrough);
|
|
498
|
+
|
|
499
|
+
attributes.textDecorationLineType = TextDecorationLineType::Underline;
|
|
500
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).underline);
|
|
501
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).strikethrough);
|
|
502
|
+
|
|
503
|
+
attributes.textDecorationLineType = TextDecorationLineType::Strikethrough;
|
|
504
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).underline);
|
|
505
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).strikethrough);
|
|
506
|
+
|
|
507
|
+
attributes.textDecorationLineType = TextDecorationLineType::UnderlineStrikethrough;
|
|
508
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).underline);
|
|
509
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).strikethrough);
|
|
510
|
+
|
|
511
|
+
// `None` is a line the app turned off, not an absent prop, and it draws
|
|
512
|
+
// nothing -- which core/TextDecorations.h answers for all three hosts.
|
|
513
|
+
attributes.textDecorationLineType = TextDecorationLineType::None;
|
|
514
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).underline);
|
|
515
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).strikethrough);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// writingDirection, fontVariant and a fragment's opacity, as they arrive.
|
|
520
|
+
//
|
|
521
|
+
// The pixels for the first are in tests/test_win32_text.cpp; these are the
|
|
522
|
+
// translations, which need React Native's TextAttributes.
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
TEST(win32_a_writing_direction_reaches_the_style) {
|
|
525
|
+
using facebook::react::WritingDirection;
|
|
526
|
+
|
|
527
|
+
facebook::react::TextAttributes attributes;
|
|
528
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).rightToLeft);
|
|
529
|
+
|
|
530
|
+
attributes.baseWritingDirection = WritingDirection::RightToLeft;
|
|
531
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).rightToLeft);
|
|
532
|
+
|
|
533
|
+
attributes.baseWritingDirection = WritingDirection::LeftToRight;
|
|
534
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).rightToLeft);
|
|
535
|
+
|
|
536
|
+
// `natural` is left-to-right here, as it is on the other two hosts for Latin
|
|
537
|
+
// text: DirectWrite takes a direction rather than deciding one from the
|
|
538
|
+
// string, so there is no third state to pass on.
|
|
539
|
+
attributes.baseWritingDirection = WritingDirection::Natural;
|
|
540
|
+
EXPECT(!basalt::win32::buildTextStyle(attributes).rightToLeft);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// `lineBreakMode`, which React Native's JavaScript spells `lineBreakModeIOS`:
|
|
544
|
+
// whether a line that does not fit wraps or is cut, and where.
|
|
545
|
+
//
|
|
546
|
+
// The prop reached nothing here -- `RnTextStyle` had no field for it -- so a
|
|
547
|
+
// `<Text lineBreakModeIOS="tail">` wrapped onto four lines on this host and cut
|
|
548
|
+
// to one on the platform the prop is named after. What the style carries is
|
|
549
|
+
// the mode; what DirectWrite does with it is in RnWin32TextLayout.cpp, which
|
|
550
|
+
// turns wrapping off for the four that cut and trims the line.
|
|
551
|
+
TEST(win32_a_line_break_mode_reaches_the_style) {
|
|
552
|
+
using basalt::win32::RnLineBreakMode;
|
|
553
|
+
using facebook::react::LineBreakMode;
|
|
554
|
+
|
|
555
|
+
facebook::react::TextAttributes attributes;
|
|
556
|
+
// Nothing asked for wraps between words, which is React Native's default and
|
|
557
|
+
// what every ordinary paragraph depends on.
|
|
558
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Word);
|
|
559
|
+
|
|
560
|
+
attributes.lineBreakMode = LineBreakMode::Word;
|
|
561
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Word);
|
|
562
|
+
attributes.lineBreakMode = LineBreakMode::Char;
|
|
563
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Char);
|
|
564
|
+
attributes.lineBreakMode = LineBreakMode::Clip;
|
|
565
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Clip);
|
|
566
|
+
attributes.lineBreakMode = LineBreakMode::Tail;
|
|
567
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Tail);
|
|
568
|
+
|
|
569
|
+
// Head and middle arrive as themselves even though DirectWrite cuts the end
|
|
570
|
+
// for both: the style says what the app asked for, so the host can say what
|
|
571
|
+
// it did with it rather than pretending the app asked for a tail.
|
|
572
|
+
attributes.lineBreakMode = LineBreakMode::Head;
|
|
573
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Head);
|
|
574
|
+
attributes.lineBreakMode = LineBreakMode::Middle;
|
|
575
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).lineBreakMode == RnLineBreakMode::Middle);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
TEST(win32_an_alignment_keeps_start_and_end_relative) {
|
|
579
|
+
using basalt::win32::RnTextAlign;
|
|
580
|
+
using facebook::react::TextAlignment;
|
|
581
|
+
|
|
582
|
+
facebook::react::TextAttributes attributes;
|
|
583
|
+
// Nothing asked for is natural, not physical left: the difference only shows
|
|
584
|
+
// in a right-to-left paragraph, where it is the difference between the entry
|
|
585
|
+
// this closed and the bug it described.
|
|
586
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Natural);
|
|
587
|
+
|
|
588
|
+
attributes.alignment = TextAlignment::Left;
|
|
589
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Left);
|
|
590
|
+
attributes.alignment = TextAlignment::Right;
|
|
591
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Right);
|
|
592
|
+
attributes.alignment = TextAlignment::Center;
|
|
593
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Center);
|
|
594
|
+
attributes.alignment = TextAlignment::Justified;
|
|
595
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Justified);
|
|
596
|
+
attributes.alignment = TextAlignment::Natural;
|
|
597
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Natural);
|
|
598
|
+
#if BASALT_RN_MINOR >= 87
|
|
599
|
+
// `start` and `end` are resolved against the *layout* direction before they
|
|
600
|
+
// reach DirectWrite, which is what the fourth argument carries. Nothing
|
|
601
|
+
// passed is a left-to-right app, so they are the left and right edges.
|
|
602
|
+
attributes.alignment = TextAlignment::Start;
|
|
603
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Left);
|
|
604
|
+
attributes.alignment = TextAlignment::End;
|
|
605
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).align == RnTextAlign::Right);
|
|
606
|
+
|
|
607
|
+
// And the other way round on a mirrored desktop, for the same Latin text:
|
|
608
|
+
// this is the case DirectWrite's own LEADING and TRAILING get wrong, they
|
|
609
|
+
// being relative to the paragraph rather than to the app. See
|
|
610
|
+
// core/TextAlignments.h and backlog/text.md.
|
|
611
|
+
const basalt::TextDirections mirrored{.paragraph = false, .relative = true};
|
|
612
|
+
attributes.alignment = TextAlignment::Start;
|
|
613
|
+
EXPECT(basalt::win32::buildTextStyle(attributes, {}, std::nullopt, mirrored).align
|
|
614
|
+
== RnTextAlign::Right);
|
|
615
|
+
attributes.alignment = TextAlignment::End;
|
|
616
|
+
EXPECT(basalt::win32::buildTextStyle(attributes, {}, std::nullopt, mirrored).align
|
|
617
|
+
== RnTextAlign::Left);
|
|
618
|
+
#endif
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
TEST(win32_font_variants_become_opentype_tags) {
|
|
622
|
+
using facebook::react::FontVariant;
|
|
623
|
+
|
|
624
|
+
facebook::react::TextAttributes attributes;
|
|
625
|
+
EXPECT(basalt::win32::buildTextStyle(attributes).fontFeatures.empty());
|
|
626
|
+
|
|
627
|
+
attributes.fontVariant = static_cast<FontVariant>(
|
|
628
|
+
static_cast<int>(FontVariant::SmallCaps) | static_cast<int>(FontVariant::TabularNums));
|
|
629
|
+
const auto features = basalt::win32::buildTextStyle(attributes).fontFeatures;
|
|
630
|
+
EXPECT_EQ((int)features.size(), 2);
|
|
631
|
+
// In core's order rather than the bitmask's, which is what makes this
|
|
632
|
+
// comparable with the GTK host's settings string.
|
|
633
|
+
EXPECT_EQ(features[0], std::string("smcp"));
|
|
634
|
+
EXPECT_EQ(features[1], std::string("tnum"));
|
|
635
|
+
|
|
636
|
+
// A stylistic alternate is `ssNN`, which is the case a table of tags gets
|
|
637
|
+
// wrong and core builds from the number instead.
|
|
638
|
+
attributes.fontVariant = FontVariant::StylisticSeven;
|
|
639
|
+
const auto alternate = basalt::win32::buildTextStyle(attributes).fontFeatures;
|
|
640
|
+
EXPECT_EQ((int)alternate.size(), 1);
|
|
641
|
+
EXPECT_EQ(alternate[0], std::string("ss07"));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
TEST(win32_a_fragment_opacity_multiplies_the_alpha) {
|
|
645
|
+
facebook::react::TextAttributes attributes;
|
|
646
|
+
attributes.foregroundColor = facebook::react::colorFromComponents(
|
|
647
|
+
facebook::react::ColorComponents{1.0F, 0.0F, 0.0F, 1.0F});
|
|
648
|
+
|
|
649
|
+
// No opacity asked for: opaque, which is also what an unset colour is.
|
|
650
|
+
EXPECT_NEAR(basalt::win32::buildTextStyle(attributes).color[3], 1.0f, 0.01f);
|
|
651
|
+
|
|
652
|
+
attributes.opacity = 0.5;
|
|
653
|
+
const auto style = basalt::win32::buildTextStyle(attributes);
|
|
654
|
+
EXPECT_NEAR(style.color[3], 0.5f, 0.01f);
|
|
655
|
+
// And the channels are untouched: opacity is alpha, not a tint.
|
|
656
|
+
EXPECT_NEAR(style.color[0], 1.0f, 0.01f);
|
|
657
|
+
EXPECT_NEAR(style.color[1], 0.0f, 0.01f);
|
|
658
|
+
|
|
659
|
+
// Out of range is clamped, a stylesheet being able to ask for either.
|
|
660
|
+
attributes.opacity = 2.0;
|
|
661
|
+
EXPECT_NEAR(basalt::win32::buildTextStyle(attributes).color[3], 1.0f, 0.01f);
|
|
662
|
+
attributes.opacity = -1.0;
|
|
663
|
+
EXPECT_NEAR(basalt::win32::buildTextStyle(attributes).color[3], 0.0f, 0.01f);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// --- adjustsFontSizeToFit ----------------------------------------------------
|
|
667
|
+
//
|
|
668
|
+
// The search is core's and tested there; what these ask is the half that is
|
|
669
|
+
// DirectWrite's -- that the ratio reaches the font sizes, that the fitted
|
|
670
|
+
// paragraph actually fits, and that a paragraph which never asked is untouched.
|
|
671
|
+
namespace {
|
|
672
|
+
|
|
673
|
+
facebook::react::AttributedString fittingLabel(const std::string &text, float fontSize) {
|
|
674
|
+
facebook::react::TextAttributes attributes;
|
|
675
|
+
attributes.fontSize = fontSize;
|
|
676
|
+
|
|
677
|
+
facebook::react::AttributedString::Fragment fragment;
|
|
678
|
+
fragment.string = text;
|
|
679
|
+
fragment.textAttributes = attributes;
|
|
680
|
+
|
|
681
|
+
facebook::react::AttributedString string;
|
|
682
|
+
string.appendFragment(std::move(fragment));
|
|
683
|
+
return string;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
} // namespace
|
|
687
|
+
|
|
688
|
+
TEST(win32_adjusts_font_size_to_fit_shrinks_until_it_fits) {
|
|
689
|
+
const auto text = fittingLabel("Delete all of the messages", 32.0f);
|
|
690
|
+
|
|
691
|
+
facebook::react::ParagraphAttributes fitting;
|
|
692
|
+
fitting.adjustsFontSizeToFit = true;
|
|
693
|
+
const basalt::FontFit fit = basalt::win32::textFitScale(text, fitting, 200.0f, 40.0f);
|
|
694
|
+
EXPECT(fit.scales());
|
|
695
|
+
EXPECT(fit.ratio < 1.0);
|
|
696
|
+
|
|
697
|
+
// And the fitted paragraph fits, measured through the same builder the host
|
|
698
|
+
// paints with.
|
|
699
|
+
const auto layout = basalt::win32::buildTextLayout(text, fitting, fit);
|
|
700
|
+
EXPECT(layout != nullptr);
|
|
701
|
+
if (layout == nullptr) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const basalt::win32::RnTextSize size = layout->measure(200.0f);
|
|
705
|
+
EXPECT(size.width <= 200.0f);
|
|
706
|
+
EXPECT(size.height <= 40.0f);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
TEST(win32_a_paragraph_that_did_not_ask_is_not_shrunk) {
|
|
710
|
+
const auto text = fittingLabel("Delete all of the messages", 32.0f);
|
|
711
|
+
facebook::react::ParagraphAttributes plain;
|
|
712
|
+
EXPECT(!basalt::win32::textFitScale(text, plain, 200.0f, 40.0f).scales());
|
|
713
|
+
|
|
714
|
+
const auto layout = basalt::win32::buildTextLayout(text, plain);
|
|
715
|
+
EXPECT(layout != nullptr);
|
|
716
|
+
if (layout == nullptr) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// Wrapped and taller than the box, rather than shrunk into it.
|
|
720
|
+
EXPECT(layout->measure(200.0f).height > 40.0f);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
TEST(win32_a_paragraph_that_already_fits_keeps_its_size) {
|
|
724
|
+
facebook::react::ParagraphAttributes fitting;
|
|
725
|
+
fitting.adjustsFontSizeToFit = true;
|
|
726
|
+
EXPECT(
|
|
727
|
+
!basalt::win32::textFitScale(fittingLabel("Hi", 16.0f), fitting, 300.0f, 100.0f).scales());
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
TEST(win32_the_minimum_font_size_stops_the_shrinking) {
|
|
731
|
+
facebook::react::ParagraphAttributes fitting;
|
|
732
|
+
fitting.adjustsFontSizeToFit = true;
|
|
733
|
+
fitting.minimumFontSize = 24.0f;
|
|
734
|
+
const basalt::FontFit fit = basalt::win32::textFitScale(
|
|
735
|
+
fittingLabel("Delete all of the messages", 32.0f), fitting, 60.0f, 30.0f);
|
|
736
|
+
EXPECT_NEAR(fit.apply(32.0), 24.0, 0.0001);
|
|
737
|
+
|
|
738
|
+
// Which is the size the style ends up with, this being where the ratio is
|
|
739
|
+
// applied.
|
|
740
|
+
facebook::react::TextAttributes attributes;
|
|
741
|
+
attributes.fontSize = 32.0;
|
|
742
|
+
EXPECT_NEAR(basalt::win32::buildTextStyle(attributes, fit).fontSize, 24.0f, 0.01f);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
TEST(win32_a_line_limit_does_not_hide_the_overflow_from_the_search) {
|
|
746
|
+
// `numberOfLines` with `adjustsFontSizeToFit` is the pairing a button label
|
|
747
|
+
// is written with, and the reason the search measures the *untruncated*
|
|
748
|
+
// paragraph: a one-line limit makes a layout that fits any box by throwing
|
|
749
|
+
// text away.
|
|
750
|
+
facebook::react::ParagraphAttributes oneLine;
|
|
751
|
+
oneLine.adjustsFontSizeToFit = true;
|
|
752
|
+
oneLine.maximumNumberOfLines = 1;
|
|
753
|
+
const basalt::FontFit fit = basalt::win32::textFitScale(
|
|
754
|
+
fittingLabel("Delete all of the messages", 32.0f), oneLine, 200.0f, 40.0f);
|
|
755
|
+
EXPECT(fit.scales());
|
|
756
|
+
EXPECT(fit.ratio < 1.0);
|
|
757
|
+
}
|