basalt-win32 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/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -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 +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -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 +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -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 +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -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 +515 -0
- package/native/win32/Win32ScrollView.h +208 -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 +811 -0
- package/native/win32/Win32TextInput.h +232 -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 +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -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 +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// Tests for <ScrollView> on Windows: the offset, the clamp, the wheel's
|
|
2
|
+
// routing, and the commands.
|
|
3
|
+
//
|
|
4
|
+
// The first tests any of the three desktops has for a scroll manager. GTK's and
|
|
5
|
+
// AppKit's are exercised end to end by `scripts/compare_hosts.sh` running
|
|
6
|
+
// `js/scroll.js` on both, which is a better test in every way except that it
|
|
7
|
+
// needs two working desktops and this machine has one -- WSL2 will not start
|
|
8
|
+
// here, so nothing else can check Windows against another host. So the
|
|
9
|
+
// arithmetic gets tested directly instead.
|
|
10
|
+
//
|
|
11
|
+
// What is observable from here is narrower than what the manager does, and the
|
|
12
|
+
// two limits are worth naming rather than working around. A ShadowView built by
|
|
13
|
+
// hand has no ShadowNodeFamily, so `State::updateState` finds nothing to commit
|
|
14
|
+
// into and the unthrottled state write-back cannot be asserted on. And an
|
|
15
|
+
// EventEmitter built by hand has no EventDispatcher, so `onScroll` goes
|
|
16
|
+
// nowhere. What is left is the part that decides where the content actually
|
|
17
|
+
// sits: the clamp, the adoption, the routing and the commands.
|
|
18
|
+
|
|
19
|
+
#include "TestHarness.h"
|
|
20
|
+
|
|
21
|
+
#include "Win32MountingManager.h"
|
|
22
|
+
#include "Win32ScrollView.h"
|
|
23
|
+
// For hitTestTag: what a press would find, which is the other half of "the
|
|
24
|
+
// scroll moved" and the half a scroll manager can get silently wrong.
|
|
25
|
+
#include "Win32TouchDispatcher.h"
|
|
26
|
+
|
|
27
|
+
#include <react/renderer/components/scrollview/ScrollViewProps.h>
|
|
28
|
+
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
|
|
29
|
+
#include <react/renderer/components/scrollview/ScrollViewState.h>
|
|
30
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
31
|
+
|
|
32
|
+
#include <memory>
|
|
33
|
+
|
|
34
|
+
using basalt::Win32MountingManager;
|
|
35
|
+
using basalt::Win32ScrollViewManager;
|
|
36
|
+
using basalt::win32::RnWin32View;
|
|
37
|
+
using facebook::react::LayoutMetrics;
|
|
38
|
+
using facebook::react::MountingTransaction;
|
|
39
|
+
using facebook::react::Point;
|
|
40
|
+
using facebook::react::Rect;
|
|
41
|
+
using facebook::react::ScrollViewProps;
|
|
42
|
+
using facebook::react::ScrollViewShadowNode;
|
|
43
|
+
using facebook::react::ScrollViewState;
|
|
44
|
+
using facebook::react::ShadowNodeFamily;
|
|
45
|
+
using facebook::react::ShadowView;
|
|
46
|
+
using facebook::react::ShadowViewMutation;
|
|
47
|
+
using facebook::react::ShadowViewMutationList;
|
|
48
|
+
using facebook::react::Size;
|
|
49
|
+
using facebook::react::SurfaceId;
|
|
50
|
+
using facebook::react::Tag;
|
|
51
|
+
using facebook::react::TransactionTelemetry;
|
|
52
|
+
using facebook::react::ViewProps;
|
|
53
|
+
|
|
54
|
+
namespace {
|
|
55
|
+
|
|
56
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
57
|
+
|
|
58
|
+
ShadowView makeView(Tag tag, float x, float y, float width, float height) {
|
|
59
|
+
LayoutMetrics metrics;
|
|
60
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
61
|
+
|
|
62
|
+
ShadowView view;
|
|
63
|
+
view.componentName = "View";
|
|
64
|
+
view.surfaceId = kSurfaceId;
|
|
65
|
+
view.tag = tag;
|
|
66
|
+
view.props = std::make_shared<ViewProps>();
|
|
67
|
+
view.layoutMetrics = metrics;
|
|
68
|
+
return view;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A ScrollView whose viewport is `width`x`height` and whose content is
|
|
72
|
+
// `contentWidth`x`contentHeight` -- which is the whole of what the manager
|
|
73
|
+
// needs, because Yoga has already laid the content out at its full size by the
|
|
74
|
+
// time a mutation arrives.
|
|
75
|
+
ShadowView makeScrollView(Tag tag,
|
|
76
|
+
float x,
|
|
77
|
+
float y,
|
|
78
|
+
float width,
|
|
79
|
+
float height,
|
|
80
|
+
float contentWidth,
|
|
81
|
+
float contentHeight,
|
|
82
|
+
bool scrollEnabled = true,
|
|
83
|
+
Point initialOffset = Point{0, 0}) {
|
|
84
|
+
LayoutMetrics metrics;
|
|
85
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
86
|
+
|
|
87
|
+
auto props = std::make_shared<ScrollViewProps>();
|
|
88
|
+
props->scrollEnabled = scrollEnabled;
|
|
89
|
+
|
|
90
|
+
ScrollViewState data;
|
|
91
|
+
data.contentOffset = initialOffset;
|
|
92
|
+
data.contentBoundingRect = Rect{.origin = {.x = 0, .y = 0},
|
|
93
|
+
.size = Size{.width = contentWidth, .height = contentHeight}};
|
|
94
|
+
|
|
95
|
+
ShadowView view;
|
|
96
|
+
view.componentName = "ScrollView";
|
|
97
|
+
view.surfaceId = kSurfaceId;
|
|
98
|
+
view.tag = tag;
|
|
99
|
+
view.props = props;
|
|
100
|
+
view.layoutMetrics = metrics;
|
|
101
|
+
// An empty family: nothing can be committed back through it, which is what
|
|
102
|
+
// makes the state write-back unobservable here. See the file header.
|
|
103
|
+
view.state = std::make_shared<const ScrollViewShadowNode::ConcreteState>(
|
|
104
|
+
std::make_shared<const ScrollViewState>(data), ShadowNodeFamily::Weak{});
|
|
105
|
+
return view;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
|
|
109
|
+
manager.applyTransaction(
|
|
110
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Creates and inserts one shadow view under `parent`, in a single transaction.
|
|
114
|
+
void mount(Win32MountingManager &manager, Tag parent, const ShadowView &shadowView) {
|
|
115
|
+
ShadowViewMutationList mutations;
|
|
116
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(shadowView));
|
|
117
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(parent, shadowView, 0));
|
|
118
|
+
apply(manager, std::move(mutations));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// One notch's worth of pixels, in the direction a contentOffset grows.
|
|
122
|
+
constexpr double kNotch = Win32ScrollViewManager::kWheelStepPixels;
|
|
123
|
+
|
|
124
|
+
} // namespace
|
|
125
|
+
|
|
126
|
+
// The offset is applied to the view, which is where painting and hit testing
|
|
127
|
+
// both read it -- so this is the one assertion that covers all three.
|
|
128
|
+
TEST(win32_a_wheel_scrolls_the_view_under_it) {
|
|
129
|
+
Win32MountingManager manager;
|
|
130
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
131
|
+
root->setFrame(0, 0, 400, 300);
|
|
132
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
133
|
+
|
|
134
|
+
EXPECT(manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
|
|
135
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 5 * kNotch, 0.01);
|
|
136
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollX(), 0.0, 0.01);
|
|
137
|
+
|
|
138
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The clamp, at both ends. Content 1000 tall in a 300 viewport can scroll 700
|
|
142
|
+
// and no further, and nothing can scroll above the top.
|
|
143
|
+
TEST(win32_the_offset_is_clamped_to_the_content) {
|
|
144
|
+
Win32MountingManager manager;
|
|
145
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
146
|
+
root->setFrame(0, 0, 400, 300);
|
|
147
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
148
|
+
|
|
149
|
+
manager.scrollAt(root, 200, 150, 0, 100 * kNotch);
|
|
150
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
151
|
+
|
|
152
|
+
manager.scrollAt(root, 200, 150, 0, -100 * kNotch);
|
|
153
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
154
|
+
|
|
155
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Content that fits does not scroll at all, but the wheel is still consumed --
|
|
159
|
+
// matching AppKit, and matching what every desktop does: a list at its end must
|
|
160
|
+
// not hand the wheel to whatever is behind it.
|
|
161
|
+
TEST(win32_content_that_fits_consumes_the_wheel_and_does_not_move) {
|
|
162
|
+
Win32MountingManager manager;
|
|
163
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
164
|
+
root->setFrame(0, 0, 400, 300);
|
|
165
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 200));
|
|
166
|
+
|
|
167
|
+
EXPECT(manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
|
|
168
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
169
|
+
|
|
170
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// scrollEnabled: false does not consume, so the wheel keeps going up the tree.
|
|
174
|
+
TEST(win32_a_disabled_scroll_view_passes_the_wheel_on) {
|
|
175
|
+
Win32MountingManager manager;
|
|
176
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
177
|
+
root->setFrame(0, 0, 400, 300);
|
|
178
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000, false));
|
|
179
|
+
|
|
180
|
+
EXPECT(!manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
|
|
181
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
182
|
+
|
|
183
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// A wheel over a plain row scrolls the list containing it. GTK gets this from
|
|
187
|
+
// attaching a controller per widget and AppKit from the responder chain; here
|
|
188
|
+
// it is the parent walk in scrollAt, so it is worth an assertion.
|
|
189
|
+
TEST(win32_a_wheel_over_a_child_scrolls_its_scroll_view) {
|
|
190
|
+
Win32MountingManager manager;
|
|
191
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
192
|
+
root->setFrame(0, 0, 400, 300);
|
|
193
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
194
|
+
mount(manager, 10, makeView(11, 0, 0, 400, 80));
|
|
195
|
+
|
|
196
|
+
// The point lands on the row, not on the ScrollView.
|
|
197
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 40)), 11L);
|
|
198
|
+
|
|
199
|
+
EXPECT(manager.scrollAt(root, 200, 40, 0, 2 * kNotch));
|
|
200
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 2 * kNotch, 0.01);
|
|
201
|
+
|
|
202
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// A list inside a list scrolls the inner one. Innermost-first is the whole
|
|
206
|
+
// reason the walk starts at the hit view rather than at the root.
|
|
207
|
+
TEST(win32_a_nested_scroll_view_takes_the_wheel_first) {
|
|
208
|
+
Win32MountingManager manager;
|
|
209
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
210
|
+
root->setFrame(0, 0, 400, 300);
|
|
211
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
212
|
+
mount(manager, 10, makeScrollView(11, 0, 0, 400, 100, 400, 500));
|
|
213
|
+
|
|
214
|
+
EXPECT(manager.scrollAt(root, 200, 50, 0, 1 * kNotch));
|
|
215
|
+
EXPECT_NEAR(manager.viewForTag(11)->scrollY(), kNotch, 0.01);
|
|
216
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
217
|
+
|
|
218
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Scrolling moves what a press lands on. `hitTest` adds the scroll offset back
|
|
222
|
+
// on the way down, so this needs nothing from the scroll manager -- which is
|
|
223
|
+
// exactly the claim being checked, because a list that scrolls correctly and
|
|
224
|
+
// answers presses meant for the row above is the bug this prevents.
|
|
225
|
+
TEST(win32_hit_testing_follows_the_scroll) {
|
|
226
|
+
Win32MountingManager manager;
|
|
227
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
228
|
+
root->setFrame(0, 0, 400, 300);
|
|
229
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
230
|
+
// Two rows, the second below the fold at y=400.
|
|
231
|
+
mount(manager, 10, makeView(12, 0, 400, 400, 100));
|
|
232
|
+
mount(manager, 10, makeView(11, 0, 0, 400, 100));
|
|
233
|
+
|
|
234
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 50)), 11L);
|
|
235
|
+
|
|
236
|
+
manager.scrollAt(root, 200, 150, 0, 400);
|
|
237
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 50)), 12L);
|
|
238
|
+
|
|
239
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A ScrollView must clip whatever its props say. Its content is deliberately
|
|
243
|
+
// larger than its frame, and without this it paints over its siblings.
|
|
244
|
+
TEST(win32_a_scroll_view_clips_regardless_of_overflow) {
|
|
245
|
+
Win32MountingManager manager;
|
|
246
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
247
|
+
root->setFrame(0, 0, 400, 300);
|
|
248
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
249
|
+
|
|
250
|
+
EXPECT(manager.viewForTag(10)->clipsChildren());
|
|
251
|
+
|
|
252
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// An offset already in the state is adopted rather than reset. That is what a
|
|
256
|
+
// `contentOffset` prop looks like by the time it reaches here, and what a
|
|
257
|
+
// surface that was suspended and remounted carries.
|
|
258
|
+
TEST(win32_an_initial_offset_in_the_state_is_adopted) {
|
|
259
|
+
Win32MountingManager manager;
|
|
260
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
261
|
+
root->setFrame(0, 0, 400, 300);
|
|
262
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000, true, Point{0, 120}));
|
|
263
|
+
|
|
264
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 120.0, 0.01);
|
|
265
|
+
|
|
266
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Content can shrink under a scrolled offset -- a list whose items were
|
|
270
|
+
// filtered away. The re-clamp on every mutation is what stops the view from
|
|
271
|
+
// being left showing empty space below its own content.
|
|
272
|
+
TEST(win32_shrinking_content_pulls_the_offset_back) {
|
|
273
|
+
Win32MountingManager manager;
|
|
274
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
275
|
+
root->setFrame(0, 0, 400, 300);
|
|
276
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
277
|
+
|
|
278
|
+
manager.scrollAt(root, 200, 150, 0, 700);
|
|
279
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
280
|
+
|
|
281
|
+
ShadowViewMutationList mutations;
|
|
282
|
+
mutations.push_back(
|
|
283
|
+
ShadowViewMutation::UpdateMutation(makeScrollView(10, 0, 0, 400, 300, 400, 1000),
|
|
284
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 500),
|
|
285
|
+
kSurfaceId));
|
|
286
|
+
apply(manager, std::move(mutations));
|
|
287
|
+
|
|
288
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 200.0, 0.01);
|
|
289
|
+
|
|
290
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// scrollTo and scrollToEnd, which arrive through dispatchCommand. js/scroll.js
|
|
294
|
+
// drives itself through a ref, so this is the path that file exercises and the
|
|
295
|
+
// only one that needs no wheel.
|
|
296
|
+
TEST(win32_scroll_to_and_scroll_to_end_move_the_offset) {
|
|
297
|
+
Win32MountingManager manager;
|
|
298
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
299
|
+
root->setFrame(0, 0, 400, 300);
|
|
300
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
301
|
+
|
|
302
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 265, false));
|
|
303
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 265.0, 0.01);
|
|
304
|
+
|
|
305
|
+
manager.applyCommand(10, "scrollToEnd", folly::dynamic::array(false));
|
|
306
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
307
|
+
|
|
308
|
+
// scrollTo past the end is clamped like any other offset.
|
|
309
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 5000, false));
|
|
310
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
311
|
+
|
|
312
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// A command for a tag that is not a ScrollView, and one for a tag that is gone.
|
|
316
|
+
// Both are ordinary: a scrollTo can arrive after the surface that owned it was
|
|
317
|
+
// torn down.
|
|
318
|
+
TEST(win32_a_command_for_an_unknown_tag_is_survivable) {
|
|
319
|
+
Win32MountingManager manager;
|
|
320
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
321
|
+
root->setFrame(0, 0, 400, 300);
|
|
322
|
+
mount(manager, kSurfaceId, makeView(10, 0, 0, 100, 100));
|
|
323
|
+
|
|
324
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 100, false));
|
|
325
|
+
manager.applyCommand(999, "scrollTo", folly::dynamic::array(0, 100, false));
|
|
326
|
+
manager.applyCommand(10, "somethingElse", folly::dynamic::array());
|
|
327
|
+
|
|
328
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// The entry has to go with the view. A wheel arriving after the ScrollView was
|
|
332
|
+
// deleted must find nothing rather than a dangling RnWin32View pointer.
|
|
333
|
+
TEST(win32_deleting_a_scroll_view_forgets_its_entry) {
|
|
334
|
+
Win32MountingManager manager;
|
|
335
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
336
|
+
root->setFrame(0, 0, 400, 300);
|
|
337
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
338
|
+
EXPECT(manager.scrollAt(root, 200, 150, 0, kNotch));
|
|
339
|
+
|
|
340
|
+
ShadowViewMutationList mutations;
|
|
341
|
+
mutations.push_back(ShadowViewMutation::RemoveMutation(
|
|
342
|
+
kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000), 0));
|
|
343
|
+
mutations.push_back(
|
|
344
|
+
ShadowViewMutation::DeleteMutation(makeScrollView(10, 0, 0, 400, 300, 400, 1000)));
|
|
345
|
+
apply(manager, std::move(mutations));
|
|
346
|
+
|
|
347
|
+
EXPECT(manager.viewForTag(10) == nullptr);
|
|
348
|
+
EXPECT(!manager.scrollAt(root, 200, 150, 0, kNotch));
|
|
349
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 100, false));
|
|
350
|
+
|
|
351
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Nothing under the pointer, and no surface at all. The host reads the return
|
|
355
|
+
// value to decide whether to leave the message to DefWindowProc, so "false"
|
|
356
|
+
// here is the difference between a wheel over a non-scrolling app doing nothing
|
|
357
|
+
// and doing nothing *and* being swallowed.
|
|
358
|
+
TEST(win32_a_wheel_over_nothing_is_not_consumed) {
|
|
359
|
+
Win32MountingManager manager;
|
|
360
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
361
|
+
root->setFrame(0, 0, 400, 300);
|
|
362
|
+
mount(manager, kSurfaceId, makeView(10, 0, 0, 100, 100));
|
|
363
|
+
|
|
364
|
+
EXPECT(!manager.scrollAt(root, 50, 50, 0, kNotch));
|
|
365
|
+
EXPECT(!manager.scrollAt(root, 5000, 5000, 0, kNotch));
|
|
366
|
+
EXPECT(!manager.scrollAt(nullptr, 50, 50, 0, kNotch));
|
|
367
|
+
|
|
368
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Horizontal, which travels the same path with the other axis and is easy to
|
|
372
|
+
// leave half-wired: WM_MOUSEHWHEEL is a separate message with the opposite
|
|
373
|
+
// sign convention to WM_MOUSEWHEEL.
|
|
374
|
+
TEST(win32_a_horizontal_wheel_scrolls_the_other_axis) {
|
|
375
|
+
Win32MountingManager manager;
|
|
376
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
377
|
+
root->setFrame(0, 0, 400, 300);
|
|
378
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 1200, 300));
|
|
379
|
+
|
|
380
|
+
EXPECT(manager.scrollAt(root, 200, 150, 3 * kNotch, 0));
|
|
381
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollX(), 3 * kNotch, 0.01);
|
|
382
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
383
|
+
|
|
384
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
385
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// DirectWrite measurement, and where the glyphs land.
|
|
2
|
+
//
|
|
3
|
+
// The union of what tests/test_text.cpp asks of Pango and
|
|
4
|
+
// tests/test_appkit_text.mm asks of Core Text. Two of them are regression tests
|
|
5
|
+
// on the other platforms rather than general questions, and they are here
|
|
6
|
+
// because the bug each describes is a bug any text engine can have:
|
|
7
|
+
// `font_size_is_absolute_not_points`, and that a default ellipsize mode does
|
|
8
|
+
// not collapse a wrapping paragraph.
|
|
9
|
+
//
|
|
10
|
+
// No React Native and no window: RnWin32TextLayout knows nothing about either.
|
|
11
|
+
|
|
12
|
+
#include "TestHarness.h"
|
|
13
|
+
|
|
14
|
+
#include "RnWin32TextLayout.h"
|
|
15
|
+
#include "RnWin32View.h"
|
|
16
|
+
#include "Win32Snapshot.h"
|
|
17
|
+
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <sstream>
|
|
20
|
+
#include <string>
|
|
21
|
+
|
|
22
|
+
using basalt::win32::RnTextAlign;
|
|
23
|
+
using basalt::win32::RnTextSize;
|
|
24
|
+
using basalt::win32::RnTextStyle;
|
|
25
|
+
using basalt::win32::RnWin32TextLayout;
|
|
26
|
+
using basalt::win32::RnWin32View;
|
|
27
|
+
|
|
28
|
+
namespace {
|
|
29
|
+
|
|
30
|
+
std::shared_ptr<RnWin32TextLayout> paragraph(const std::string &text,
|
|
31
|
+
float fontSize = 16.0f,
|
|
32
|
+
int maxLines = 0) {
|
|
33
|
+
RnTextStyle style;
|
|
34
|
+
style.fontSize = fontSize;
|
|
35
|
+
return RnWin32TextLayout::create(text, style, maxLines);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constexpr const char *kLongText =
|
|
39
|
+
"The quick brown fox jumps over the lazy dog, and then does it again "
|
|
40
|
+
"because once was never going to be enough for a wrapping test.";
|
|
41
|
+
|
|
42
|
+
} // namespace
|
|
43
|
+
|
|
44
|
+
TEST(text_has_a_size) {
|
|
45
|
+
auto layout = paragraph("Hello");
|
|
46
|
+
EXPECT(layout != nullptr);
|
|
47
|
+
|
|
48
|
+
const RnTextSize size = layout->measure(-1.0f);
|
|
49
|
+
EXPECT(size.width > 0.0f);
|
|
50
|
+
EXPECT(size.height > 0.0f);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
TEST(empty_text_measures_to_zero_width) {
|
|
54
|
+
auto layout = paragraph("");
|
|
55
|
+
EXPECT(layout != nullptr);
|
|
56
|
+
|
|
57
|
+
const RnTextSize size = layout->measure(-1.0f);
|
|
58
|
+
EXPECT_NEAR(size.width, 0.0, 0.5);
|
|
59
|
+
// A height, though: an empty line still occupies one. Both other platforms
|
|
60
|
+
// report the same, and a zero-height empty <Text> would collapse a layout
|
|
61
|
+
// that was relying on it to hold a row open.
|
|
62
|
+
EXPECT(size.height > 0.0f);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
TEST(larger_font_measures_larger) {
|
|
66
|
+
const RnTextSize small = paragraph("Hello", 12.0f)->measure(-1.0f);
|
|
67
|
+
const RnTextSize large = paragraph("Hello", 36.0f)->measure(-1.0f);
|
|
68
|
+
|
|
69
|
+
EXPECT(large.width > small.width);
|
|
70
|
+
EXPECT(large.height > small.height);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
TEST(font_size_is_absolute_not_points) {
|
|
74
|
+
// The bug this guards against, in the shape Pango had it: a size interpreted
|
|
75
|
+
// as points and resolved against 96dpi renders about a third larger than
|
|
76
|
+
// asked. React Native's fontSize is in density-independent pixels and every
|
|
77
|
+
// coordinate here lives in that same space, so a hundred-unit font must
|
|
78
|
+
// produce a line in the neighbourhood of a hundred units -- not a hundred and
|
|
79
|
+
// thirty-three, and certainly not a hundred and seventy-seven.
|
|
80
|
+
//
|
|
81
|
+
// The bounds are wide because a font's natural line spacing is its own
|
|
82
|
+
// business; what they exclude is a multiply by 96/72.
|
|
83
|
+
const RnTextSize size = paragraph("Hg", 100.0f)->measure(-1.0f);
|
|
84
|
+
EXPECT(size.height >= 90.0f);
|
|
85
|
+
EXPECT(size.height < 160.0f);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
TEST(constraining_the_width_wraps_and_grows_taller) {
|
|
89
|
+
auto layout = paragraph(kLongText);
|
|
90
|
+
|
|
91
|
+
const RnTextSize wide = layout->measure(-1.0f);
|
|
92
|
+
const RnTextSize narrow = layout->measure(200.0f);
|
|
93
|
+
|
|
94
|
+
EXPECT(narrow.width <= 200.5f);
|
|
95
|
+
EXPECT(narrow.width < wide.width);
|
|
96
|
+
EXPECT(narrow.height > wide.height);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
TEST(number_of_lines_truncates) {
|
|
100
|
+
auto unlimited = paragraph(kLongText, 16.0f, 0);
|
|
101
|
+
auto limited = paragraph(kLongText, 16.0f, 2);
|
|
102
|
+
|
|
103
|
+
const RnTextSize full = unlimited->measure(200.0f);
|
|
104
|
+
const RnTextSize clipped = limited->measure(200.0f);
|
|
105
|
+
|
|
106
|
+
EXPECT(clipped.height < full.height);
|
|
107
|
+
EXPECT(clipped.height > 0.0f);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
TEST(a_line_limit_that_fits_is_not_truncated) {
|
|
111
|
+
// Five lines allowed and the paragraph needs fewer, so the limit must change
|
|
112
|
+
// nothing at all -- not the height, and not the text.
|
|
113
|
+
auto unlimited = paragraph("Short", 16.0f, 0);
|
|
114
|
+
auto limited = paragraph("Short", 16.0f, 5);
|
|
115
|
+
|
|
116
|
+
EXPECT_NEAR(limited->measure(-1.0f).height, unlimited->measure(-1.0f).height, 0.01);
|
|
117
|
+
EXPECT_NEAR(limited->measure(-1.0f).width, unlimited->measure(-1.0f).width, 0.01);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
TEST(default_ellipsize_mode_does_not_collapse_a_paragraph) {
|
|
121
|
+
// Pango's trap, stated as a test: React Native's ellipsizeMode defaults to
|
|
122
|
+
// tail, and translating that faithfully by switching ellipsization on without
|
|
123
|
+
// also setting a height collapsed every wrapping paragraph to one line. A
|
|
124
|
+
// paragraph with no line limit must wrap to as many lines as it needs.
|
|
125
|
+
auto layout = paragraph(kLongText);
|
|
126
|
+
|
|
127
|
+
const RnTextSize oneLine = paragraph("Hello")->measure(-1.0f);
|
|
128
|
+
const RnTextSize wrapped = layout->measure(200.0f);
|
|
129
|
+
|
|
130
|
+
EXPECT(wrapped.height > oneLine.height * 2.0f);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
TEST(bold_is_wider_than_regular) {
|
|
134
|
+
RnTextStyle regular;
|
|
135
|
+
regular.fontSize = 24.0f;
|
|
136
|
+
RnTextStyle bold = regular;
|
|
137
|
+
bold.bold = true;
|
|
138
|
+
|
|
139
|
+
const RnTextSize plain =
|
|
140
|
+
RnWin32TextLayout::create("Handgloves", regular, 0)->measure(-1.0f);
|
|
141
|
+
const RnTextSize heavy = RnWin32TextLayout::create("Handgloves", bold, 0)->measure(-1.0f);
|
|
142
|
+
|
|
143
|
+
EXPECT(heavy.width > plain.width);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
TEST(alignment_does_not_change_the_measured_size) {
|
|
147
|
+
RnTextStyle left;
|
|
148
|
+
left.fontSize = 16.0f;
|
|
149
|
+
RnTextStyle centred = left;
|
|
150
|
+
centred.align = RnTextAlign::Center;
|
|
151
|
+
|
|
152
|
+
const RnTextSize a = RnWin32TextLayout::create(kLongText, left, 0)->measure(300.0f);
|
|
153
|
+
const RnTextSize b = RnWin32TextLayout::create(kLongText, centred, 0)->measure(300.0f);
|
|
154
|
+
|
|
155
|
+
// Alignment moves glyphs inside the box. It does not change how big the box
|
|
156
|
+
// has to be, and a platform that reported otherwise would make Yoga lay out
|
|
157
|
+
// a centred paragraph differently from a left-aligned one.
|
|
158
|
+
EXPECT_NEAR(a.width, b.width, 0.01);
|
|
159
|
+
EXPECT_NEAR(a.height, b.height, 0.01);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
TEST(text_is_reported_in_the_tree) {
|
|
163
|
+
auto view = std::make_unique<RnWin32View>(7);
|
|
164
|
+
view->setFrame(0, 0, 200, 40);
|
|
165
|
+
view->setTextLayout(paragraph("Hi \"there\"\nyou"));
|
|
166
|
+
|
|
167
|
+
// Escaped the way the GTK side escapes it, so a quote or a newline keeps the
|
|
168
|
+
// dump to one line per view.
|
|
169
|
+
EXPECT_EQ(view->describeTree(),
|
|
170
|
+
std::string("view tag=7 frame=(0,0 200x40) text=\"Hi \\\"there\\\"\\nyou\"\n"));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
TEST(text_draws_where_the_alignment_says) {
|
|
174
|
+
// The assertion the other two hosts cannot make: not that alignment was set,
|
|
175
|
+
// but that the glyphs moved. A left-aligned line puts ink near the left edge
|
|
176
|
+
// and none near the right; a right-aligned one does the opposite.
|
|
177
|
+
const auto inkColumns = [](RnTextAlign align) {
|
|
178
|
+
RnTextStyle style;
|
|
179
|
+
style.fontSize = 24.0f;
|
|
180
|
+
style.align = align;
|
|
181
|
+
|
|
182
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
183
|
+
root->setFrame(0, 0, 300, 40);
|
|
184
|
+
root->setTextLayout(RnWin32TextLayout::create("Ill", style, 0));
|
|
185
|
+
|
|
186
|
+
const basalt::win32::RnPixels pixels = basalt::win32::renderToPixels(*root);
|
|
187
|
+
int left = 0;
|
|
188
|
+
int right = 0;
|
|
189
|
+
for (unsigned y = 0; y < pixels.height(); y++) {
|
|
190
|
+
for (unsigned x = 0; x < 60; x++) {
|
|
191
|
+
if (pixels.at(x, y).alpha > 32) {
|
|
192
|
+
left++;
|
|
193
|
+
}
|
|
194
|
+
if (pixels.at(pixels.width() - 1 - x, y).alpha > 32) {
|
|
195
|
+
right++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return std::pair<int, int>{left, right};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const auto [leftInkWhenLeft, rightInkWhenLeft] = inkColumns(RnTextAlign::Left);
|
|
203
|
+
EXPECT(leftInkWhenLeft > 0);
|
|
204
|
+
EXPECT_EQ(rightInkWhenLeft, 0);
|
|
205
|
+
|
|
206
|
+
const auto [leftInkWhenRight, rightInkWhenRight] = inkColumns(RnTextAlign::Right);
|
|
207
|
+
EXPECT_EQ(leftInkWhenRight, 0);
|
|
208
|
+
EXPECT(rightInkWhenRight > 0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
TEST(each_run_draws_in_its_own_colour) {
|
|
212
|
+
// `Hello <Text style={{color:'red'}}>world</Text>` is two runs of one
|
|
213
|
+
// paragraph, and DirectWrite carries colour as a *drawing effect* rather than
|
|
214
|
+
// as a range attribute -- so a layout built the obvious way renders the whole
|
|
215
|
+
// paragraph in the first run's colour. Both other desktops get this from
|
|
216
|
+
// their attributed string for free, which is why the gap here lasted as long
|
|
217
|
+
// as it did.
|
|
218
|
+
//
|
|
219
|
+
// Ink rather than an exact pixel: where a glyph lands depends on the font,
|
|
220
|
+
// and what is being asked is only which colours appear at all.
|
|
221
|
+
RnTextStyle red;
|
|
222
|
+
red.fontSize = 32.0f;
|
|
223
|
+
red.color[0] = 1.0f;
|
|
224
|
+
red.color[1] = 0.0f;
|
|
225
|
+
red.color[2] = 0.0f;
|
|
226
|
+
|
|
227
|
+
RnTextStyle blue = red;
|
|
228
|
+
blue.color[0] = 0.0f;
|
|
229
|
+
blue.color[2] = 1.0f;
|
|
230
|
+
|
|
231
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
232
|
+
root->setFrame(0, 0, 400, 60);
|
|
233
|
+
root->setTextLayout(RnWin32TextLayout::createFromRuns(
|
|
234
|
+
{basalt::win32::RnTextRun{"IIII", red}, basalt::win32::RnTextRun{"IIII", blue}}, 0));
|
|
235
|
+
|
|
236
|
+
const basalt::win32::RnPixels pixels = basalt::win32::renderToPixels(*root);
|
|
237
|
+
EXPECT(!pixels.empty());
|
|
238
|
+
|
|
239
|
+
int reddish = 0;
|
|
240
|
+
int bluish = 0;
|
|
241
|
+
for (unsigned y = 0; y < pixels.height(); y++) {
|
|
242
|
+
for (unsigned x = 0; x < pixels.width(); x++) {
|
|
243
|
+
const auto pixel = pixels.at(x, y);
|
|
244
|
+
if (pixel.alpha < 128) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (pixel.red > pixel.blue + 64) {
|
|
248
|
+
reddish++;
|
|
249
|
+
}
|
|
250
|
+
if (pixel.blue > pixel.red + 64) {
|
|
251
|
+
bluish++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
EXPECT(reddish > 0);
|
|
257
|
+
EXPECT(bluish > 0);
|
|
258
|
+
}
|