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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +429 -0
  5. package/native/tests/test_win32_accessibility.cpp +585 -0
  6. package/native/tests/test_win32_animated_image.cpp +351 -0
  7. package/native/tests/test_win32_clipboard.cpp +121 -0
  8. package/native/tests/test_win32_focus.cpp +195 -0
  9. package/native/tests/test_win32_hittest.cpp +750 -0
  10. package/native/tests/test_win32_image.cpp +370 -0
  11. package/native/tests/test_win32_imageloader.cpp +208 -0
  12. package/native/tests/test_win32_input.cpp +511 -0
  13. package/native/tests/test_win32_key_events.cpp +84 -0
  14. package/native/tests/test_win32_mounting.cpp +757 -0
  15. package/native/tests/test_win32_paint.cpp +1799 -0
  16. package/native/tests/test_win32_scrollview.cpp +647 -0
  17. package/native/tests/test_win32_selection.cpp +215 -0
  18. package/native/tests/test_win32_text.cpp +1456 -0
  19. package/native/tests/test_win32_textinput.cpp +1594 -0
  20. package/native/tests/test_win32_titlebar.cpp +111 -0
  21. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  22. package/native/tests/test_win32_view.cpp +222 -0
  23. package/native/win32/ColorSchemeWin32.cpp +56 -0
  24. package/native/win32/ComponentRegistryWin32.cpp +156 -0
  25. package/native/win32/DirectWriteLayout.cpp +485 -0
  26. package/native/win32/DirectWriteLayout.h +77 -0
  27. package/native/win32/DirectWriteLayoutManager.cpp +154 -0
  28. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  29. package/native/win32/PlatformServicesWin32.cpp +744 -0
  30. package/native/win32/RnWin32Accessible.cpp +343 -0
  31. package/native/win32/RnWin32Accessible.h +130 -0
  32. package/native/win32/RnWin32Image.cpp +588 -0
  33. package/native/win32/RnWin32Image.h +139 -0
  34. package/native/win32/RnWin32TextLayout.cpp +1379 -0
  35. package/native/win32/RnWin32TextLayout.h +390 -0
  36. package/native/win32/RnWin32View.cpp +2956 -0
  37. package/native/win32/RnWin32View.h +926 -0
  38. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  39. package/native/win32/Win32AnimationChoreographer.h +61 -0
  40. package/native/win32/Win32Clip.h +209 -0
  41. package/native/win32/Win32Cursors.cpp +309 -0
  42. package/native/win32/Win32Cursors.h +121 -0
  43. package/native/win32/Win32DropTarget.cpp +567 -0
  44. package/native/win32/Win32DropTarget.h +42 -0
  45. package/native/win32/Win32Focus.cpp +262 -0
  46. package/native/win32/Win32Focus.h +109 -0
  47. package/native/win32/Win32ImageLoader.cpp +199 -0
  48. package/native/win32/Win32ImageLoader.h +133 -0
  49. package/native/win32/Win32InputScopes.h +92 -0
  50. package/native/win32/Win32KeyEvents.cpp +126 -0
  51. package/native/win32/Win32KeyEvents.h +20 -0
  52. package/native/win32/Win32MenuBar.cpp +382 -0
  53. package/native/win32/Win32MenuBar.h +16 -0
  54. package/native/win32/Win32MountingManager.cpp +1282 -0
  55. package/native/win32/Win32MountingManager.h +243 -0
  56. package/native/win32/Win32Offscreen.h +53 -0
  57. package/native/win32/Win32Packaging.cpp +161 -0
  58. package/native/win32/Win32Packaging.h +56 -0
  59. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  60. package/native/win32/Win32RunLoopObserver.h +42 -0
  61. package/native/win32/Win32ScrollView.cpp +718 -0
  62. package/native/win32/Win32ScrollView.h +256 -0
  63. package/native/win32/Win32SkiaContext.cpp +221 -0
  64. package/native/win32/Win32SkiaContext.h +53 -0
  65. package/native/win32/Win32SkiaModule.cpp +80 -0
  66. package/native/win32/Win32SkiaModule.h +50 -0
  67. package/native/win32/Win32SkiaPeer.cpp +358 -0
  68. package/native/win32/Win32SkiaPeer.h +66 -0
  69. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  70. package/native/win32/Win32Snapshot.cpp +201 -0
  71. package/native/win32/Win32Snapshot.h +66 -0
  72. package/native/win32/Win32Strings.h +54 -0
  73. package/native/win32/Win32TextInput.cpp +1296 -0
  74. package/native/win32/Win32TextInput.h +341 -0
  75. package/native/win32/Win32TitleBar.cpp +562 -0
  76. package/native/win32/Win32TitleBar.h +146 -0
  77. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  78. package/native/win32/Win32TitleBarLayout.h +58 -0
  79. package/native/win32/Win32TouchDispatcher.cpp +577 -0
  80. package/native/win32/Win32TouchDispatcher.h +201 -0
  81. package/native/win32/Win32UiThread.h +27 -0
  82. package/native/win32/Win32WindowControl.cpp +329 -0
  83. package/native/win32/Win32WindowModule.cpp +209 -0
  84. package/native/win32/Win32WindowModule.h +72 -0
  85. package/native/win32/demo_layout_win32.cpp +232 -0
  86. package/native/win32/main_win32.cpp +2722 -0
  87. package/native/win32/mount_harness_win32.cpp +316 -0
  88. package/package.json +25 -4
  89. package/react-native.config.js +22 -0
  90. 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
+ }