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,370 @@
|
|
|
1
|
+
// The resize modes, against a real bitmap.
|
|
2
|
+
//
|
|
3
|
+
// The same seven questions tests/test_appkit_image.mm asks of AppKit, and they
|
|
4
|
+
// are asked differently here. That suite checks the destination rect the view
|
|
5
|
+
// computed; this one renders and reads the pixels, because the arithmetic is
|
|
6
|
+
// only half the claim -- `cover` that scales correctly and forgets to clip
|
|
7
|
+
// looks right in a rect and wrong on a screen.
|
|
8
|
+
//
|
|
9
|
+
// The picture is a two-colour image: red on the left half, blue on the right.
|
|
10
|
+
// That makes it asymmetric, so a fit that mirrors or transposes is visible, and
|
|
11
|
+
// it makes "which part got cropped" answerable.
|
|
12
|
+
|
|
13
|
+
#include "TestHarness.h"
|
|
14
|
+
|
|
15
|
+
#include "RnWin32Image.h"
|
|
16
|
+
#include "RnWin32View.h"
|
|
17
|
+
#include "Win32Snapshot.h"
|
|
18
|
+
|
|
19
|
+
#include <cmath>
|
|
20
|
+
#include <cstdio>
|
|
21
|
+
#include <cstdlib>
|
|
22
|
+
#include <memory>
|
|
23
|
+
#include <sstream>
|
|
24
|
+
#include <string>
|
|
25
|
+
#include <vector>
|
|
26
|
+
|
|
27
|
+
using basalt::win32::RnImageFit;
|
|
28
|
+
using basalt::win32::RnPixel;
|
|
29
|
+
using basalt::win32::RnPixels;
|
|
30
|
+
using basalt::win32::RnWin32Image;
|
|
31
|
+
using basalt::win32::RnWin32View;
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
constexpr double kTolerance = 3.0;
|
|
36
|
+
|
|
37
|
+
// Premultiplied BGRA, which is what fromPixels documents. Both halves are
|
|
38
|
+
// opaque, so premultiplied and straight are the same bytes here.
|
|
39
|
+
std::vector<uint8_t> twoTone(unsigned width, unsigned height) {
|
|
40
|
+
std::vector<uint8_t> pixels(static_cast<size_t>(width) * height * 4);
|
|
41
|
+
for (unsigned y = 0; y < height; y++) {
|
|
42
|
+
for (unsigned x = 0; x < width; x++) {
|
|
43
|
+
const size_t offset = (static_cast<size_t>(y) * width + x) * 4;
|
|
44
|
+
const bool leftHalf = x < width / 2;
|
|
45
|
+
pixels[offset + 0] = leftHalf ? 0 : 255; // blue
|
|
46
|
+
pixels[offset + 1] = 0; // green
|
|
47
|
+
pixels[offset + 2] = leftHalf ? 255 : 0; // red
|
|
48
|
+
pixels[offset + 3] = 255; // alpha
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return pixels;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
std::shared_ptr<RnWin32Image> twoToneImage(unsigned width, unsigned height) {
|
|
55
|
+
const std::vector<uint8_t> pixels = twoTone(width, height);
|
|
56
|
+
return RnWin32Image::fromPixels(width, height, pixels.data());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Renders one view holding the image in a box, and hands back the pixels.
|
|
60
|
+
RnPixels renderInBox(const std::shared_ptr<RnWin32Image> &image,
|
|
61
|
+
RnImageFit fit,
|
|
62
|
+
float boxWidth,
|
|
63
|
+
float boxHeight) {
|
|
64
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
65
|
+
root->setFrame(0, 0, boxWidth, boxHeight);
|
|
66
|
+
root->setImage(image, fit);
|
|
67
|
+
return basalt::win32::renderToPixels(*root);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
} // namespace
|
|
71
|
+
|
|
72
|
+
#define EXPECT_PIXEL(pixels, x, y, r, g, b, a) \
|
|
73
|
+
do { \
|
|
74
|
+
const RnPixel pixel = (pixels).at((x), (y)); \
|
|
75
|
+
EXPECT_NEAR(pixel.red, (r), kTolerance); \
|
|
76
|
+
EXPECT_NEAR(pixel.green, (g), kTolerance); \
|
|
77
|
+
EXPECT_NEAR(pixel.blue, (b), kTolerance); \
|
|
78
|
+
EXPECT_NEAR(pixel.alpha, (a), kTolerance); \
|
|
79
|
+
} while (false)
|
|
80
|
+
|
|
81
|
+
#define EXPECT_TRANSPARENT(pixels, x, y) EXPECT_NEAR((pixels).at((x), (y)).alpha, 0, kTolerance)
|
|
82
|
+
|
|
83
|
+
TEST(image_decodes_from_pixels) {
|
|
84
|
+
auto image = twoToneImage(40, 20);
|
|
85
|
+
EXPECT(image != nullptr);
|
|
86
|
+
EXPECT_EQ(image->width(), 40u);
|
|
87
|
+
EXPECT_EQ(image->height(), 20u);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
TEST(image_stretch_fills_the_frame) {
|
|
91
|
+
auto image = twoToneImage(40, 20);
|
|
92
|
+
const RnPixels pixels = renderInBox(image, RnImageFit::Stretch, 200, 100);
|
|
93
|
+
EXPECT(!pixels.empty());
|
|
94
|
+
|
|
95
|
+
// Distorted to the whole box, so every corner is covered and the halves land
|
|
96
|
+
// where the aspect ratio says they should not.
|
|
97
|
+
EXPECT_PIXEL(pixels, 50, 50, 255, 0, 0, 255);
|
|
98
|
+
EXPECT_PIXEL(pixels, 150, 50, 0, 0, 255, 255);
|
|
99
|
+
EXPECT_PIXEL(pixels, 5, 5, 255, 0, 0, 255);
|
|
100
|
+
EXPECT_PIXEL(pixels, 195, 95, 0, 0, 255, 255);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
TEST(image_contain_fits_inside_and_centres) {
|
|
104
|
+
// A 40x20 image in a 200x200 box: contain scales by 5, giving 200x100
|
|
105
|
+
// centred, so the top and bottom fifty rows are empty.
|
|
106
|
+
auto image = twoToneImage(40, 20);
|
|
107
|
+
const RnPixels pixels = renderInBox(image, RnImageFit::Contain, 200, 200);
|
|
108
|
+
EXPECT(!pixels.empty());
|
|
109
|
+
|
|
110
|
+
EXPECT_TRANSPARENT(pixels, 100, 10);
|
|
111
|
+
EXPECT_TRANSPARENT(pixels, 100, 190);
|
|
112
|
+
EXPECT_PIXEL(pixels, 50, 100, 255, 0, 0, 255);
|
|
113
|
+
EXPECT_PIXEL(pixels, 150, 100, 0, 0, 255, 255);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
TEST(image_cover_fills_the_frame_and_crops) {
|
|
117
|
+
// The same image in the same box: cover scales by 10, giving 400x200, so the
|
|
118
|
+
// sides are cropped and nothing is empty.
|
|
119
|
+
auto image = twoToneImage(40, 20);
|
|
120
|
+
const RnPixels pixels = renderInBox(image, RnImageFit::Cover, 200, 200);
|
|
121
|
+
EXPECT(!pixels.empty());
|
|
122
|
+
|
|
123
|
+
// Top and bottom are covered, which is the difference from `contain` -- and
|
|
124
|
+
// sampled away from x=100, because the seam between the two halves lands
|
|
125
|
+
// exactly there and the sample would read the interpolation across it. That
|
|
126
|
+
// is not a nicety: the first version of this test asked at x=100 and got
|
|
127
|
+
// (115,0,140), which is a real answer to the wrong question.
|
|
128
|
+
EXPECT_PIXEL(pixels, 50, 10, 255, 0, 0, 255);
|
|
129
|
+
EXPECT_PIXEL(pixels, 50, 190, 255, 0, 0, 255);
|
|
130
|
+
// The seam is at the image's centre, which cover keeps at the box's centre --
|
|
131
|
+
// so the left of the box is red and the right is blue, with the outer
|
|
132
|
+
// quarters of the image cropped away.
|
|
133
|
+
EXPECT_PIXEL(pixels, 20, 100, 255, 0, 0, 255);
|
|
134
|
+
EXPECT_PIXEL(pixels, 180, 100, 0, 0, 255, 255);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
TEST(image_cover_does_not_paint_outside_the_frame) {
|
|
138
|
+
// The clip is the half of `cover` a destination rect cannot show. The image
|
|
139
|
+
// is drawn 400 wide inside a 200-wide view, so without the clip it would
|
|
140
|
+
// reach the surrounding root.
|
|
141
|
+
auto image = twoToneImage(40, 20);
|
|
142
|
+
|
|
143
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
144
|
+
root->setFrame(0, 0, 400, 200);
|
|
145
|
+
auto framed = std::make_unique<RnWin32View>(2);
|
|
146
|
+
framed->setFrame(100, 0, 200, 200);
|
|
147
|
+
framed->setImage(image, RnImageFit::Cover);
|
|
148
|
+
root->insertChild(framed.get(), 0);
|
|
149
|
+
|
|
150
|
+
const RnPixels pixels = basalt::win32::renderToPixels(*root);
|
|
151
|
+
EXPECT(!pixels.empty());
|
|
152
|
+
|
|
153
|
+
EXPECT_PIXEL(pixels, 150, 100, 255, 0, 0, 255);
|
|
154
|
+
EXPECT_TRANSPARENT(pixels, 50, 100);
|
|
155
|
+
EXPECT_TRANSPARENT(pixels, 350, 100);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
TEST(image_center_draws_at_natural_size) {
|
|
159
|
+
// 40x20 in a 200x200 box, centred and unscaled: it occupies x 80..120,
|
|
160
|
+
// y 90..110.
|
|
161
|
+
auto image = twoToneImage(40, 20);
|
|
162
|
+
const RnPixels pixels = renderInBox(image, RnImageFit::Center, 200, 200);
|
|
163
|
+
EXPECT(!pixels.empty());
|
|
164
|
+
|
|
165
|
+
EXPECT_PIXEL(pixels, 90, 100, 255, 0, 0, 255);
|
|
166
|
+
EXPECT_PIXEL(pixels, 110, 100, 0, 0, 255, 255);
|
|
167
|
+
EXPECT_TRANSPARENT(pixels, 60, 100);
|
|
168
|
+
EXPECT_TRANSPARENT(pixels, 100, 70);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
TEST(image_center_shrinks_an_oversized_image) {
|
|
172
|
+
// Bigger than the box, so `center` behaves like `contain` rather than
|
|
173
|
+
// overflowing: 400x200 into 200x200 scales by a half to 200x100, centred.
|
|
174
|
+
auto image = twoToneImage(400, 200);
|
|
175
|
+
const RnPixels pixels = renderInBox(image, RnImageFit::Center, 200, 200);
|
|
176
|
+
EXPECT(!pixels.empty());
|
|
177
|
+
|
|
178
|
+
EXPECT_PIXEL(pixels, 50, 100, 255, 0, 0, 255);
|
|
179
|
+
EXPECT_PIXEL(pixels, 150, 100, 0, 0, 255, 255);
|
|
180
|
+
EXPECT_TRANSPARENT(pixels, 100, 10);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
TEST(image_is_reported_in_the_tree) {
|
|
184
|
+
auto view = std::make_unique<RnWin32View>(9);
|
|
185
|
+
view->setFrame(0, 0, 64, 64);
|
|
186
|
+
view->setImage(twoToneImage(40, 20), RnImageFit::Contain);
|
|
187
|
+
|
|
188
|
+
EXPECT_EQ(view->describeTree(),
|
|
189
|
+
std::string("view tag=9 frame=(0,0 64x64) texture=40x20 fit=contain\n"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
TEST(image_can_be_cleared) {
|
|
193
|
+
auto view = std::make_unique<RnWin32View>(9);
|
|
194
|
+
view->setFrame(0, 0, 64, 64);
|
|
195
|
+
view->setImage(twoToneImage(40, 20), RnImageFit::Cover);
|
|
196
|
+
view->setImage(nullptr, RnImageFit::Cover);
|
|
197
|
+
|
|
198
|
+
EXPECT(view->image() == nullptr);
|
|
199
|
+
EXPECT_EQ(view->describeTree(), std::string("view tag=9 frame=(0,0 64x64)\n"));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
TEST(image_decodes_an_encoded_png) {
|
|
203
|
+
// A round trip through the two halves of this platform's image handling: the
|
|
204
|
+
// snapshot encoder writes a PNG, and WIC reads it back. Cheaper than checking
|
|
205
|
+
// in a fixture, and it fails if either half breaks.
|
|
206
|
+
auto source = std::make_unique<RnWin32View>(1);
|
|
207
|
+
source->setFrame(0, 0, 24, 12);
|
|
208
|
+
source->setBackgroundColor(0.0f, 1.0f, 0.0f, 1.0f, true);
|
|
209
|
+
|
|
210
|
+
const std::string path = std::string(std::getenv("TEMP") ? std::getenv("TEMP") : ".") +
|
|
211
|
+
"\\basalt_win32_roundtrip.png";
|
|
212
|
+
EXPECT(basalt::win32::writeSnapshot(*source, path));
|
|
213
|
+
|
|
214
|
+
std::vector<uint8_t> encoded;
|
|
215
|
+
if (FILE *file = std::fopen(path.c_str(), "rb")) {
|
|
216
|
+
std::fseek(file, 0, SEEK_END);
|
|
217
|
+
const long size = std::ftell(file);
|
|
218
|
+
std::fseek(file, 0, SEEK_SET);
|
|
219
|
+
encoded.resize(static_cast<size_t>(size > 0 ? size : 0));
|
|
220
|
+
if (!encoded.empty()) {
|
|
221
|
+
const size_t read = std::fread(encoded.data(), 1, encoded.size(), file);
|
|
222
|
+
encoded.resize(read);
|
|
223
|
+
}
|
|
224
|
+
std::fclose(file);
|
|
225
|
+
}
|
|
226
|
+
EXPECT(!encoded.empty());
|
|
227
|
+
|
|
228
|
+
auto decoded = RnWin32Image::fromEncodedBytes(encoded.data(), encoded.size());
|
|
229
|
+
EXPECT(decoded != nullptr);
|
|
230
|
+
if (decoded != nullptr) {
|
|
231
|
+
EXPECT_EQ(decoded->width(), 24u);
|
|
232
|
+
EXPECT_EQ(decoded->height(), 12u);
|
|
233
|
+
}
|
|
234
|
+
std::remove(path.c_str());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
TEST(image_rejects_bytes_that_are_not_an_image) {
|
|
238
|
+
const uint8_t rubbish[] = {'n', 'o', 't', ' ', 'a', ' ', 'p', 'n', 'g'};
|
|
239
|
+
EXPECT(RnWin32Image::fromEncodedBytes(rubbish, sizeof(rubbish)) == nullptr);
|
|
240
|
+
EXPECT(RnWin32Image::fromEncodedBytes(nullptr, 0) == nullptr);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// `blurRadius`.
|
|
245
|
+
//
|
|
246
|
+
// Measured as the width of the ramp between the two halves, which is what a
|
|
247
|
+
// blur does and what a radius means: the two-tone image has one hard edge down
|
|
248
|
+
// its middle, and blurring spreads it. The same instrument
|
|
249
|
+
// tests/test_appkit_image.mm uses, for the same reason -- an exact pixel would
|
|
250
|
+
// pin the filter's kernel rather than the prop.
|
|
251
|
+
//
|
|
252
|
+
// In the view's coordinates rather than the image's pixels, which is the rule
|
|
253
|
+
// both other hosts measured: the same number has to be the same picture on a
|
|
254
|
+
// photograph and on an icon.
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
namespace {
|
|
257
|
+
|
|
258
|
+
RnPixels renderBlurred(const std::shared_ptr<RnWin32Image> &image,
|
|
259
|
+
float radius,
|
|
260
|
+
float boxWidth,
|
|
261
|
+
float boxHeight) {
|
|
262
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
263
|
+
root->setFrame(0, 0, boxWidth, boxHeight);
|
|
264
|
+
root->setImage(image, RnImageFit::Stretch);
|
|
265
|
+
root->setImageBlur(radius);
|
|
266
|
+
return basalt::win32::renderToPixels(*root);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// How many columns along the middle row are neither red nor blue, which is the
|
|
270
|
+
// ramp the blur produced. A hard edge is one or two columns of antialiasing.
|
|
271
|
+
int rampWidth(const RnPixels &pixels) {
|
|
272
|
+
int mixed = 0;
|
|
273
|
+
const unsigned row = pixels.height() / 2;
|
|
274
|
+
for (unsigned x = 0; x < pixels.width(); x++) {
|
|
275
|
+
const RnPixel pixel = pixels.at(x, row);
|
|
276
|
+
if (pixel.alpha < 32) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const bool red = pixel.red > 200 && pixel.blue < 60;
|
|
280
|
+
const bool blue = pixel.blue > 200 && pixel.red < 60;
|
|
281
|
+
if (!red && !blue) {
|
|
282
|
+
mixed++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return mixed;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
} // namespace
|
|
289
|
+
|
|
290
|
+
TEST(image_blur_radius_softens_the_edge) {
|
|
291
|
+
const auto image = twoToneImage(40, 8);
|
|
292
|
+
EXPECT(image != nullptr);
|
|
293
|
+
|
|
294
|
+
const int sharp = rampWidth(renderBlurred(image, 0.0f, 100, 100));
|
|
295
|
+
const int blurred = rampWidth(renderBlurred(image, 12.0f, 100, 100));
|
|
296
|
+
|
|
297
|
+
// A hard edge is a column or two of antialiasing; a twelve point blur is a
|
|
298
|
+
// ramp several times wider. Not an exact width: that would pin Direct2D's
|
|
299
|
+
// kernel rather than the prop.
|
|
300
|
+
EXPECT(sharp <= 4);
|
|
301
|
+
EXPECT(blurred > sharp + 6);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
TEST(image_blur_radius_is_in_view_coordinates) {
|
|
305
|
+
// The same image at two box sizes with the same radius. If the blur were
|
|
306
|
+
// applied in the image's own pixels, the ramp would scale with the box; in
|
|
307
|
+
// the view's coordinates it is the same width in both, which is the rule the
|
|
308
|
+
// other two hosts measured.
|
|
309
|
+
const auto image = twoToneImage(40, 8);
|
|
310
|
+
const int small = rampWidth(renderBlurred(image, 10.0f, 100, 100));
|
|
311
|
+
const int large = rampWidth(renderBlurred(image, 10.0f, 200, 100));
|
|
312
|
+
|
|
313
|
+
EXPECT(small > 4);
|
|
314
|
+
EXPECT(large > 4);
|
|
315
|
+
// Within a few pixels of each other, where blurring in source pixels would
|
|
316
|
+
// have doubled one of them.
|
|
317
|
+
EXPECT(std::abs(small - large) < 8);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
TEST(image_blur_radius_of_zero_or_less_draws_the_image_itself) {
|
|
321
|
+
const auto image = twoToneImage(40, 8);
|
|
322
|
+
|
|
323
|
+
const RnPixels sharp = renderBlurred(image, 0.0f, 100, 100);
|
|
324
|
+
// Left half red, right half blue, as the unblurred tests assert.
|
|
325
|
+
EXPECT_PIXEL(sharp, 10, 50, 255, 0, 0, 255);
|
|
326
|
+
EXPECT_PIXEL(sharp, 90, 50, 0, 0, 255, 255);
|
|
327
|
+
|
|
328
|
+
// Negative is no blur rather than a crash, which nothing stops an app from
|
|
329
|
+
// sending.
|
|
330
|
+
const RnPixels negative = renderBlurred(image, -4.0f, 100, 100);
|
|
331
|
+
EXPECT_PIXEL(negative, 10, 50, 255, 0, 0, 255);
|
|
332
|
+
EXPECT_PIXEL(negative, 90, 50, 0, 0, 255, 255);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A blur spreads beyond its input, and an <Image> never paints outside its own
|
|
336
|
+
// frame: both other hosts blur inside the clip, so this one does too.
|
|
337
|
+
TEST(image_blur_does_not_paint_outside_the_frame) {
|
|
338
|
+
const auto image = twoToneImage(40, 8);
|
|
339
|
+
|
|
340
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
341
|
+
root->setFrame(0, 0, 100, 100);
|
|
342
|
+
auto child = std::make_unique<RnWin32View>(2);
|
|
343
|
+
child->setFrame(20, 20, 60, 60);
|
|
344
|
+
child->setImage(image, RnImageFit::Stretch);
|
|
345
|
+
child->setImageBlur(14.0f);
|
|
346
|
+
root->insertChild(child.get(), 0);
|
|
347
|
+
|
|
348
|
+
const RnPixels pixels = basalt::win32::renderToPixels(*root);
|
|
349
|
+
EXPECT(!pixels.empty());
|
|
350
|
+
// Just outside the child's box on each side.
|
|
351
|
+
EXPECT_TRANSPARENT(pixels, 50, 18);
|
|
352
|
+
EXPECT_TRANSPARENT(pixels, 50, 82);
|
|
353
|
+
EXPECT_TRANSPARENT(pixels, 18, 50);
|
|
354
|
+
EXPECT_TRANSPARENT(pixels, 82, 50);
|
|
355
|
+
// And something inside it.
|
|
356
|
+
EXPECT(pixels.at(50, 50).alpha > 200);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
TEST(image_blur_is_reported_in_the_tree) {
|
|
360
|
+
const auto image = twoToneImage(4, 2);
|
|
361
|
+
auto root = std::make_unique<RnWin32View>(1);
|
|
362
|
+
root->setFrame(0, 0, 50, 50);
|
|
363
|
+
root->setImage(image, RnImageFit::Stretch);
|
|
364
|
+
|
|
365
|
+
EXPECT(root->describeTree().find("blur=") == std::string::npos);
|
|
366
|
+
root->setImageBlur(12.0f);
|
|
367
|
+
EXPECT(root->describeTree().find("blur=12") != std::string::npos);
|
|
368
|
+
root->setImageBlur(0.0f);
|
|
369
|
+
EXPECT(root->describeTree().find("blur=") == std::string::npos);
|
|
370
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// The image loader: the cache, the ways a load can fail, and the two lifetimes
|
|
2
|
+
// that make this a class rather than a function.
|
|
3
|
+
//
|
|
4
|
+
// The equivalent of `tests/test_image.cpp` on GTK, which asks the same three
|
|
5
|
+
// questions of its loader. What is extra here is the loader outliving its owner
|
|
6
|
+
// -- GTK's loader does not have that hazard, because a completion queued on a
|
|
7
|
+
// GLib source is cancelled with the source, and AppKit's has the hazard and no
|
|
8
|
+
// test.
|
|
9
|
+
//
|
|
10
|
+
// These run with no UI thread installed, which is deliberate and is the
|
|
11
|
+
// configuration `load` detects: with nothing to marshal back to it stays on one
|
|
12
|
+
// thread, so a completion has run by the time `load` returns and nothing here
|
|
13
|
+
// has to wait for anything.
|
|
14
|
+
|
|
15
|
+
#include "TestHarness.h"
|
|
16
|
+
|
|
17
|
+
#include "Win32ImageLoader.h"
|
|
18
|
+
#include "Win32Snapshot.h"
|
|
19
|
+
#include "RnWin32View.h"
|
|
20
|
+
|
|
21
|
+
#include <cstdio>
|
|
22
|
+
#include <cstdlib>
|
|
23
|
+
#include <memory>
|
|
24
|
+
#include <sstream>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
28
|
+
using basalt::win32::RnWin32Image;
|
|
29
|
+
using basalt::win32::RnWin32View;
|
|
30
|
+
using basalt::win32::Win32ImageLoader;
|
|
31
|
+
|
|
32
|
+
namespace {
|
|
33
|
+
|
|
34
|
+
// A real PNG on disk, written by the snapshot encoder rather than checked in.
|
|
35
|
+
// Cheaper than a fixture and it fails if either half of the image path breaks.
|
|
36
|
+
std::string writeTemporaryPng(const char *name) {
|
|
37
|
+
auto source = std::make_unique<RnWin32View>(1);
|
|
38
|
+
source->setFrame(0, 0, 20, 10);
|
|
39
|
+
source->setBackgroundColor(0.0f, 0.5f, 1.0f, 1.0f, true);
|
|
40
|
+
|
|
41
|
+
const char *temp = std::getenv("TEMP");
|
|
42
|
+
std::string path = std::string(temp != nullptr ? temp : ".") + "\\" + name;
|
|
43
|
+
if (!basalt::win32::writeSnapshot(*source, path)) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// file:// is what an app's bundled asset arrives as, and what
|
|
50
|
+
// core/ImageBytes.cpp understands without a network.
|
|
51
|
+
std::string fileUri(const std::string &path) {
|
|
52
|
+
std::string uri = "file:///" + path;
|
|
53
|
+
for (char &c : uri) {
|
|
54
|
+
if (c == '\\') {
|
|
55
|
+
c = '/';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return uri;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
} // namespace
|
|
62
|
+
|
|
63
|
+
TEST(image_loader_decodes_a_file_uri) {
|
|
64
|
+
const std::string path = writeTemporaryPng("basalt_loader_one.png");
|
|
65
|
+
EXPECT(!path.empty());
|
|
66
|
+
|
|
67
|
+
Win32ImageLoader loader;
|
|
68
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
69
|
+
std::string error;
|
|
70
|
+
loader.load(fileUri(path), [&](std::shared_ptr<RnWin32Image> image, const std::string &why) {
|
|
71
|
+
loaded = std::move(image);
|
|
72
|
+
error = why;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
EXPECT(loaded != nullptr);
|
|
76
|
+
EXPECT_EQ(error, std::string{});
|
|
77
|
+
if (loaded != nullptr) {
|
|
78
|
+
EXPECT_EQ(loaded->width(), 20u);
|
|
79
|
+
EXPECT_EQ(loaded->height(), 10u);
|
|
80
|
+
}
|
|
81
|
+
std::remove(path.c_str());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
TEST(image_loader_serves_a_second_request_from_cache) {
|
|
85
|
+
const std::string path = writeTemporaryPng("basalt_loader_two.png");
|
|
86
|
+
EXPECT(!path.empty());
|
|
87
|
+
const std::string uri = fileUri(path);
|
|
88
|
+
|
|
89
|
+
Win32ImageLoader loader;
|
|
90
|
+
std::shared_ptr<RnWin32Image> first;
|
|
91
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
92
|
+
first = std::move(image);
|
|
93
|
+
});
|
|
94
|
+
EXPECT(first != nullptr);
|
|
95
|
+
|
|
96
|
+
// Deleted before the second request, so a loader that went back to the disk
|
|
97
|
+
// would fail. The same object coming back is what says it did not.
|
|
98
|
+
std::remove(path.c_str());
|
|
99
|
+
|
|
100
|
+
std::shared_ptr<RnWin32Image> second;
|
|
101
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
102
|
+
second = std::move(image);
|
|
103
|
+
});
|
|
104
|
+
EXPECT(second != nullptr);
|
|
105
|
+
EXPECT(first == second);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
TEST(image_loader_reports_a_missing_file) {
|
|
109
|
+
Win32ImageLoader loader;
|
|
110
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
111
|
+
std::string error;
|
|
112
|
+
loader.load("file:///c:/nothing/here/at/all.png",
|
|
113
|
+
[&](std::shared_ptr<RnWin32Image> image, const std::string &why) {
|
|
114
|
+
loaded = std::move(image);
|
|
115
|
+
error = why;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
EXPECT(loaded == nullptr);
|
|
119
|
+
EXPECT(!error.empty());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
TEST(image_loader_rejects_an_empty_uri) {
|
|
123
|
+
Win32ImageLoader loader;
|
|
124
|
+
bool called = false;
|
|
125
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
126
|
+
loader.load("", [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
127
|
+
called = true;
|
|
128
|
+
loaded = std::move(image);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Answered rather than ignored: a caller that never hears back cannot tell
|
|
132
|
+
// the difference between a slow load and a dropped one.
|
|
133
|
+
EXPECT(called);
|
|
134
|
+
EXPECT(loaded == nullptr);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
TEST(image_loader_reports_bytes_that_are_not_an_image) {
|
|
138
|
+
const char *temp = std::getenv("TEMP");
|
|
139
|
+
const std::string path = std::string(temp != nullptr ? temp : ".") + "\\basalt_loader_junk.png";
|
|
140
|
+
if (FILE *file = std::fopen(path.c_str(), "wb")) {
|
|
141
|
+
std::fputs("not a png at all", file);
|
|
142
|
+
std::fclose(file);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Win32ImageLoader loader;
|
|
146
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
147
|
+
std::string error;
|
|
148
|
+
loader.load(fileUri(path), [&](std::shared_ptr<RnWin32Image> image, const std::string &why) {
|
|
149
|
+
loaded = std::move(image);
|
|
150
|
+
error = why;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
EXPECT(loaded == nullptr);
|
|
154
|
+
EXPECT(!error.empty());
|
|
155
|
+
std::remove(path.c_str());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
TEST(image_loader_clears_its_cache) {
|
|
159
|
+
const std::string path = writeTemporaryPng("basalt_loader_three.png");
|
|
160
|
+
EXPECT(!path.empty());
|
|
161
|
+
const std::string uri = fileUri(path);
|
|
162
|
+
|
|
163
|
+
Win32ImageLoader loader;
|
|
164
|
+
std::shared_ptr<RnWin32Image> first;
|
|
165
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
166
|
+
first = std::move(image);
|
|
167
|
+
});
|
|
168
|
+
EXPECT(first != nullptr);
|
|
169
|
+
|
|
170
|
+
loader.clearCache();
|
|
171
|
+
|
|
172
|
+
std::shared_ptr<RnWin32Image> second;
|
|
173
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
174
|
+
second = std::move(image);
|
|
175
|
+
});
|
|
176
|
+
EXPECT(second != nullptr);
|
|
177
|
+
// A different object: the cache really was dropped and the file read again.
|
|
178
|
+
EXPECT(first != second);
|
|
179
|
+
|
|
180
|
+
std::remove(path.c_str());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
TEST(image_loader_state_outlives_the_loader) {
|
|
184
|
+
// The hazard this class exists to avoid. A load's completion holds a shared
|
|
185
|
+
// reference to the loader's state rather than to the loader, so a loader
|
|
186
|
+
// destroyed between the two is not a use-after-free.
|
|
187
|
+
//
|
|
188
|
+
// Synchronous here, so this cannot catch the race directly -- what it does
|
|
189
|
+
// catch is the shape: destroying a loader that has loaded must not disturb
|
|
190
|
+
// an image its caller is still holding, which would be the symptom if the
|
|
191
|
+
// cache owned rather than shared.
|
|
192
|
+
const std::string path = writeTemporaryPng("basalt_loader_four.png");
|
|
193
|
+
EXPECT(!path.empty());
|
|
194
|
+
|
|
195
|
+
std::shared_ptr<RnWin32Image> survivor;
|
|
196
|
+
{
|
|
197
|
+
Win32ImageLoader loader;
|
|
198
|
+
loader.load(fileUri(path), [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
199
|
+
survivor = std::move(image);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
EXPECT(survivor != nullptr);
|
|
204
|
+
if (survivor != nullptr) {
|
|
205
|
+
EXPECT_EQ(survivor->width(), 20u);
|
|
206
|
+
}
|
|
207
|
+
std::remove(path.c_str());
|
|
208
|
+
}
|