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,351 @@
|
|
|
1
|
+
// An animated GIF: the frames WIC decodes, composited, and paced by the clamp
|
|
2
|
+
// all three hosts share.
|
|
3
|
+
//
|
|
4
|
+
// Against real pixels rather than against which frame the view thinks it is on,
|
|
5
|
+
// for the reason tests/test_win32_paint.cpp exists: a view that advanced its
|
|
6
|
+
// index correctly and kept painting the first bitmap would pass either way.
|
|
7
|
+
//
|
|
8
|
+
// The file is a real GIF, four pixels square and ninety-five bytes, inline as
|
|
9
|
+
// base64 so this needs no asset on disk. It is the same fixture the GTK suite
|
|
10
|
+
// uses, which is what makes the two suites comparable. Written with ImageMagick:
|
|
11
|
+
//
|
|
12
|
+
// magick -loop 0 -delay 8 -size 4x4 xc:'#ff0000' \
|
|
13
|
+
// -delay 4 -size 4x4 xc:'#0000ff' anim.gif
|
|
14
|
+
//
|
|
15
|
+
// so frame one is red for 80ms and frame two is blue for 40ms, forever. The
|
|
16
|
+
// delays differ on purpose: equal ones hide an engine that uses the first
|
|
17
|
+
// frame's delay for all of them.
|
|
18
|
+
//
|
|
19
|
+
// Where this host differs from GTK is the ownership. gdk-pixbuf owns the frames
|
|
20
|
+
// and the clock behind `GdkPixbufAnimationIter`, so that host asks it for a
|
|
21
|
+
// frame; WIC hands over indexed frames and raw metadata and nothing else, so
|
|
22
|
+
// the compositing, the delays and the loop count are all this host's, and each
|
|
23
|
+
// of them is checked here.
|
|
24
|
+
|
|
25
|
+
#include "TestHarness.h"
|
|
26
|
+
|
|
27
|
+
#include "ImageBytes.h"
|
|
28
|
+
#include "RnWin32Image.h"
|
|
29
|
+
#include "RnWin32View.h"
|
|
30
|
+
#include "Win32ImageLoader.h"
|
|
31
|
+
#include "Win32Snapshot.h"
|
|
32
|
+
|
|
33
|
+
#include <cstdint>
|
|
34
|
+
#include <memory>
|
|
35
|
+
#include <sstream>
|
|
36
|
+
#include <string>
|
|
37
|
+
#include <vector>
|
|
38
|
+
|
|
39
|
+
using basalt::win32::RnImageFit;
|
|
40
|
+
using basalt::win32::RnPixel;
|
|
41
|
+
using basalt::win32::RnPixels;
|
|
42
|
+
using basalt::win32::RnWin32Image;
|
|
43
|
+
using basalt::win32::RnWin32ImageFrames;
|
|
44
|
+
using basalt::win32::RnWin32View;
|
|
45
|
+
using basalt::win32::Win32ImageLoader;
|
|
46
|
+
|
|
47
|
+
namespace {
|
|
48
|
+
|
|
49
|
+
const char *const kAnimatedGifBase64 =
|
|
50
|
+
"R0lGODlhBAAEAPAAAP8AAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQACAAAACwAAAAABAAEAAAC"
|
|
51
|
+
"BISPCQUAIfkEAAQAAAAsAAAAAAQABACAAAD/AAAAAgSEjwkFADs=";
|
|
52
|
+
|
|
53
|
+
// The same picture with no delays at all, which is what a great many GIFs carry
|
|
54
|
+
// and what the shared clamp exists for:
|
|
55
|
+
//
|
|
56
|
+
// magick -loop 0 -size 4x4 xc:'#ff0000' -size 4x4 xc:'#0000ff' anim.gif
|
|
57
|
+
const char *const kInstantGifBase64 =
|
|
58
|
+
"R0lGODlhBAAEAPAAAP8AAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQAAAAAACwAAAAABAAEAAAC"
|
|
59
|
+
"BISPCQUAIfkEAAAAAAAsAAAAAAQABACAAAD/AAAAAgSEjwkFADs=";
|
|
60
|
+
|
|
61
|
+
// A GIF with no NETSCAPE2.0 extension at all, which is what "play once" looks
|
|
62
|
+
// like in the format: `magick -loop 1 ...` writes no application extension
|
|
63
|
+
// rather than a count of one.
|
|
64
|
+
const char *const kOnceGifBase64 =
|
|
65
|
+
"R0lGODlhBAAEAPAAAP8AAAAAACH5BAAIAAAALAAAAAAEAAQAAAIEhI8JBQAh+QQABAAAACwAAAAA"
|
|
66
|
+
"BAAEAIAAAP8AAAACBISPCQUAOw==";
|
|
67
|
+
|
|
68
|
+
// The still PNG the loader tests use: one frame, no delays.
|
|
69
|
+
const char *const kStillPngBase64 =
|
|
70
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAIAAAB7QOjdAAAADUlEQVR42mP4zwAE/wEHAAH/PX2MSQAAAABJRU5ErkJggg==";
|
|
71
|
+
|
|
72
|
+
std::string dataUri(const char *mediaType, const char *base64) {
|
|
73
|
+
return std::string("data:") + mediaType + ";base64," + base64;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The bytes inside one of those strings, through the same `data:` reader the
|
|
77
|
+
// loader fetches with, so the test needs no base64 decoder of its own.
|
|
78
|
+
std::string decode(const char *mediaType, const char *base64) {
|
|
79
|
+
std::string bytes;
|
|
80
|
+
std::string error;
|
|
81
|
+
basalt::fetchImageBytes(dataUri(mediaType, base64), &bytes, &error);
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
RnWin32ImageFrames framesOf(const char *base64) {
|
|
86
|
+
const std::string bytes = decode("image/gif", base64);
|
|
87
|
+
if (bytes.empty()) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
return RnWin32Image::framesFromEncodedBytes(
|
|
91
|
+
reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A view holding an animation, rendered so the colour of a frame is readable.
|
|
95
|
+
// The box is twenty square and the image four, so the default fit fills it and
|
|
96
|
+
// the middle pixel is the image's colour.
|
|
97
|
+
class View {
|
|
98
|
+
public:
|
|
99
|
+
explicit View(const RnWin32ImageFrames &frames) : view_(std::make_unique<RnWin32View>(1)) {
|
|
100
|
+
view_->setFrame(0.0f, 0.0f, 20.0f, 20.0f);
|
|
101
|
+
// The order the mounting manager uses: the still picture, then the frames.
|
|
102
|
+
view_->setImage(frames.frames.empty() ? nullptr : frames.frames.front(), RnImageFit::Cover);
|
|
103
|
+
view_->setImageFrames(frames.frames, frames.delaysMs, frames.loopCount);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
RnWin32View *get() const { return view_.get(); }
|
|
107
|
+
|
|
108
|
+
RnPixel centre() const {
|
|
109
|
+
const RnPixels pixels = basalt::win32::renderToPixels(*view_);
|
|
110
|
+
return pixels.at(10, 10);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private:
|
|
114
|
+
std::unique_ptr<RnWin32View> view_;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
} // namespace
|
|
118
|
+
|
|
119
|
+
// What WIC hands over, which is the half gdk-pixbuf does for the other host:
|
|
120
|
+
// two frames, the file's own delays, and "forever".
|
|
121
|
+
TEST(win32_animated_image_decodes_every_frame) {
|
|
122
|
+
const RnWin32ImageFrames frames = framesOf(kAnimatedGifBase64);
|
|
123
|
+
EXPECT(frames.animated());
|
|
124
|
+
if (!frames.animated()) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
EXPECT_EQ(frames.frames.size(), 2u);
|
|
128
|
+
EXPECT_EQ(frames.delaysMs.size(), 2u);
|
|
129
|
+
// Hundredths of a second in the file, milliseconds here.
|
|
130
|
+
EXPECT_EQ(frames.delaysMs[0], 80u);
|
|
131
|
+
EXPECT_EQ(frames.delaysMs[1], 40u);
|
|
132
|
+
// NETSCAPE2.0 with a count of zero, which is what a looping GIF writes.
|
|
133
|
+
EXPECT_EQ(frames.loopCount, 0u);
|
|
134
|
+
// Four pixels square: the logical screen, which is what every frame is
|
|
135
|
+
// composited onto.
|
|
136
|
+
EXPECT_EQ(frames.frames[0]->width(), 4u);
|
|
137
|
+
EXPECT_EQ(frames.frames[0]->height(), 4u);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
TEST(win32_animated_image_the_frame_follows_the_time_it_is_advanced_by) {
|
|
141
|
+
const RnWin32ImageFrames frames = framesOf(kAnimatedGifBase64);
|
|
142
|
+
EXPECT(frames.animated());
|
|
143
|
+
if (!frames.animated()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
View view(frames);
|
|
148
|
+
|
|
149
|
+
// Frame one, red, which is also what a still <Image> of this file paints.
|
|
150
|
+
EXPECT(view.centre().red > 200);
|
|
151
|
+
EXPECT(view.centre().blue < 60);
|
|
152
|
+
|
|
153
|
+
// Still frame one at 79ms: the delay is the file's and not a guess.
|
|
154
|
+
view.get()->advanceImageAnimation(79.0);
|
|
155
|
+
EXPECT(view.centre().red > 200);
|
|
156
|
+
|
|
157
|
+
// Frame two, blue, one millisecond later.
|
|
158
|
+
view.get()->advanceImageAnimation(1.0);
|
|
159
|
+
EXPECT(view.centre().blue > 200);
|
|
160
|
+
EXPECT(view.centre().red < 60);
|
|
161
|
+
|
|
162
|
+
// And round again after the second frame's own, shorter delay: 40ms, not
|
|
163
|
+
// another 80.
|
|
164
|
+
view.get()->advanceImageAnimation(40.0);
|
|
165
|
+
EXPECT(view.centre().red > 200);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
TEST(win32_animated_image_says_how_long_until_the_next_frame) {
|
|
169
|
+
const RnWin32ImageFrames frames = framesOf(kAnimatedGifBase64);
|
|
170
|
+
EXPECT(frames.animated());
|
|
171
|
+
if (!frames.animated()) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
View view(frames);
|
|
176
|
+
// What the host's timer stops on: the whole 80ms at the start of frame one,
|
|
177
|
+
// and what is left of it in the middle.
|
|
178
|
+
EXPECT_NEAR(view.get()->advanceImageAnimation(0.0), 80.0, 0.01);
|
|
179
|
+
EXPECT_NEAR(view.get()->advanceImageAnimation(30.0), 50.0, 0.01);
|
|
180
|
+
// Into frame two, which is 40ms long.
|
|
181
|
+
EXPECT_NEAR(view.get()->advanceImageAnimation(50.0), 40.0, 0.01);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// A GIF that asks for no delay at all is paced at a tenth of a second.
|
|
185
|
+
//
|
|
186
|
+
// **The clamp is load-bearing on this host, and the first assertion is what
|
|
187
|
+
// says so rather than a claim about WIC.** `/grctlext/Delay` is the file's own
|
|
188
|
+
// number, so if WIC hands over the zero this reads zero and the hundred below
|
|
189
|
+
// is core/ImageAnimation.h's doing; if some future WIC substituted a default,
|
|
190
|
+
// this line would fail and say so. gdk-pixbuf does substitute, which is why the
|
|
191
|
+
// equivalent GTK test records that it does not discriminate the clamp there.
|
|
192
|
+
TEST(win32_animated_image_a_delay_of_zero_is_paced_like_a_browsers) {
|
|
193
|
+
const RnWin32ImageFrames frames = framesOf(kInstantGifBase64);
|
|
194
|
+
EXPECT(frames.animated());
|
|
195
|
+
if (!frames.animated()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
EXPECT_EQ(frames.delaysMs[0], 0u);
|
|
199
|
+
|
|
200
|
+
View view(frames);
|
|
201
|
+
EXPECT_NEAR(view.get()->advanceImageAnimation(0.0), 100.0, 0.01);
|
|
202
|
+
|
|
203
|
+
// Nothing at 99ms, and the next frame at 100.
|
|
204
|
+
view.get()->advanceImageAnimation(99.0);
|
|
205
|
+
EXPECT(view.centre().red > 200);
|
|
206
|
+
view.get()->advanceImageAnimation(1.0);
|
|
207
|
+
EXPECT(view.centre().blue > 200);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
TEST(win32_animated_image_is_reported_in_the_tree) {
|
|
211
|
+
const RnWin32ImageFrames frames = framesOf(kAnimatedGifBase64);
|
|
212
|
+
EXPECT(frames.animated());
|
|
213
|
+
if (!frames.animated()) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
View view(frames);
|
|
218
|
+
EXPECT(view.get()->describeTree().find("animated=1") != std::string::npos);
|
|
219
|
+
EXPECT(view.get()->hasAnimatedImage());
|
|
220
|
+
|
|
221
|
+
// A parent reports a child's animation, which is what the host's timer asks
|
|
222
|
+
// of the root: an <Image> is never the root of a surface.
|
|
223
|
+
auto parent = std::make_unique<RnWin32View>(2);
|
|
224
|
+
parent->setFrame(0.0f, 0.0f, 20.0f, 20.0f);
|
|
225
|
+
parent->insertChild(view.get(), 0);
|
|
226
|
+
EXPECT(parent->hasAnimatedImage());
|
|
227
|
+
parent->removeChild(view.get());
|
|
228
|
+
EXPECT(!parent->hasAnimatedImage());
|
|
229
|
+
parent->insertChild(view.get(), 0);
|
|
230
|
+
|
|
231
|
+
// Cleared with the frames, so an <Image> whose source becomes a still one
|
|
232
|
+
// does not keep claiming to move and does not keep the timer awake.
|
|
233
|
+
view.get()->setImageFrames({}, {}, 1u);
|
|
234
|
+
EXPECT(!view.get()->hasAnimatedImage());
|
|
235
|
+
EXPECT(!parent->hasAnimatedImage());
|
|
236
|
+
EXPECT(view.get()->describeTree().find("animated=1") == std::string::npos);
|
|
237
|
+
// The picture stays: clearing the animation is not clearing the image.
|
|
238
|
+
EXPECT(view.get()->describeTree().find("texture=4x4") != std::string::npos);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A re-mount must not restart the animation, which is the bug this shape of
|
|
242
|
+
// mounting manager invites: every mutation that touches an <Image> re-applies
|
|
243
|
+
// its props, a layout-only one included, so a reaction GIF would stutter back
|
|
244
|
+
// to the top on every resize.
|
|
245
|
+
TEST(win32_animated_image_survives_the_props_being_reapplied) {
|
|
246
|
+
const RnWin32ImageFrames frames = framesOf(kAnimatedGifBase64);
|
|
247
|
+
EXPECT(frames.animated());
|
|
248
|
+
if (!frames.animated()) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
View view(frames);
|
|
253
|
+
view.get()->advanceImageAnimation(100.0);
|
|
254
|
+
EXPECT(view.centre().blue > 200);
|
|
255
|
+
|
|
256
|
+
// What the mounting manager does on a mutation: the same frames again, from
|
|
257
|
+
// the loader's cache, and the still picture that is now the current frame.
|
|
258
|
+
view.get()->setImage(view.get()->image(), RnImageFit::Cover);
|
|
259
|
+
view.get()->setImageFrames(frames.frames, frames.delaysMs, frames.loopCount);
|
|
260
|
+
EXPECT(view.centre().blue > 200);
|
|
261
|
+
|
|
262
|
+
// A different animation does start over, which is the same check from the
|
|
263
|
+
// other side: the frames are what tell them apart.
|
|
264
|
+
const RnWin32ImageFrames other = framesOf(kAnimatedGifBase64);
|
|
265
|
+
view.get()->setImageFrames(other.frames, other.delaysMs, other.loopCount);
|
|
266
|
+
EXPECT(view.centre().red > 200);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
TEST(win32_animated_image_a_gif_that_does_not_loop_plays_once) {
|
|
270
|
+
const RnWin32ImageFrames frames = framesOf(kOnceGifBase64);
|
|
271
|
+
EXPECT(frames.animated());
|
|
272
|
+
if (!frames.animated()) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// No application extension means one pass, which is also what ImageIO
|
|
276
|
+
// answers for these bytes on the AppKit host.
|
|
277
|
+
EXPECT_EQ(frames.loopCount, 1u);
|
|
278
|
+
|
|
279
|
+
View view(frames);
|
|
280
|
+
// Halfway through the first frame's 80ms, still running.
|
|
281
|
+
EXPECT(view.get()->advanceImageAnimation(60.0) > 0.0);
|
|
282
|
+
// One pass is 120ms, after which nothing more is due: the host's timer stops
|
|
283
|
+
// on exactly this, which is what keeps a played-out GIF from waking the
|
|
284
|
+
// process for the rest of the session.
|
|
285
|
+
EXPECT_NEAR(view.get()->advanceImageAnimation(60.0), 0.0, 0.01);
|
|
286
|
+
// And the last frame stays, rather than snapping back to the first.
|
|
287
|
+
EXPECT(view.centre().blue > 200);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
TEST(win32_animated_image_a_still_file_is_not_animated) {
|
|
291
|
+
const std::string bytes = decode("image/png", kStillPngBase64);
|
|
292
|
+
EXPECT(!bytes.empty());
|
|
293
|
+
const RnWin32ImageFrames frames = RnWin32Image::framesFromEncodedBytes(
|
|
294
|
+
reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size());
|
|
295
|
+
// Empty rather than one frame: a still image has no animation, and the view
|
|
296
|
+
// tells them apart by the count.
|
|
297
|
+
EXPECT(!frames.animated());
|
|
298
|
+
EXPECT(frames.frames.empty());
|
|
299
|
+
|
|
300
|
+
// The same bytes still decode as a picture, which is the thing a still
|
|
301
|
+
// <Image> paints: the two decodes are independent.
|
|
302
|
+
EXPECT(RnWin32Image::fromEncodedBytes(
|
|
303
|
+
reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size()) != nullptr);
|
|
304
|
+
|
|
305
|
+
View view(frames);
|
|
306
|
+
EXPECT(!view.get()->hasAnimatedImage());
|
|
307
|
+
EXPECT_NEAR(view.get()->advanceImageAnimation(500.0), 0.0, 0.01);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// And the loader's half: that an animated file is recognised as one, that the
|
|
311
|
+
// frames are the same objects on a second load -- which is what lets a view
|
|
312
|
+
// tell a re-mount from a new animation -- and that a still file has none.
|
|
313
|
+
TEST(win32_animated_image_the_loader_keeps_the_frames) {
|
|
314
|
+
Win32ImageLoader loader;
|
|
315
|
+
const std::string uri = dataUri("image/gif", kAnimatedGifBase64);
|
|
316
|
+
|
|
317
|
+
// No UI thread is installed here, so the load is synchronous; see the top of
|
|
318
|
+
// tests/test_win32_imageloader.cpp.
|
|
319
|
+
bool done = false;
|
|
320
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &error) {
|
|
321
|
+
EXPECT(image != nullptr);
|
|
322
|
+
EXPECT(error.empty());
|
|
323
|
+
done = true;
|
|
324
|
+
});
|
|
325
|
+
EXPECT(done);
|
|
326
|
+
|
|
327
|
+
const RnWin32ImageFrames frames = loader.animation(uri);
|
|
328
|
+
EXPECT(frames.animated());
|
|
329
|
+
EXPECT_EQ(frames.frames.size(), 2u);
|
|
330
|
+
|
|
331
|
+
bool again = false;
|
|
332
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image>, const std::string &) { again = true; });
|
|
333
|
+
EXPECT(again);
|
|
334
|
+
EXPECT(loader.animation(uri).frames == frames.frames);
|
|
335
|
+
|
|
336
|
+
// A still image has none, rather than a one-frame animation nobody can use.
|
|
337
|
+
const std::string png = dataUri("image/png", kStillPngBase64);
|
|
338
|
+
bool still = false;
|
|
339
|
+
loader.load(png, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
340
|
+
EXPECT(image != nullptr);
|
|
341
|
+
still = true;
|
|
342
|
+
});
|
|
343
|
+
EXPECT(still);
|
|
344
|
+
EXPECT(!loader.animation(png).animated());
|
|
345
|
+
EXPECT(loader.animation(png).frames.empty());
|
|
346
|
+
|
|
347
|
+
// And a URI nobody asked for: an empty result rather than a lookup that
|
|
348
|
+
// inserts one, which is what the mounting manager leans on when it applies
|
|
349
|
+
// the animation of an <Image> that has none.
|
|
350
|
+
EXPECT(loader.animation("data:image/gif;base64,nonsense").frames.empty());
|
|
351
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Tests for the clipboard half of core/PlatformServices.h on Windows.
|
|
2
|
+
//
|
|
3
|
+
// These use the real clipboard, because the thing being tested is how it
|
|
4
|
+
// behaves when somebody else holds it -- and the only somebody that can be
|
|
5
|
+
// arranged from a test is a second thread in this process, which OpenClipboard
|
|
6
|
+
// treats exactly like another program. What the clipboard held before is put
|
|
7
|
+
// back at the end, as text; anything richer than text is lost, the same as
|
|
8
|
+
// e2e/modules.js running.
|
|
9
|
+
|
|
10
|
+
#include "TestHarness.h"
|
|
11
|
+
|
|
12
|
+
#include "PlatformServices.h"
|
|
13
|
+
|
|
14
|
+
#include <windows.h>
|
|
15
|
+
|
|
16
|
+
#include <atomic>
|
|
17
|
+
#include <chrono>
|
|
18
|
+
#include <string>
|
|
19
|
+
#include <thread>
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
|
|
23
|
+
// Opens the clipboard on another thread and keeps it for `holdMs`, which is
|
|
24
|
+
// what a clipboard listener does right after every write. Returns once the
|
|
25
|
+
// clipboard is actually held, so the caller's attempt is guaranteed to collide.
|
|
26
|
+
class ClipboardHolder {
|
|
27
|
+
public:
|
|
28
|
+
explicit ClipboardHolder(int holdMs) {
|
|
29
|
+
thread_ = std::thread([this, holdMs] {
|
|
30
|
+
// Opened against a window, because only that excludes anyone. A holder
|
|
31
|
+
// that passes no window -- as the code under test does -- blocks nobody,
|
|
32
|
+
// not even another process, and the first version of this test held the
|
|
33
|
+
// clipboard that way and passed with the retry removed. A clipboard
|
|
34
|
+
// listener has a window by definition; that is how it is told.
|
|
35
|
+
const HWND window = CreateWindowExW(
|
|
36
|
+
0, L"STATIC", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
|
|
37
|
+
// Retried, and for longer than the code under test retries: the write
|
|
38
|
+
// just before this is announced to every listener on the machine, and
|
|
39
|
+
// one of them is usually still reading.
|
|
40
|
+
for (int attempt = 0; attempt < 200 && !held_; ++attempt) {
|
|
41
|
+
if (OpenClipboard(window)) {
|
|
42
|
+
held_ = true;
|
|
43
|
+
} else {
|
|
44
|
+
Sleep(5);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (held_) {
|
|
48
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(holdMs));
|
|
49
|
+
CloseClipboard();
|
|
50
|
+
}
|
|
51
|
+
if (window != nullptr) {
|
|
52
|
+
DestroyWindow(window);
|
|
53
|
+
}
|
|
54
|
+
finished_ = true;
|
|
55
|
+
});
|
|
56
|
+
// Until it is held, or the attempt to hold it has failed and held() says so.
|
|
57
|
+
while (!held_ && !finished_) {
|
|
58
|
+
std::this_thread::yield();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
~ClipboardHolder() {
|
|
62
|
+
thread_.join();
|
|
63
|
+
}
|
|
64
|
+
ClipboardHolder(const ClipboardHolder &) = delete;
|
|
65
|
+
ClipboardHolder &operator=(const ClipboardHolder &) = delete;
|
|
66
|
+
|
|
67
|
+
bool held() const {
|
|
68
|
+
return held_;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private:
|
|
72
|
+
std::thread thread_;
|
|
73
|
+
std::atomic<bool> held_{false};
|
|
74
|
+
std::atomic<bool> finished_{false};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
class RestoreClipboard {
|
|
78
|
+
public:
|
|
79
|
+
RestoreClipboard() : saved_(basalt::clipboardText()) {}
|
|
80
|
+
~RestoreClipboard() {
|
|
81
|
+
basalt::setClipboardText(saved_);
|
|
82
|
+
}
|
|
83
|
+
RestoreClipboard(const RestoreClipboard &) = delete;
|
|
84
|
+
RestoreClipboard &operator=(const RestoreClipboard &) = delete;
|
|
85
|
+
|
|
86
|
+
private:
|
|
87
|
+
std::string saved_;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
} // namespace
|
|
91
|
+
|
|
92
|
+
TEST(win32_clipboard_round_trips_unicode) {
|
|
93
|
+
RestoreClipboard restore;
|
|
94
|
+
const std::string written = "h\xC3\xA9llo \xE4\xB8\x96\xE7\x95\x8C \xF0\x9F\x9C\x82";
|
|
95
|
+
basalt::setClipboardText(written);
|
|
96
|
+
EXPECT_EQ(basalt::clipboardText(), written);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
TEST(win32_clipboard_write_waits_out_a_brief_holder) {
|
|
100
|
+
RestoreClipboard restore;
|
|
101
|
+
basalt::setClipboardText("before");
|
|
102
|
+
{
|
|
103
|
+
// Well inside the retry budget, and far longer than one attempt.
|
|
104
|
+
ClipboardHolder holder(20);
|
|
105
|
+
EXPECT(holder.held());
|
|
106
|
+
basalt::setClipboardText("after");
|
|
107
|
+
}
|
|
108
|
+
EXPECT_EQ(basalt::clipboardText(), std::string("after"));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
TEST(win32_clipboard_read_waits_out_a_brief_holder) {
|
|
112
|
+
RestoreClipboard restore;
|
|
113
|
+
basalt::setClipboardText("still here");
|
|
114
|
+
std::string read;
|
|
115
|
+
{
|
|
116
|
+
ClipboardHolder holder(20);
|
|
117
|
+
EXPECT(holder.held());
|
|
118
|
+
read = basalt::clipboardText();
|
|
119
|
+
}
|
|
120
|
+
EXPECT_EQ(read, std::string("still here"));
|
|
121
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Keyboard focus: what Tab stops on, in what order, and what Enter does.
|
|
2
|
+
//
|
|
3
|
+
// The same questions the GTK and AppKit suites ask, in the same order, because
|
|
4
|
+
// the answers have to match -- but here they are testable with no window at
|
|
5
|
+
// all. That is the difference this platform makes: on the other two, focus
|
|
6
|
+
// belongs to the toolkit and a test has to show a window to get at it. Here a
|
|
7
|
+
// React Native view is a C++ object with no window behind it, so the whole
|
|
8
|
+
// chain is Win32Focus.cpp's and can be driven directly.
|
|
9
|
+
//
|
|
10
|
+
// No emitter is attached to these hand-built shadow views, so the focus and
|
|
11
|
+
// blur events have nowhere to go and the click dispatch does nothing. What is
|
|
12
|
+
// observable is the part that decides where focus is, which is the part this
|
|
13
|
+
// project can get wrong on its own.
|
|
14
|
+
|
|
15
|
+
#include "TestHarness.h"
|
|
16
|
+
|
|
17
|
+
#include "Win32Focus.h"
|
|
18
|
+
#include "Win32MountingManager.h"
|
|
19
|
+
|
|
20
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
21
|
+
|
|
22
|
+
#include <cstdint>
|
|
23
|
+
#include <memory>
|
|
24
|
+
#include <sstream>
|
|
25
|
+
|
|
26
|
+
using basalt::Win32FocusManager;
|
|
27
|
+
using basalt::Win32MountingManager;
|
|
28
|
+
using basalt::win32::RnWin32View;
|
|
29
|
+
using facebook::react::LayoutMetrics;
|
|
30
|
+
using facebook::react::MountingTransaction;
|
|
31
|
+
using facebook::react::ShadowView;
|
|
32
|
+
using facebook::react::ShadowViewMutation;
|
|
33
|
+
using facebook::react::ShadowViewMutationList;
|
|
34
|
+
using facebook::react::SurfaceId;
|
|
35
|
+
using facebook::react::Tag;
|
|
36
|
+
using facebook::react::TransactionTelemetry;
|
|
37
|
+
using facebook::react::ViewProps;
|
|
38
|
+
|
|
39
|
+
namespace {
|
|
40
|
+
|
|
41
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
42
|
+
|
|
43
|
+
ShadowView makeAccessibleView(Tag tag, float y, bool accessible) {
|
|
44
|
+
auto props = std::make_shared<ViewProps>();
|
|
45
|
+
props->accessible = accessible;
|
|
46
|
+
|
|
47
|
+
LayoutMetrics metrics;
|
|
48
|
+
metrics.frame = {.origin = {.x = 0, .y = y}, .size = {.width = 200, .height = 40}};
|
|
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
|
+
void mountAll(Win32MountingManager &manager, std::initializer_list<ShadowView> views) {
|
|
60
|
+
ShadowViewMutationList mutations;
|
|
61
|
+
int index = 0;
|
|
62
|
+
for (const ShadowView &view : views) {
|
|
63
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(view));
|
|
64
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, view, index++));
|
|
65
|
+
}
|
|
66
|
+
manager.applyTransaction(
|
|
67
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
} // namespace
|
|
71
|
+
|
|
72
|
+
TEST(win32_focus_accessible_views_are_the_ones_tab_stops_on) {
|
|
73
|
+
Win32MountingManager manager;
|
|
74
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
75
|
+
root->setFrame(0, 0, 400, 400);
|
|
76
|
+
// React Native's `focusable` prop never reaches this platform, so `accessible`
|
|
77
|
+
// is the signal -- and it is what <Pressable> sets on everything it renders.
|
|
78
|
+
mountAll(manager,
|
|
79
|
+
{makeAccessibleView(10, 0, true),
|
|
80
|
+
makeAccessibleView(11, 50, false),
|
|
81
|
+
makeAccessibleView(12, 100, true)});
|
|
82
|
+
|
|
83
|
+
EXPECT(manager.viewForTag(10)->focusable());
|
|
84
|
+
EXPECT(!manager.viewForTag(11)->focusable());
|
|
85
|
+
EXPECT(manager.viewForTag(12)->focusable());
|
|
86
|
+
// And it shows up in the tree dump, so the cross-host diff can say the prop
|
|
87
|
+
// arrived on all three.
|
|
88
|
+
EXPECT(root->describeTree().find("focusable") != std::string::npos);
|
|
89
|
+
|
|
90
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
TEST(win32_focus_tab_visits_focusable_views_in_tree_order_and_wraps) {
|
|
94
|
+
Win32MountingManager manager;
|
|
95
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
96
|
+
root->setFrame(0, 0, 400, 400);
|
|
97
|
+
Win32FocusManager focus(&manager, root);
|
|
98
|
+
mountAll(manager,
|
|
99
|
+
{makeAccessibleView(10, 0, true),
|
|
100
|
+
makeAccessibleView(11, 50, false),
|
|
101
|
+
makeAccessibleView(12, 100, true)});
|
|
102
|
+
|
|
103
|
+
// Nothing is focused when a window opens, on any of the three.
|
|
104
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 0L);
|
|
105
|
+
|
|
106
|
+
EXPECT(focus.moveFocus(true));
|
|
107
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 10L);
|
|
108
|
+
// 11 is not accessible, so Tab goes past it.
|
|
109
|
+
EXPECT(focus.moveFocus(true));
|
|
110
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 12L);
|
|
111
|
+
// And round again: a window's Tab order wraps.
|
|
112
|
+
EXPECT(focus.moveFocus(true));
|
|
113
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 10L);
|
|
114
|
+
// Backwards is the same order in reverse.
|
|
115
|
+
EXPECT(focus.moveFocus(false));
|
|
116
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 12L);
|
|
117
|
+
|
|
118
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
TEST(win32_focus_moves_the_ring_with_it) {
|
|
122
|
+
Win32MountingManager manager;
|
|
123
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
124
|
+
root->setFrame(0, 0, 400, 400);
|
|
125
|
+
Win32FocusManager focus(&manager, root);
|
|
126
|
+
mountAll(manager, {makeAccessibleView(10, 0, true), makeAccessibleView(11, 50, true)});
|
|
127
|
+
|
|
128
|
+
// The ring is this project's own here: a view is not a window, so there is no
|
|
129
|
+
// Win32 focus for the painting to ask about.
|
|
130
|
+
focus.moveFocus(true);
|
|
131
|
+
EXPECT(manager.viewForTag(10)->showsFocusRing());
|
|
132
|
+
EXPECT(!manager.viewForTag(11)->showsFocusRing());
|
|
133
|
+
|
|
134
|
+
focus.moveFocus(true);
|
|
135
|
+
EXPECT(!manager.viewForTag(10)->showsFocusRing());
|
|
136
|
+
EXPECT(manager.viewForTag(11)->showsFocusRing());
|
|
137
|
+
|
|
138
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
TEST(win32_focus_a_hidden_view_is_not_a_tab_stop) {
|
|
142
|
+
Win32MountingManager manager;
|
|
143
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
144
|
+
root->setFrame(0, 0, 400, 400);
|
|
145
|
+
Win32FocusManager focus(&manager, root);
|
|
146
|
+
mountAll(manager, {makeAccessibleView(10, 0, true), makeAccessibleView(11, 50, true)});
|
|
147
|
+
|
|
148
|
+
// Tab stopping on something nobody can see is worse than Tab skipping it.
|
|
149
|
+
manager.viewForTag(10)->setHidden(true);
|
|
150
|
+
focus.moveFocus(true);
|
|
151
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 11L);
|
|
152
|
+
|
|
153
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
TEST(win32_focus_activating_nothing_is_not_a_click) {
|
|
157
|
+
Win32MountingManager manager;
|
|
158
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
159
|
+
root->setFrame(0, 0, 400, 400);
|
|
160
|
+
Win32FocusManager focus(&manager, root);
|
|
161
|
+
mountAll(manager, {makeAccessibleView(10, 0, true)});
|
|
162
|
+
|
|
163
|
+
// Nothing has focus yet, so there is nothing to activate -- and reporting a
|
|
164
|
+
// press with no target is how a keystroke reaches the wrong view.
|
|
165
|
+
EXPECT(!focus.activateFocused());
|
|
166
|
+
|
|
167
|
+
focus.moveFocus(true);
|
|
168
|
+
// No emitter is attached to these hand-built shadow views, so the click has
|
|
169
|
+
// nowhere to deliver. It must not crash, and the key is still consumed: that
|
|
170
|
+
// is exactly the shape of Enter arriving between a Remove and its Delete.
|
|
171
|
+
EXPECT(focus.activateFocused());
|
|
172
|
+
|
|
173
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
TEST(win32_focus_keys_that_are_not_tab_or_enter_are_left_alone) {
|
|
177
|
+
Win32MountingManager manager;
|
|
178
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
179
|
+
root->setFrame(0, 0, 400, 400);
|
|
180
|
+
Win32FocusManager focus(&manager, root);
|
|
181
|
+
mountAll(manager, {makeAccessibleView(10, 0, true)});
|
|
182
|
+
|
|
183
|
+
// Reported as unhandled, so the window's own shortcuts still see them. VK_F1
|
|
184
|
+
// and VK_ESCAPE stand for everything that is not this file's business.
|
|
185
|
+
EXPECT(!focus.handleKeyDown(0x70)); // VK_F1
|
|
186
|
+
EXPECT(!focus.handleKeyDown(0x1B)); // VK_ESCAPE
|
|
187
|
+
// And Enter with nothing focused goes on rather than being swallowed.
|
|
188
|
+
EXPECT(!focus.handleKeyDown(0x0D)); // VK_RETURN
|
|
189
|
+
|
|
190
|
+
EXPECT(focus.handleKeyDown(0x09)); // VK_TAB
|
|
191
|
+
EXPECT_EQ(static_cast<long>(focus.focusedTag()), 10L);
|
|
192
|
+
EXPECT(focus.handleKeyDown(0x0D)); // VK_RETURN, now that something has focus
|
|
193
|
+
|
|
194
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
195
|
+
}
|