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,1799 @@
1
+ // What actually reached the pixels.
2
+ //
3
+ // Every other suite in this project asserts on the view tree: that a view has a
4
+ // colour, a frame and a transform. `docs/BACKLOG.md` has carried the gap that
5
+ // leaves since GTK -- "GTK's cairo renderer mangled every transform in the demo
6
+ // and no test noticed" -- because closing it on the other two platforms means a
7
+ // display server or an offscreen window and a display cycle.
8
+ //
9
+ // Direct2D renders into a WIC bitmap with no window and no display, so here it
10
+ // costs a function call. These are the assertions that would have caught that
11
+ // bug: a transform composed in the wrong order, a clip that does not clip, an
12
+ // opacity applied per-brush instead of per-subtree, and a zIndex that reorders
13
+ // nothing all leave the tree dump identical and the picture wrong.
14
+
15
+ #include "TestHarness.h"
16
+
17
+ #include "RnWin32View.h"
18
+ #include "Win32Snapshot.h"
19
+
20
+ #include <d2d1.h>
21
+ #include <wrl/client.h>
22
+
23
+ #include <memory>
24
+ #include <sstream>
25
+ #include <string>
26
+ #include <vector>
27
+
28
+ using basalt::win32::RnPixel;
29
+ using basalt::win32::RnPixels;
30
+ using basalt::win32::RnWin32View;
31
+
32
+ namespace {
33
+
34
+ // Rounding through premultiplied storage costs a unit or two, and Direct2D
35
+ // antialiases edges -- so every point these tests ask about is well inside or
36
+ // well outside a shape, never on its boundary.
37
+ constexpr double kTolerance = 3.0;
38
+
39
+ class Tree {
40
+ public:
41
+ RnWin32View *box(int32_t tag, float x, float y, float w, float h) {
42
+ auto view = std::make_unique<RnWin32View>(tag);
43
+ view->setFrame(x, y, w, h);
44
+ RnWin32View *raw = view.get();
45
+ views_.push_back(std::move(view));
46
+ return raw;
47
+ }
48
+
49
+ RnWin32View *
50
+ colouredBox(int32_t tag, float x, float y, float w, float h, float r, float g, float b) {
51
+ RnWin32View *view = box(tag, x, y, w, h);
52
+ view->setBackgroundColor(r, g, b, 1.0f, true);
53
+ return view;
54
+ }
55
+
56
+ private:
57
+ std::vector<std::unique_ptr<RnWin32View>> views_;
58
+ };
59
+
60
+ // Stands in for the Skia surface behind a `<Canvas>`: fills the box it is
61
+ // handed, and remembers what it was handed.
62
+ //
63
+ // A fake rather than the real thing because the seam is what these tests are
64
+ // about. Whether Skia drew the right picture is Skia's business; whether this
65
+ // host puts what it drew in the right place, inside the clip and under the
66
+ // opacity, is the part that is this host's and the part that has been wrong on
67
+ // every platform at least once.
68
+ class FillPainter final : public basalt::win32::RnWin32Painter {
69
+ public:
70
+ FillPainter(float red, float green, float blue) : red_(red), green_(green), blue_(blue) {}
71
+
72
+ void draw(ID2D1RenderTarget *target, float boxWidth, float boxHeight) override {
73
+ calls++;
74
+ lastWidth = boxWidth;
75
+ lastHeight = boxHeight;
76
+ Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
77
+ if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(red_, green_, blue_, 1.0f),
78
+ brush.GetAddressOf()))) {
79
+ return;
80
+ }
81
+ target->FillRectangle(D2D1::RectF(0.0f, 0.0f, boxWidth, boxHeight), brush.Get());
82
+ }
83
+
84
+ int calls = 0;
85
+ float lastWidth = 0.0f;
86
+ float lastHeight = 0.0f;
87
+
88
+ private:
89
+ float red_;
90
+ float green_;
91
+ float blue_;
92
+ };
93
+
94
+ } // namespace
95
+
96
+ // A macro rather than a function so a failure reports the line that asked,
97
+ // which is the only thing that makes a wrong pixel findable.
98
+ #define EXPECT_PIXEL(pixels, x, y, r, g, b, a) \
99
+ do { \
100
+ const RnPixel pixel = (pixels).at((x), (y)); \
101
+ EXPECT_NEAR(pixel.red, (r), kTolerance); \
102
+ EXPECT_NEAR(pixel.green, (g), kTolerance); \
103
+ EXPECT_NEAR(pixel.blue, (b), kTolerance); \
104
+ EXPECT_NEAR(pixel.alpha, (a), kTolerance); \
105
+ } while (false)
106
+
107
+ #define EXPECT_TRANSPARENT(pixels, x, y) EXPECT_NEAR((pixels).at((x), (y)).alpha, 0, kTolerance)
108
+
109
+ TEST(win32_paint_puts_a_child_at_its_frame) {
110
+ Tree tree;
111
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
112
+ root->insertChild(tree.colouredBox(2, 20, 30, 40, 10, 1.0f, 0.0f, 0.0f), 0);
113
+
114
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
115
+ EXPECT(!pixels.empty());
116
+
117
+ EXPECT_PIXEL(pixels, 25, 35, 255, 0, 0, 255);
118
+ EXPECT_TRANSPARENT(pixels, 5, 5);
119
+ EXPECT_TRANSPARENT(pixels, 65, 35);
120
+ EXPECT_TRANSPARENT(pixels, 25, 45);
121
+ }
122
+
123
+ TEST(win32_paint_composites_opacity_over_the_whole_subtree) {
124
+ Tree tree;
125
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
126
+ RnWin32View *faded = tree.colouredBox(2, 0, 0, 60, 60, 1.0f, 0.0f, 0.0f);
127
+ faded->setOpacity(0.4f);
128
+ root->insertChild(faded, 0);
129
+
130
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
131
+ EXPECT(!pixels.empty());
132
+
133
+ // Still fully red, at 40% coverage. An opacity folded into the brush's alpha
134
+ // would look the same here and wrong the moment two children overlap, which
135
+ // is what the layer in RnWin32View::paint is for.
136
+ EXPECT_PIXEL(pixels, 30, 30, 255, 0, 0, 102);
137
+ }
138
+
139
+ TEST(win32_paint_clips_children_only_when_overflow_is_hidden) {
140
+ Tree tree;
141
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
142
+ RnWin32View *parent = tree.box(2, 0, 0, 50, 50);
143
+ parent->insertChild(tree.colouredBox(3, 0, 0, 100, 100, 0.0f, 1.0f, 0.0f), 0);
144
+ root->insertChild(parent, 0);
145
+
146
+ const RnPixels visible = basalt::win32::renderToPixels(*root);
147
+ EXPECT(!visible.empty());
148
+ EXPECT_PIXEL(visible, 25, 25, 0, 255, 0, 255);
149
+ // overflow: visible is the default, so the child escapes its parent's box.
150
+ EXPECT_PIXEL(visible, 75, 25, 0, 255, 0, 255);
151
+
152
+ parent->setClipsChildren(true);
153
+ const RnPixels clipped = basalt::win32::renderToPixels(*root);
154
+ EXPECT(!clipped.empty());
155
+ EXPECT_PIXEL(clipped, 25, 25, 0, 255, 0, 255);
156
+ EXPECT_TRANSPARENT(clipped, 75, 25);
157
+ }
158
+
159
+ TEST(win32_paint_rotates_about_the_centre_and_in_the_right_direction) {
160
+ Tree tree;
161
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
162
+ // Deliberately wide and short, so a quarter turn changes which pixels are
163
+ // covered rather than only which way up they are.
164
+ RnWin32View *card = tree.colouredBox(2, 10, 40, 80, 20, 0.0f, 0.0f, 1.0f);
165
+ // A marker in the card's top-left corner. Without it a quarter turn and a
166
+ // three-quarter turn are indistinguishable, because the card is symmetric
167
+ // under a half turn -- and the difference between them is exactly the
168
+ // transpose that no test on an axis-aligned box can see.
169
+ card->insertChild(tree.colouredBox(3, 0, 0, 10, 10, 1.0f, 1.0f, 1.0f), 0);
170
+ root->insertChild(card, 0);
171
+
172
+ const RnPixels upright = basalt::win32::renderToPixels(*root);
173
+ EXPECT(!upright.empty());
174
+ // The card spans x 10..90, y 40..60; the marker is its corner, x 10..20,
175
+ // y 40..50.
176
+ EXPECT_PIXEL(upright, 30, 50, 0, 0, 255, 255);
177
+ EXPECT_PIXEL(upright, 15, 45, 255, 255, 255, 255);
178
+ EXPECT_TRANSPARENT(upright, 50, 20);
179
+
180
+ // CSS rotate(90deg), in matrix3d's column-major order: m11=0, m12=1, m21=-1,
181
+ // m22=0. On screen, with y downwards, that turns clockwise.
182
+ const float quarterTurn[16] = {
183
+ 0, 1, 0, 0, //
184
+ -1, 0, 0, 0, //
185
+ 0, 0, 1, 0, //
186
+ 0, 0, 0, 1};
187
+ card->setTransform(quarterTurn);
188
+
189
+ const RnPixels turned = basalt::win32::renderToPixels(*root);
190
+ EXPECT(!turned.empty());
191
+
192
+ // The card's centre is (50,50) and it is now 20 wide and 80 tall about it, so
193
+ // the pixels it covers have swapped: (50,60) is inside and (20,50) is not.
194
+ EXPECT_PIXEL(turned, 50, 60, 0, 0, 255, 255);
195
+ EXPECT_TRANSPARENT(turned, 20, 50);
196
+
197
+ // And the marker has swung to x 50..60, y 10..20 -- clockwise. The transposed
198
+ // matrix, which is the mistake no axis-aligned test can see, would put it at
199
+ // x 40..50, y 80..90 instead. Both ends are asserted, because the card covers
200
+ // both and only the marker's colour tells them apart.
201
+ EXPECT_PIXEL(turned, 55, 15, 255, 255, 255, 255);
202
+ EXPECT_PIXEL(turned, 45, 85, 0, 0, 255, 255);
203
+ }
204
+
205
+ TEST(win32_paint_orders_children_by_z_index) {
206
+ Tree tree;
207
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
208
+ RnWin32View *under = tree.colouredBox(2, 0, 0, 50, 50, 1.0f, 0.0f, 0.0f);
209
+ RnWin32View *over = tree.colouredBox(3, 0, 0, 50, 50, 0.0f, 1.0f, 0.0f);
210
+ root->insertChild(under, 0);
211
+ root->insertChild(over, 1);
212
+
213
+ // Document order: the later child wins.
214
+ const RnPixels ordered = basalt::win32::renderToPixels(*root);
215
+ EXPECT(!ordered.empty());
216
+ EXPECT_PIXEL(ordered, 25, 25, 0, 255, 0, 255);
217
+
218
+ // A negative zIndex on the later child puts it underneath, and the child list
219
+ // does not move -- which test_win32_view.cpp asserts separately, because
220
+ // Fabric indexes into that list.
221
+ over->setZIndex(-1);
222
+ const RnPixels restacked = basalt::win32::renderToPixels(*root);
223
+ EXPECT(!restacked.empty());
224
+ EXPECT_PIXEL(restacked, 25, 25, 255, 0, 0, 255);
225
+ }
226
+
227
+ TEST(win32_paint_moves_children_by_the_scroll_offset) {
228
+ Tree tree;
229
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
230
+ RnWin32View *row = tree.colouredBox(2, 0, 50, 100, 20, 1.0f, 0.0f, 0.0f);
231
+ root->insertChild(row, 0);
232
+
233
+ const RnPixels unscrolled = basalt::win32::renderToPixels(*root);
234
+ EXPECT(!unscrolled.empty());
235
+ EXPECT_PIXEL(unscrolled, 50, 55, 255, 0, 0, 255);
236
+ EXPECT_TRANSPARENT(unscrolled, 50, 25);
237
+
238
+ // Scrolled down by 30, the row that was at y=50 is drawn at y=20 -- and its
239
+ // frame has not changed, which is the invariant: Yoga decides where things
240
+ // are and the offset is applied at paint time.
241
+ root->setScrollOffset(0, 30);
242
+ const RnPixels scrolled = basalt::win32::renderToPixels(*root);
243
+ EXPECT(!scrolled.empty());
244
+ EXPECT_PIXEL(scrolled, 50, 25, 255, 0, 0, 255);
245
+ EXPECT_TRANSPARENT(scrolled, 50, 55);
246
+ EXPECT_NEAR(row->frame().y, 50.0, 0.001);
247
+ }
248
+
249
+ TEST(win32_paint_rounds_the_background_to_the_corner_radius) {
250
+ Tree tree;
251
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
252
+ RnWin32View *rounded = tree.colouredBox(2, 0, 0, 100, 100, 1.0f, 0.0f, 0.0f);
253
+ rounded->setCornerRadius(40.0f);
254
+ root->insertChild(rounded, 0);
255
+
256
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
257
+ EXPECT(!pixels.empty());
258
+
259
+ // The middle is filled and the corner is not, which is the whole claim. The
260
+ // background is clipped to the radius whether or not children are.
261
+ EXPECT_PIXEL(pixels, 50, 50, 255, 0, 0, 255);
262
+ EXPECT_TRANSPARENT(pixels, 2, 2);
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Per-corner radii and borders
267
+ //
268
+ // This host drew one circular radius -- the top-left horizontal one -- and no
269
+ // border at all, while GTK and AppKit drew both. The tree dump said so, which
270
+ // is how scripts/compare_hosts.sh caught it; these say what reached the pixels.
271
+ // ---------------------------------------------------------------------------
272
+
273
+ TEST(win32_paint_rounds_each_corner_on_its_own) {
274
+ Tree tree;
275
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
276
+ RnWin32View *box = tree.colouredBox(2, 0, 0, 100, 100, 1.0f, 0.0f, 0.0f);
277
+ // Top-left and bottom-right rounded, the other two square: what
278
+ // borderTopLeftRadius and borderBottomRightRadius alone ask for.
279
+ const float radii[8] = {40, 40, 0, 0, 40, 40, 0, 0};
280
+ box->setCornerRadii(radii);
281
+ root->insertChild(box, 0);
282
+
283
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
284
+ EXPECT(!pixels.empty());
285
+ EXPECT_TRANSPARENT(pixels, 3, 3);
286
+ EXPECT_TRANSPARENT(pixels, 96, 96);
287
+ // The two square corners are still there, which one radius for all four
288
+ // could never have produced.
289
+ EXPECT_PIXEL(pixels, 97, 2, 255, 0, 0, 255);
290
+ EXPECT_PIXEL(pixels, 2, 97, 255, 0, 0, 255);
291
+ EXPECT_PIXEL(pixels, 50, 50, 255, 0, 0, 255);
292
+ }
293
+
294
+ TEST(win32_paint_rounds_a_corner_elliptically) {
295
+ Tree tree;
296
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
297
+ RnWin32View *box = tree.colouredBox(2, 0, 0, 100, 100, 1.0f, 0.0f, 0.0f);
298
+ // 60 across and 20 down at the top-left. (15,3) is inside a 20-radius circle
299
+ // and outside this ellipse; (50,15) is inside the ellipse. Only an elliptical
300
+ // corner answers both.
301
+ const float radii[8] = {60, 20, 0, 0, 0, 0, 0, 0};
302
+ box->setCornerRadii(radii);
303
+ root->insertChild(box, 0);
304
+
305
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
306
+ EXPECT(!pixels.empty());
307
+ EXPECT_TRANSPARENT(pixels, 15, 3);
308
+ EXPECT_PIXEL(pixels, 50, 15, 255, 0, 0, 255);
309
+ }
310
+
311
+ TEST(win32_paint_draws_a_border_inside_the_box) {
312
+ Tree tree;
313
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
314
+ RnWin32View *box = tree.colouredBox(2, 0, 0, 100, 100, 0.0f, 0.0f, 1.0f);
315
+ const float widths[4] = {10, 10, 10, 10};
316
+ const float colours[16] = {1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1};
317
+ box->setBorders(widths, colours);
318
+ root->insertChild(box, 0);
319
+
320
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
321
+ EXPECT(!pixels.empty());
322
+ // Inside the frame on every side -- the border is part of the box, which is
323
+ // what Yoga already inset the content by -- and the middle untouched.
324
+ EXPECT_PIXEL(pixels, 50, 4, 255, 0, 0, 255);
325
+ EXPECT_PIXEL(pixels, 95, 50, 255, 0, 0, 255);
326
+ EXPECT_PIXEL(pixels, 50, 95, 255, 0, 0, 255);
327
+ EXPECT_PIXEL(pixels, 4, 50, 255, 0, 0, 255);
328
+ EXPECT_PIXEL(pixels, 50, 50, 0, 0, 255, 255);
329
+ }
330
+
331
+ TEST(win32_paint_colours_each_edge_and_mitres_the_corners) {
332
+ Tree tree;
333
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
334
+ RnWin32View *box = tree.box(2, 0, 0, 100, 100);
335
+ const float widths[4] = {10, 10, 10, 10};
336
+ // Top red, right green, bottom blue, left yellow.
337
+ const float colours[16] = {1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1};
338
+ box->setBorders(widths, colours);
339
+ root->insertChild(box, 0);
340
+
341
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
342
+ EXPECT(!pixels.empty());
343
+ EXPECT_PIXEL(pixels, 50, 4, 255, 0, 0, 255);
344
+ EXPECT_PIXEL(pixels, 95, 50, 0, 255, 0, 255);
345
+ EXPECT_PIXEL(pixels, 50, 95, 0, 0, 255, 255);
346
+ EXPECT_PIXEL(pixels, 4, 50, 255, 255, 0, 255);
347
+ // The top-left corner is split down its diagonal, as CSS splits it: above the
348
+ // line from (0,0) to (10,10) is the top edge's, below it the left edge's.
349
+ EXPECT_PIXEL(pixels, 7, 2, 255, 0, 0, 255);
350
+ EXPECT_PIXEL(pixels, 2, 7, 255, 255, 0, 255);
351
+ // No background, so the middle is nothing.
352
+ EXPECT_TRANSPARENT(pixels, 50, 50);
353
+ }
354
+
355
+ TEST(win32_paint_draws_the_border_over_the_children) {
356
+ Tree tree;
357
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
358
+ RnWin32View *parent = tree.box(2, 0, 0, 100, 100);
359
+ const float widths[4] = {10, 10, 10, 10};
360
+ const float colours[16] = {1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1};
361
+ parent->setBorders(widths, colours);
362
+ root->insertChild(parent, 0);
363
+ // A child covering the whole box, border included: the border still shows,
364
+ // because it is painted after the content on every platform.
365
+ parent->insertChild(tree.colouredBox(3, 0, 0, 100, 100, 0.0f, 1.0f, 0.0f), 0);
366
+
367
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
368
+ EXPECT(!pixels.empty());
369
+ EXPECT_PIXEL(pixels, 50, 4, 255, 0, 0, 255);
370
+ EXPECT_PIXEL(pixels, 50, 50, 0, 255, 0, 255);
371
+ }
372
+
373
+ TEST(win32_paint_clips_children_to_each_corner) {
374
+ Tree tree;
375
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
376
+ RnWin32View *parent = tree.box(2, 0, 0, 100, 100);
377
+ const float radii[8] = {40, 40, 0, 0, 0, 0, 0, 0};
378
+ parent->setCornerRadii(radii);
379
+ parent->setClipsChildren(true);
380
+ root->insertChild(parent, 0);
381
+ parent->insertChild(tree.colouredBox(3, 0, 0, 100, 100, 0.0f, 1.0f, 0.0f), 0);
382
+
383
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
384
+ EXPECT(!pixels.empty());
385
+ // overflow: hidden cuts the child at the rounded corner and nowhere else.
386
+ EXPECT_TRANSPARENT(pixels, 3, 3);
387
+ EXPECT_PIXEL(pixels, 97, 3, 0, 255, 0, 255);
388
+ EXPECT_PIXEL(pixels, 3, 97, 0, 255, 0, 255);
389
+ }
390
+
391
+ TEST(win32_describe_prints_radii_and_borders_as_gtk_does) {
392
+ // The line e2e/index.js's header produces on Linux, which is the one the
393
+ // cross-host comparison failed on: two rounded corners, one colour on three
394
+ // edges and another on the fourth.
395
+ RnWin32View view(16);
396
+ view.setFrame(0, 0, 50, 50);
397
+ const float radii[8] = {18, 18, 0, 0, 18, 18, 0, 0};
398
+ view.setCornerRadii(radii);
399
+ const float widths[4] = {4, 4, 4, 4};
400
+ const float gold[4] = {242 / 255.0f, 193 / 255.0f, 78 / 255.0f, 1.0f};
401
+ const float coral[4] = {242 / 255.0f, 111 / 255.0f, 86 / 255.0f, 1.0f};
402
+ float colours[16];
403
+ for (int c = 0; c < 4; c++) {
404
+ colours[0 + c] = gold[c];
405
+ colours[4 + c] = gold[c];
406
+ colours[8 + c] = gold[c];
407
+ colours[12 + c] = coral[c];
408
+ }
409
+ view.setBorders(widths, colours);
410
+
411
+ const std::string tree = view.describeTree();
412
+ EXPECT(tree.find(" radii=(18,18,0,0,18,18,0,0)") != std::string::npos);
413
+ EXPECT(tree.find(" borderw=(4,4,4,4) borderc=(#f2c14eff,#f2c14eff,#f2c14eff,#f26f56ff)") !=
414
+ std::string::npos);
415
+
416
+ // And a border that cannot be seen is not a border: GTK prints none for it,
417
+ // so neither does this.
418
+ const float clear[16] = {};
419
+ view.setBorders(widths, clear);
420
+ EXPECT(view.describeTree().find("borderw=") == std::string::npos);
421
+ }
422
+
423
+ TEST(win32_paint_gives_a_painter_its_views_box) {
424
+ Tree tree;
425
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
426
+ RnWin32View *canvas = tree.box(2, 20, 30, 40, 10);
427
+ auto painter = std::make_shared<FillPainter>(1.0f, 0.0f, 0.0f);
428
+ canvas->setPainter(painter);
429
+ root->insertChild(canvas, 0);
430
+
431
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
432
+ EXPECT(!pixels.empty());
433
+
434
+ // Once, with the frame Yoga resolved -- not the root's, and not in pixels.
435
+ EXPECT_EQ(painter->calls, 1);
436
+ EXPECT_NEAR(painter->lastWidth, 40.0, 0.01);
437
+ EXPECT_NEAR(painter->lastHeight, 10.0, 0.01);
438
+
439
+ // And at the view's origin, so what it drew at 0,0 lands at 20,30.
440
+ EXPECT_PIXEL(pixels, 25, 35, 255, 0, 0, 255);
441
+ EXPECT_TRANSPARENT(pixels, 5, 5);
442
+ EXPECT_TRANSPARENT(pixels, 65, 35);
443
+ }
444
+
445
+ TEST(win32_paint_clips_and_fades_a_painter_with_the_views_above_it) {
446
+ // The whole argument for drawing a <Canvas> in the walk instead of giving it
447
+ // a surface of its own: it is clipped and faded by its ancestors without
448
+ // knowing that any of them exist. A sibling layer would need every one of
449
+ // these reimplemented, which is where the other toolkits' bugs live.
450
+ Tree tree;
451
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
452
+ RnWin32View *clip = tree.box(2, 0, 0, 50, 50);
453
+ clip->setClipsChildren(true);
454
+ clip->setOpacity(0.4f);
455
+ RnWin32View *canvas = tree.box(3, 0, 0, 100, 100);
456
+ canvas->setPainter(std::make_shared<FillPainter>(0.0f, 1.0f, 0.0f));
457
+ clip->insertChild(canvas, 0);
458
+ root->insertChild(clip, 0);
459
+
460
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
461
+ EXPECT(!pixels.empty());
462
+
463
+ // Inside the clip: green at 40%.
464
+ EXPECT_PIXEL(pixels, 25, 25, 0, 255, 0, 102);
465
+ // Outside it: nothing, though the painter filled all hundred points.
466
+ EXPECT_TRANSPARENT(pixels, 75, 25);
467
+ }
468
+
469
+ TEST(win32_paint_scrolls_a_painter_with_the_content_around_it) {
470
+ Tree tree;
471
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
472
+ RnWin32View *scroller = tree.box(2, 0, 0, 100, 100);
473
+ RnWin32View *canvas = tree.box(3, 0, 40, 100, 20);
474
+ canvas->setPainter(std::make_shared<FillPainter>(0.0f, 0.0f, 1.0f));
475
+ scroller->insertChild(canvas, 0);
476
+ root->insertChild(scroller, 0);
477
+
478
+ const RnPixels still = basalt::win32::renderToPixels(*root);
479
+ EXPECT(!still.empty());
480
+ EXPECT_PIXEL(still, 50, 45, 0, 0, 255, 255);
481
+
482
+ scroller->setScrollOffset(0, 30);
483
+ const RnPixels scrolled = basalt::win32::renderToPixels(*root);
484
+ EXPECT(!scrolled.empty());
485
+ // Up by thirty: the painter is not told, and does not need to be.
486
+ EXPECT_PIXEL(scrolled, 50, 15, 0, 0, 255, 255);
487
+ EXPECT_TRANSPARENT(scrolled, 50, 45);
488
+ }
489
+
490
+ TEST(win32_paint_skips_a_painter_on_a_hidden_view) {
491
+ Tree tree;
492
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
493
+ RnWin32View *canvas = tree.box(2, 0, 0, 100, 100);
494
+ auto painter = std::make_shared<FillPainter>(1.0f, 1.0f, 0.0f);
495
+ canvas->setPainter(painter);
496
+ canvas->setHidden(true);
497
+ root->insertChild(canvas, 0);
498
+
499
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
500
+ EXPECT(!pixels.empty());
501
+
502
+ // `display: none` reaches the painter as not being called at all, which for a
503
+ // canvas is the difference between idle and rendering a frame nobody sees.
504
+ EXPECT_EQ(painter->calls, 0);
505
+ EXPECT_TRANSPARENT(pixels, 50, 50);
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // CSS's outline: `outlineWidth`, `outlineColor`, `outlineOffset` and
510
+ // `outlineStyle`.
511
+ //
512
+ // The same five questions tests/test_gtk_paint.cpp asks, in the same order and
513
+ // with the same geometry, because the ring has to land in the same place on
514
+ // both: it is drawn *outside* the box and takes no layout space, so what proves
515
+ // it is ink beyond the view's own frame with the frame unchanged. Web-ported
516
+ // code sets it for a focus ring and expects exactly that.
517
+ // ---------------------------------------------------------------------------
518
+
519
+ TEST(win32_paint_draws_an_outline_outside_the_box) {
520
+ Tree tree;
521
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
522
+ RnWin32View *view = tree.colouredBox(2, 30, 30, 40, 40, 1.0f, 1.0f, 1.0f);
523
+ root->insertChild(view, 0);
524
+ const float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
525
+ view->setOutline(4.0f, 0.0f, red, RnWin32View::LineStyle::Solid);
526
+
527
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
528
+ EXPECT(!pixels.empty());
529
+ // Two points outside the box's left edge: inside a four point outline.
530
+ EXPECT_PIXEL(pixels, 28, 50, 255, 0, 0, 255);
531
+ // And on every other side, so a ring rather than one edge.
532
+ EXPECT_PIXEL(pixels, 72, 50, 255, 0, 0, 255);
533
+ EXPECT_PIXEL(pixels, 50, 28, 255, 0, 0, 255);
534
+ EXPECT_PIXEL(pixels, 50, 72, 255, 0, 0, 255);
535
+
536
+ // The box itself is untouched: an outline that ate into the view would be a
537
+ // border, and the two are different props.
538
+ EXPECT_PIXEL(pixels, 50, 50, 255, 255, 255, 255);
539
+ EXPECT_PIXEL(pixels, 32, 50, 255, 255, 255, 255);
540
+ // Past the outline, nothing.
541
+ EXPECT_TRANSPARENT(pixels, 20, 50);
542
+ }
543
+
544
+ // `outlineOffset` is the gap between the box and the ring, which is what makes a
545
+ // focus ring look deliberate rather than like a second border.
546
+ TEST(win32_paint_an_outline_offset_leaves_a_gap) {
547
+ Tree tree;
548
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
549
+ RnWin32View *view = tree.colouredBox(2, 30, 30, 40, 40, 1.0f, 1.0f, 1.0f);
550
+ root->insertChild(view, 0);
551
+ const float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
552
+ view->setOutline(4.0f, 6.0f, red, RnWin32View::LineStyle::Solid);
553
+
554
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
555
+ // The gap: three points out from the edge, between the box and the ring.
556
+ EXPECT_TRANSPARENT(pixels, 27, 50);
557
+ // The ring itself, eight points out.
558
+ EXPECT_PIXEL(pixels, 22, 50, 255, 0, 0, 255);
559
+ // And past it again.
560
+ EXPECT_TRANSPARENT(pixels, 16, 50);
561
+ }
562
+
563
+ // A dashed outline leaves gaps along the ring, the way a dashed border does.
564
+ // Direct2D's dash lengths are multiples of the stroke width, where the other two
565
+ // hosts' arrays are absolute, so this is the test that would catch a pattern
566
+ // converted into the wrong units: four times too long is a ring with no gaps.
567
+ TEST(win32_paint_a_dashed_outline_is_not_solid) {
568
+ Tree tree;
569
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
570
+ RnWin32View *view = tree.colouredBox(2, 20, 20, 60, 60, 1.0f, 1.0f, 1.0f);
571
+ root->insertChild(view, 0);
572
+ const float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
573
+
574
+ view->setOutline(4.0f, 0.0f, red, RnWin32View::LineStyle::Solid);
575
+ const RnPixels solid = basalt::win32::renderToPixels(*root);
576
+ int solidInk = 0;
577
+ for (unsigned x = 0; x < solid.width(); x++) {
578
+ if (solid.at(x, 18).alpha > 40) {
579
+ solidInk++;
580
+ }
581
+ }
582
+
583
+ view->setOutline(4.0f, 0.0f, red, RnWin32View::LineStyle::Dashed);
584
+ const RnPixels dashed = basalt::win32::renderToPixels(*root);
585
+ int dashedInk = 0;
586
+ for (unsigned x = 0; x < dashed.width(); x++) {
587
+ if (dashed.at(x, 18).alpha > 40) {
588
+ dashedInk++;
589
+ }
590
+ }
591
+
592
+ // Ink either way, and less of it when it is dashed.
593
+ EXPECT(solidInk > 50);
594
+ EXPECT(dashedInk > 0);
595
+ EXPECT(dashedInk < solidInk);
596
+ }
597
+
598
+ TEST(win32_paint_no_outline_width_is_no_outline) {
599
+ Tree tree;
600
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
601
+ RnWin32View *view = tree.colouredBox(2, 30, 30, 40, 40, 1.0f, 1.0f, 1.0f);
602
+ root->insertChild(view, 0);
603
+ const float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
604
+
605
+ view->setOutline(4.0f, 0.0f, red, RnWin32View::LineStyle::Solid);
606
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 28, 50, 255, 0, 0, 255);
607
+
608
+ // Taken away again, which is what losing focus does.
609
+ view->setOutline(0.0f, 0.0f, red, RnWin32View::LineStyle::Solid);
610
+ EXPECT_TRANSPARENT(basalt::win32::renderToPixels(*root), 28, 50);
611
+
612
+ // And a width with no colour paints nothing: React Native's default outline
613
+ // colour is undefined, which arrives as a transparent one.
614
+ const float invisible[4] = {0.0f, 0.0f, 0.0f, 0.0f};
615
+ view->setOutline(4.0f, 0.0f, invisible, RnWin32View::LineStyle::Solid);
616
+ EXPECT_TRANSPARENT(basalt::win32::renderToPixels(*root), 28, 50);
617
+ }
618
+
619
+ // The ring follows the view's corners, grown by the width and the offset so it
620
+ // stays concentric: a square ring around a rounded card is the giveaway that the
621
+ // radii were not carried over.
622
+ TEST(win32_paint_an_outline_follows_the_corner_radius) {
623
+ Tree tree;
624
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
625
+ RnWin32View *view = tree.colouredBox(2, 20, 20, 60, 60, 1.0f, 1.0f, 1.0f);
626
+ root->insertChild(view, 0);
627
+ const float radii[8] = {20, 20, 20, 20, 20, 20, 20, 20};
628
+ view->setCornerRadii(radii);
629
+ const float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
630
+ view->setOutline(4.0f, 0.0f, red, RnWin32View::LineStyle::Solid);
631
+
632
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
633
+ // The middle of an edge is on the ring.
634
+ EXPECT_PIXEL(pixels, 18, 50, 255, 0, 0, 255);
635
+ // The corner of the bounding box is outside a 22 point radius, so it is
636
+ // empty -- where a square ring would paint it.
637
+ EXPECT_TRANSPARENT(pixels, 17, 17);
638
+ }
639
+
640
+ // And all four numbers are in the dump, spelled as the other two hosts spell
641
+ // them, because that line is what the end-to-end scenario reads on all three.
642
+ TEST(win32_describe_prints_the_outline_as_gtk_does) {
643
+ Tree tree;
644
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
645
+ RnWin32View *view = tree.box(2, 10, 10, 40, 40);
646
+ root->insertChild(view, 0);
647
+
648
+ EXPECT(root->describeTree().find("outline=") == std::string::npos);
649
+
650
+ const float colour[4] = {224.0f / 255.0f, 72.0f / 255.0f, 77.0f / 255.0f, 1.0f};
651
+ view->setOutline(3.0f, 2.0f, colour, RnWin32View::LineStyle::Dashed);
652
+ EXPECT(root->describeTree().find("outline=(3,2,#e0484dff,dashed)") != std::string::npos);
653
+
654
+ // Solid prints no style, which is what keeps the common case short and is
655
+ // what the other two hosts do.
656
+ view->setOutline(3.0f, 2.0f, colour, RnWin32View::LineStyle::Solid);
657
+ EXPECT(root->describeTree().find("outline=(3,2,#e0484dff)") != std::string::npos);
658
+ }
659
+
660
+ // ---------------------------------------------------------------------------
661
+ // `borderStyle`, dotted and dashed.
662
+ //
663
+ // One stroked path around the rounded box rather than four filled edges, which
664
+ // is what both other hosts do and why the style belongs to the whole border:
665
+ // a stroked path carries one dash pattern.
666
+ // ---------------------------------------------------------------------------
667
+
668
+ TEST(win32_paint_a_dashed_border_is_not_solid) {
669
+ Tree tree;
670
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
671
+ RnWin32View *box = tree.box(2, 0, 0, 100, 100);
672
+ const float widths[4] = {4, 4, 4, 4};
673
+ const float colours[16] = {1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1};
674
+ box->setBorders(widths, colours);
675
+ root->insertChild(box, 0);
676
+
677
+ const auto inkAlongTheTop = [&]() {
678
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
679
+ int ink = 0;
680
+ for (unsigned x = 0; x < pixels.width(); x++) {
681
+ if (pixels.at(x, 2).alpha > 40) {
682
+ ink++;
683
+ }
684
+ }
685
+ return ink;
686
+ };
687
+
688
+ const int solid = inkAlongTheTop();
689
+ box->setBorderStyle(RnWin32View::LineStyle::Dashed);
690
+ const int dashed = inkAlongTheTop();
691
+ box->setBorderStyle(RnWin32View::LineStyle::Dotted);
692
+ const int dotted = inkAlongTheTop();
693
+
694
+ // Ink in all three, and less of it once there are gaps.
695
+ EXPECT(solid > 80);
696
+ EXPECT(dashed > 0);
697
+ EXPECT(dashed < solid);
698
+ EXPECT(dotted > 0);
699
+ EXPECT(dotted < solid);
700
+ // Dots leave more gap than dashes: three widths on and two off against zero
701
+ // on and two off. A pattern converted into the wrong units would make these
702
+ // two the same, or make both solid.
703
+ EXPECT(dotted < dashed);
704
+ }
705
+
706
+ // The stroke stays inside the box, which is the half that a straddling stroke
707
+ // gets wrong: a 4pt dashed border drawn on the box's own edge would paint two
708
+ // points outside the view and overlap its neighbour.
709
+ TEST(win32_paint_a_dashed_border_stays_inside_the_box) {
710
+ Tree tree;
711
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
712
+ RnWin32View *box = tree.box(2, 20, 20, 60, 60);
713
+ const float widths[4] = {4, 4, 4, 4};
714
+ const float colours[16] = {1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1};
715
+ box->setBorders(widths, colours);
716
+ box->setBorderStyle(RnWin32View::LineStyle::Dashed);
717
+ root->insertChild(box, 0);
718
+
719
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
720
+ // Nothing outside the frame on any side. A dash can fall anywhere along the
721
+ // path, so this counts a band rather than asking about one pixel.
722
+ int outside = 0;
723
+ for (unsigned x = 0; x < pixels.width(); x++) {
724
+ for (unsigned y = 0; y < pixels.height(); y++) {
725
+ const bool insideFrame = x >= 20 && x < 80 && y >= 20 && y < 80;
726
+ if (!insideFrame && pixels.at(x, y).alpha > 40) {
727
+ outside++;
728
+ }
729
+ }
730
+ }
731
+ EXPECT_EQ(outside, 0);
732
+ }
733
+
734
+ TEST(win32_describe_prints_the_border_style_as_gtk_does) {
735
+ Tree tree;
736
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
737
+ RnWin32View *box = tree.box(2, 10, 10, 40, 40);
738
+ const float widths[4] = {2, 2, 2, 2};
739
+ const float colours[16] = {1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1};
740
+ box->setBorders(widths, colours);
741
+ root->insertChild(box, 0);
742
+
743
+ // Solid prints nothing, which keeps the common case short and is what the
744
+ // other two hosts do.
745
+ EXPECT(root->describeTree().find("border-style=") == std::string::npos);
746
+
747
+ box->setBorderStyle(RnWin32View::LineStyle::Dashed);
748
+ EXPECT(root->describeTree().find("border-style=dashed") != std::string::npos);
749
+ box->setBorderStyle(RnWin32View::LineStyle::Dotted);
750
+ EXPECT(root->describeTree().find("border-style=dotted") != std::string::npos);
751
+ }
752
+
753
+ // ---------------------------------------------------------------------------
754
+ // `filter`, which is an effect graph over the view's own picture.
755
+ //
756
+ // CSS applies a filter to an element and its descendants, so the assertions
757
+ // below are about what reached the pixels after the subtree was drawn: a colour
758
+ // matrix that recolours a child as well as its parent, a blur that spreads an
759
+ // edge, an opacity that multiplies the view's own, and a drop shadow cast by
760
+ // the result.
761
+ //
762
+ // The matrix test is deliberately asymmetric. Direct2D multiplies a row vector
763
+ // by its matrix where core's rows are outputs, so the matrix goes in
764
+ // transposed: a symmetric one would pass either way, and a channel rotation
765
+ // comes out blue when it is right and green when it is not.
766
+ // ---------------------------------------------------------------------------
767
+ namespace {
768
+
769
+ RnWin32View::Filters rotateChannels() {
770
+ // out_red = in_green, out_green = in_blue, out_blue = in_red.
771
+ RnWin32View::Filters filters;
772
+ filters.hasMatrix = true;
773
+ for (int i = 0; i < 16; i++) {
774
+ filters.matrix[i] = 0.0f;
775
+ }
776
+ filters.matrix[0 * 4 + 1] = 1.0f;
777
+ filters.matrix[1 * 4 + 2] = 1.0f;
778
+ filters.matrix[2 * 4 + 0] = 1.0f;
779
+ filters.matrix[3 * 4 + 3] = 1.0f;
780
+ return filters;
781
+ }
782
+
783
+ } // namespace
784
+
785
+ TEST(win32_paint_applies_a_filters_colour_matrix) {
786
+ Tree tree;
787
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
788
+ RnWin32View *box = tree.colouredBox(2, 0, 0, 60, 60, 1.0f, 0.0f, 0.0f);
789
+ box->setFilters(rotateChannels());
790
+ root->insertChild(box, 0);
791
+
792
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
793
+ EXPECT(!pixels.empty());
794
+ // Red in, blue out. Green out would mean the matrix went in untransposed,
795
+ // which is the one mistake this arrangement cannot hide.
796
+ EXPECT_PIXEL(pixels, 30, 30, 0, 0, 255, 255);
797
+ }
798
+
799
+ TEST(win32_paint_applies_a_filter_to_the_whole_subtree) {
800
+ Tree tree;
801
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
802
+ RnWin32View *parent = tree.box(2, 0, 0, 60, 60);
803
+ parent->setFilters(rotateChannels());
804
+ RnWin32View *child = tree.colouredBox(3, 10, 10, 40, 40, 1.0f, 0.0f, 0.0f);
805
+ parent->insertChild(child, 0);
806
+ root->insertChild(parent, 0);
807
+
808
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
809
+ // The child is red and is inside a filtered parent, so it comes out blue:
810
+ // that is what "and its descendants" means, and a host that filtered only
811
+ // its own background would leave this red.
812
+ EXPECT_PIXEL(pixels, 30, 30, 0, 0, 255, 255);
813
+ }
814
+
815
+ TEST(win32_paint_applies_a_filters_matrix_offset) {
816
+ Tree tree;
817
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
818
+ RnWin32View *box = tree.colouredBox(2, 0, 0, 60, 60, 0.0f, 0.0f, 0.0f);
819
+ RnWin32View::Filters filters;
820
+ filters.hasMatrix = true;
821
+ // Identity, plus all the green there is: `out = matrix * in + offset`, which
822
+ // is the fifth row of Direct2D's matrix rather than a fifth column.
823
+ filters.offset[1] = 1.0f;
824
+ box->setFilters(filters);
825
+ root->insertChild(box, 0);
826
+
827
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
828
+ EXPECT_PIXEL(pixels, 30, 30, 0, 255, 0, 255);
829
+ }
830
+
831
+ // The alpha mode, which is the other thing a colour matrix can get wrong.
832
+ //
833
+ // CSS defines its filters on unpremultiplied colour and Direct2D's default is
834
+ // premultiplied, so a matrix over a half-transparent view is visibly wrong the
835
+ // other way round: the red channel of a 50% red pixel is 0.5 premultiplied and
836
+ // 1.0 straight.
837
+ TEST(win32_paint_filters_in_straight_alpha) {
838
+ Tree tree;
839
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
840
+ RnWin32View *box = tree.box(2, 0, 0, 60, 60);
841
+ box->setBackgroundColor(1.0f, 0.0f, 0.0f, 0.5f, true);
842
+ RnWin32View::Filters filters;
843
+ filters.hasMatrix = true;
844
+ // Identity on colour, and alpha forced opaque: row three of the matrix is
845
+ // zeroed and the offset supplies the one.
846
+ for (int column = 0; column < 4; column++) {
847
+ filters.matrix[3 * 4 + column] = 0.0f;
848
+ }
849
+ filters.offset[3] = 1.0f;
850
+ box->setFilters(filters);
851
+ root->insertChild(box, 0);
852
+
853
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
854
+ // Full red at full alpha. Premultiplied would give a dark red, about 128.
855
+ EXPECT_PIXEL(pixels, 30, 30, 255, 0, 0, 255);
856
+ }
857
+
858
+ // The offscreen the graph runs over has to keep its alpha, which is the other
859
+ // half of the pair above and the one the *app* was getting wrong: a compatible
860
+ // render target inherits its parent's format, and the window's parent format is
861
+ // opaque where this suite's WIC bitmap is not. An identity matrix is enough to
862
+ // ask the question -- the only thing it can change is whether the alpha
863
+ // survived the trip through the bitmap.
864
+ TEST(win32_paint_a_filter_leaves_a_half_transparent_view_half_transparent) {
865
+ Tree tree;
866
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
867
+ RnWin32View *box = tree.box(2, 0, 0, 60, 60);
868
+ box->setBackgroundColor(1.0f, 0.0f, 0.0f, 0.5f, true);
869
+ RnWin32View::Filters filters;
870
+ // Identity, which the default already is: `hasMatrix` is what sends the view
871
+ // through the graph at all.
872
+ filters.hasMatrix = true;
873
+ box->setFilters(filters);
874
+ root->insertChild(box, 0);
875
+
876
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
877
+ // Red at half alpha, as it went in. An opaque offscreen would answer with the
878
+ // red already mixed into black: (128, 0, 0) at full alpha.
879
+ EXPECT_PIXEL(pixels, 30, 30, 255, 0, 0, 128);
880
+ }
881
+
882
+ TEST(win32_paint_applies_a_filters_blur) {
883
+ const auto edgeRamp = [](float radius) {
884
+ Tree tree;
885
+ RnWin32View *root = tree.box(1, 0, 0, 100, 40);
886
+ RnWin32View *parent = tree.box(2, 0, 0, 100, 40);
887
+ RnWin32View::Filters filters;
888
+ filters.blurRadius = radius;
889
+ parent->setFilters(filters);
890
+ // A hard edge down the middle: black on the left, nothing on the right.
891
+ RnWin32View *half = tree.colouredBox(3, 0, 0, 50, 40, 0.0f, 0.0f, 0.0f);
892
+ parent->insertChild(half, 0);
893
+ root->insertChild(parent, 0);
894
+
895
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
896
+ int mixed = 0;
897
+ for (unsigned x = 0; x < pixels.width(); x++) {
898
+ const int alpha = pixels.at(x, 20).alpha;
899
+ if (alpha > 24 && alpha < 232) {
900
+ mixed++;
901
+ }
902
+ }
903
+ return mixed;
904
+ };
905
+
906
+ const int sharp = edgeRamp(0.0f);
907
+ const int blurred = edgeRamp(10.0f);
908
+ EXPECT(sharp <= 4);
909
+ EXPECT(blurred > sharp + 5);
910
+ }
911
+
912
+ TEST(win32_paint_multiplies_the_views_opacity_by_the_filters) {
913
+ Tree tree;
914
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
915
+ RnWin32View *box = tree.colouredBox(2, 0, 0, 60, 60, 0.0f, 0.0f, 0.0f);
916
+ box->setOpacity(0.5f);
917
+ RnWin32View::Filters filters;
918
+ filters.opacity = 0.5f;
919
+ box->setFilters(filters);
920
+ root->insertChild(box, 0);
921
+
922
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
923
+ // CSS has two ways to ask for the same thing and an app can use both, so a
924
+ // quarter rather than a half.
925
+ EXPECT_NEAR(pixels.at(30, 30).alpha, 64, 6);
926
+ }
927
+
928
+ TEST(win32_paint_casts_a_filters_drop_shadow) {
929
+ Tree tree;
930
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
931
+ RnWin32View *box = tree.colouredBox(2, 20, 20, 40, 40, 1.0f, 1.0f, 1.0f);
932
+ RnWin32View::Filters filters;
933
+ RnWin32View::FilterShadow shadow;
934
+ shadow.dx = 12.0f;
935
+ shadow.dy = 12.0f;
936
+ shadow.standardDeviation = 0.0f;
937
+ shadow.colour[0] = 0.0f;
938
+ shadow.colour[1] = 0.0f;
939
+ shadow.colour[2] = 0.0f;
940
+ shadow.colour[3] = 1.0f;
941
+ filters.shadows.push_back(shadow);
942
+ box->setFilters(filters);
943
+ root->insertChild(box, 0);
944
+
945
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
946
+ // Down and right of the box, where the shadow landed: black.
947
+ EXPECT_PIXEL(pixels, 65, 65, 0, 0, 0, 255);
948
+ // The box itself is still white, so the shadow is behind it rather than over
949
+ // it.
950
+ EXPECT_PIXEL(pixels, 30, 30, 255, 255, 255, 255);
951
+ // And up and left of the box there is nothing: a shadow with an offset does
952
+ // not surround its caster.
953
+ EXPECT_TRANSPARENT(pixels, 10, 10);
954
+ }
955
+
956
+ TEST(win32_describe_prints_a_filter_as_gtk_does) {
957
+ Tree tree;
958
+ RnWin32View *root = tree.box(1, 0, 0, 60, 60);
959
+ RnWin32View *box = tree.box(2, 0, 0, 60, 60);
960
+ root->insertChild(box, 0);
961
+
962
+ EXPECT(root->describeTree().find("filter=") == std::string::npos);
963
+
964
+ // The probe is (1, 0.5, 0.25) and the matrix rotates the channels, so the
965
+ // answer is each channel's neighbour: green, blue, red.
966
+ box->setFilters(rotateChannels());
967
+ EXPECT(root->describeTree().find("filter=(probe=#8040ffff)") != std::string::npos);
968
+
969
+ RnWin32View::Filters pieces;
970
+ pieces.blurRadius = 8.0f;
971
+ pieces.opacity = 0.5f;
972
+ RnWin32View::FilterShadow shadow;
973
+ shadow.dx = 4.0f;
974
+ shadow.dy = 6.0f;
975
+ shadow.standardDeviation = 3.0f;
976
+ shadow.colour[3] = 0.5f;
977
+ pieces.shadows.push_back(shadow);
978
+ box->setFilters(pieces);
979
+ const std::string dump = root->describeTree();
980
+ EXPECT(dump.find("blur=8") != std::string::npos);
981
+ EXPECT(dump.find("opacity=0.5") != std::string::npos);
982
+ // The standard deviation React Native parsed, which is the number all three
983
+ // hosts are handed.
984
+ EXPECT(dump.find("shadow=(4,6,3,#00000080)") != std::string::npos);
985
+ }
986
+
987
+ // ---------------------------------------------------------------------------
988
+ // `mixBlendMode`, which is the one view prop whose whole observable is what it
989
+ // does to the pixels beneath it.
990
+ //
991
+ // A dump can say the keyword arrived; only a rendered picture can say the blend
992
+ // happened, and happened against the right backdrop. These are deliberately the
993
+ // same nine questions tests/test_gtk_paint.cpp asks, with the same arrangement
994
+ // and the same expected pixels: a mid grey child over a red parent, where the
995
+ // three answers that matter are far apart -- multiply is (128,0,0), screen is
996
+ // (255,128,128) and no blend at all is (128,128,128).
997
+ // ---------------------------------------------------------------------------
998
+ namespace {
999
+
1000
+ // A red parent with a grey child in the middle of it, which is what every blend
1001
+ // test here starts from. The child is returned; the parent is at (10,10), so a
1002
+ // pixel at (50,50) is inside both.
1003
+ RnWin32View *greyOnRed(Tree &tree, RnWin32View *root, const char *blend) {
1004
+ RnWin32View *parent = tree.colouredBox(2, 10, 10, 80, 80, 1.0f, 0.0f, 0.0f);
1005
+ RnWin32View *child = tree.colouredBox(3, 20, 20, 40, 40, 0.5f, 0.5f, 0.5f);
1006
+ parent->insertChild(child, 0);
1007
+ root->insertChild(parent, 0);
1008
+ child->setBlendMode(blend);
1009
+ return child;
1010
+ }
1011
+
1012
+ } // namespace
1013
+
1014
+ TEST(win32_paint_multiply_blends_a_child_with_its_parent) {
1015
+ Tree tree;
1016
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1017
+ greyOnRed(tree, root, "multiply");
1018
+
1019
+ // Half of red is half red, and the green and blue the child had are
1020
+ // multiplied away by the parent's zeroes. Unblended this pixel is grey.
1021
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 128, 0, 0, 255);
1022
+ }
1023
+
1024
+ // A second mode, because one mode can be hard-coded and still pass the first
1025
+ // test. Screen is multiply's opposite and lands where neither of the other two
1026
+ // answers is.
1027
+ TEST(win32_paint_screen_is_not_multiply) {
1028
+ Tree tree;
1029
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1030
+ greyOnRed(tree, root, "screen");
1031
+
1032
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 255, 128, 128, 255);
1033
+ }
1034
+
1035
+ // Which input is the backdrop, which multiply and screen cannot say: both are
1036
+ // symmetric, so a host that fed `CLSID_D2D1Blend` its two images the wrong way
1037
+ // round passes every test above. `color-dodge` is not symmetric -- it divides
1038
+ // the backdrop by the inverse of the source -- so the two orders give two
1039
+ // different pixels.
1040
+ TEST(win32_paint_a_blend_knows_which_input_is_the_backdrop) {
1041
+ Tree tree;
1042
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1043
+ greyOnRed(tree, root, "color-dodge");
1044
+
1045
+ // Red dodged by half grey: the red channel divides by a half and clamps, and
1046
+ // the other two are the parent's zeroes divided by anything, which is zero.
1047
+ // The other way round -- grey dodged by red -- is (255,128,128).
1048
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 255, 0, 0, 255);
1049
+ }
1050
+
1051
+ // The backdrop is everything beneath the child, not only its parent's
1052
+ // background. A blend that only saw the parent would come out half red here;
1053
+ // one that sees the sibling comes out half blue.
1054
+ TEST(win32_paint_a_blend_sees_the_siblings_beneath_it) {
1055
+ Tree tree;
1056
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1057
+ RnWin32View *parent = tree.colouredBox(2, 10, 10, 80, 80, 1.0f, 0.0f, 0.0f);
1058
+ RnWin32View *under = tree.colouredBox(3, 20, 20, 40, 40, 0.0f, 0.0f, 1.0f);
1059
+ RnWin32View *blended = tree.colouredBox(4, 20, 20, 40, 40, 0.5f, 0.5f, 0.5f);
1060
+ blended->setBlendMode("multiply");
1061
+ parent->insertChild(under, 0);
1062
+ parent->insertChild(blended, 1);
1063
+ root->insertChild(parent, 0);
1064
+
1065
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 0, 0, 128, 255);
1066
+ }
1067
+
1068
+ // The negative control, and the way back: `mixBlendMode: 'normal'` arrives as no
1069
+ // keyword at all, and a view that was blending has to stop -- which also means
1070
+ // the parent stops painting through an offscreen.
1071
+ TEST(win32_paint_no_blend_mode_is_no_blending) {
1072
+ Tree tree;
1073
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1074
+ RnWin32View *child = greyOnRed(tree, root, nullptr);
1075
+
1076
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 128, 128, 128, 255);
1077
+
1078
+ child->setBlendMode("multiply");
1079
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 128, 0, 0, 255);
1080
+
1081
+ child->setBlendMode(nullptr);
1082
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 128, 128, 128, 255);
1083
+ }
1084
+
1085
+ // The clip still clips, which is the case this implementation had to be careful
1086
+ // about: a clip here is a layer, a pushed layer holds what is drawn in an
1087
+ // intermediate surface, and the backdrop a blend reads is the target's own
1088
+ // bitmap. So the child is clipped where it is drawn rather than where it lands,
1089
+ // and the picture has to be the same picture.
1090
+ TEST(win32_paint_a_blended_child_is_still_clipped_by_overflow_hidden) {
1091
+ Tree tree;
1092
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1093
+ RnWin32View *parent = tree.colouredBox(2, 10, 10, 80, 80, 1.0f, 0.0f, 0.0f);
1094
+ parent->setClipsChildren(true);
1095
+ RnWin32View *child = tree.colouredBox(3, 40, 40, 60, 60, 0.5f, 0.5f, 0.5f);
1096
+ child->setBlendMode("multiply");
1097
+ parent->insertChild(child, 0);
1098
+ root->insertChild(parent, 0);
1099
+
1100
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1101
+ // Inside the parent, where the child is: blended.
1102
+ EXPECT_PIXEL(pixels, 70, 70, 128, 0, 0, 255);
1103
+ // Past the parent's edge, where the child would reach if nothing clipped it.
1104
+ EXPECT_PIXEL(pixels, 95, 95, 0, 0, 0, 0);
1105
+ }
1106
+
1107
+ // zIndex decides the backdrop, because it decides what is painted beneath. The
1108
+ // blended child here paints last, so the blue sibling is under it rather than
1109
+ // over it.
1110
+ TEST(win32_paint_z_index_decides_what_a_blend_blends_with) {
1111
+ Tree tree;
1112
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1113
+ RnWin32View *parent = tree.colouredBox(2, 10, 10, 80, 80, 1.0f, 0.0f, 0.0f);
1114
+ RnWin32View *blended = tree.colouredBox(3, 20, 20, 40, 40, 0.5f, 0.5f, 0.5f);
1115
+ blended->setBlendMode("multiply");
1116
+ RnWin32View *above = tree.colouredBox(4, 20, 20, 20, 20, 0.0f, 0.0f, 1.0f);
1117
+ parent->insertChild(blended, 0);
1118
+ parent->insertChild(above, 1);
1119
+ // The blue one is second in the list already; this says so out loud and moves
1120
+ // the blended one above it, so the two orders are not the same order.
1121
+ above->setZIndex(1);
1122
+ blended->setZIndex(2);
1123
+ root->insertChild(parent, 0);
1124
+
1125
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1126
+ // Where only the blended child is: half red, as ever.
1127
+ EXPECT_PIXEL(pixels, 55, 55, 128, 0, 0, 255);
1128
+ // Where both are, the blended one is on top of the blue one, so the blue one
1129
+ // is its backdrop: half blue.
1130
+ EXPECT_PIXEL(pixels, 40, 40, 0, 0, 128, 255);
1131
+ }
1132
+
1133
+ // Two blended children, in an order zIndex reversed. Each blends with what is
1134
+ // beneath it where it lands, so the two orders give two different pixels -- and
1135
+ // the blends have to happen in paint order rather than in insertion order,
1136
+ // which is what this is really asserting.
1137
+ TEST(win32_paint_two_blends_happen_in_paint_order) {
1138
+ Tree tree;
1139
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1140
+ RnWin32View *parent = tree.colouredBox(2, 10, 10, 80, 80, 1.0f, 0.0f, 0.0f);
1141
+ RnWin32View *grey = tree.colouredBox(3, 20, 20, 40, 40, 0.5f, 0.5f, 0.5f);
1142
+ grey->setBlendMode("multiply");
1143
+ grey->setZIndex(2);
1144
+ RnWin32View *blue = tree.colouredBox(4, 20, 20, 40, 40, 0.0f, 0.0f, 1.0f);
1145
+ blue->setBlendMode("screen");
1146
+ blue->setZIndex(1);
1147
+ parent->insertChild(grey, 0);
1148
+ parent->insertChild(blue, 1);
1149
+ root->insertChild(parent, 0);
1150
+
1151
+ // Blue first: screen of red and blue is magenta. Then grey: multiply of
1152
+ // magenta and half grey is half magenta. The other order ends in (128,0,255),
1153
+ // so the blue channel is what says which happened.
1154
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 128, 0, 128, 255);
1155
+ }
1156
+
1157
+ // A blended child with nothing beneath it, which the blend's two inputs make
1158
+ // worth asking about: the backdrop is empty.
1159
+ //
1160
+ // CSS answers this exactly -- the result is weighted by the backdrop's alpha, so
1161
+ // a transparent backdrop leaves the source colour untouched -- and the child has
1162
+ // to be drawn either way. **This is also the property the implementation leans
1163
+ // on**: the child's picture is clipped where it is drawn, and the blend result
1164
+ // is drawn back over the whole layer, which is only the same picture because a
1165
+ // blend with nothing on top is the backdrop and a blend over nothing is the top.
1166
+ TEST(win32_paint_a_blend_over_nothing_still_draws_the_child) {
1167
+ Tree tree;
1168
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1169
+ RnWin32View *parent = tree.box(2, 10, 10, 80, 80);
1170
+ RnWin32View *child = tree.colouredBox(3, 20, 20, 40, 40, 0.5f, 0.5f, 0.5f);
1171
+ child->setBlendMode("multiply");
1172
+ parent->insertChild(child, 0);
1173
+ root->insertChild(parent, 0);
1174
+
1175
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1176
+ // The child, unchanged: there was no backdrop to multiply with.
1177
+ EXPECT_PIXEL(pixels, 50, 50, 128, 128, 128, 255);
1178
+ // And beside it, where only the transparent parent is: still nothing. The
1179
+ // layer the subtree goes through must not turn into a box of its own.
1180
+ EXPECT_PIXEL(pixels, 15, 15, 0, 0, 0, 0);
1181
+ }
1182
+
1183
+ // `plus-lighter`, the one CSS mode GSK has no node for, which makes this host
1184
+ // the only one of the three with all seventeen. Direct2D's linear dodge is
1185
+ // clamped addition, which is what the keyword means, and is the same filter
1186
+ // AppKit reaches for.
1187
+ TEST(win32_paint_plus_lighter_adds_rather_than_being_ignored) {
1188
+ Tree tree;
1189
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1190
+ greyOnRed(tree, root, "plus-lighter");
1191
+
1192
+ // Red plus half grey: the red channel clamps and the other two are the grey's
1193
+ // own. Unblended this pixel is grey, which is what the GTK host answers.
1194
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 255, 128, 128, 255);
1195
+ }
1196
+
1197
+ // And the keyword is in the dump, spelled as the other two hosts spell it,
1198
+ // which is what lets one end-to-end scenario check the wiring on all three.
1199
+ TEST(win32_describe_prints_a_blend_mode) {
1200
+ Tree tree;
1201
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1202
+ RnWin32View *child = greyOnRed(tree, root, nullptr);
1203
+
1204
+ EXPECT(root->describeTree().find("blend=") == std::string::npos);
1205
+
1206
+ child->setBlendMode("color-dodge");
1207
+ EXPECT(root->describeTree().find("blend=color-dodge") != std::string::npos);
1208
+ EXPECT(child->blends());
1209
+
1210
+ // A keyword Direct2D has no mode for is still what the app asked for, so it
1211
+ // is still reported -- and it does not blend, which is the rule GTK follows
1212
+ // for `plus-lighter`.
1213
+ child->setBlendMode("not-a-mode");
1214
+ EXPECT(root->describeTree().find("blend=not-a-mode") != std::string::npos);
1215
+ EXPECT(!child->blends());
1216
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 50, 50, 128, 128, 128, 255);
1217
+
1218
+ // `normal` is the default and means no blending, so nothing is printed: the
1219
+ // other two hosts answer no keyword at all for it and the three lines are
1220
+ // diffed against each other.
1221
+ child->setBlendMode("normal");
1222
+ EXPECT(root->describeTree().find("blend=") == std::string::npos);
1223
+ EXPECT(!child->blends());
1224
+ }
1225
+
1226
+ // ---------------------------------------------------------------------------
1227
+ // `boxShadow`.
1228
+ //
1229
+ // **This host is the only one of the three that can be asked in pixels**, which
1230
+ // is why these tests are here rather than being the same tests as the others'.
1231
+ // GSK's shadows are render nodes its unit tests walk; macOS composites a
1232
+ // CALayer's shadow in the window server, and `renderInContext:` -- which is
1233
+ // what this project's snapshots use -- draws none of it, measured on a bare
1234
+ // layer. Direct2D draws into the same bitmap as everything else, so a shadow is
1235
+ // just pixels.
1236
+ //
1237
+ // A black shadow on a white view in a transparent root: the three answers are
1238
+ // far apart, and alpha says which of "shadow", "view" and "nothing" a pixel is.
1239
+ // ---------------------------------------------------------------------------
1240
+ namespace {
1241
+
1242
+ // A white view at (30,30), 40 by 40, inside a 100 by 100 root, with one shadow.
1243
+ // The view is returned so a test can change it.
1244
+ RnWin32View *shadowedBox(Tree &tree,
1245
+ RnWin32View *root,
1246
+ const RnWin32View::BoxShadow &shadow) {
1247
+ RnWin32View *view = tree.colouredBox(2, 30, 30, 40, 40, 1.0f, 1.0f, 1.0f);
1248
+ view->setBoxShadows({shadow});
1249
+ root->insertChild(view, 0);
1250
+ return view;
1251
+ }
1252
+
1253
+ RnWin32View::BoxShadow blackShadow(float dx, float dy, float blur, float spread) {
1254
+ RnWin32View::BoxShadow shadow;
1255
+ shadow.dx = dx;
1256
+ shadow.dy = dy;
1257
+ shadow.blur = blur;
1258
+ shadow.spread = spread;
1259
+ shadow.colour[3] = 1.0f;
1260
+ return shadow;
1261
+ }
1262
+
1263
+ } // namespace
1264
+
1265
+ TEST(win32_paint_an_outset_shadow_lands_at_its_offset) {
1266
+ Tree tree;
1267
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1268
+ shadowedBox(tree, root, blackShadow(10.0f, 10.0f, 0.0f, 0.0f));
1269
+
1270
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1271
+ // The shadow's own box is (40,40) to (80,80): below and right of the view,
1272
+ // which is what dx and dy mean and the thing most easily put on the wrong
1273
+ // axis.
1274
+ EXPECT_PIXEL(pixels, 75, 75, 0, 0, 0, 255);
1275
+ // The view itself, untouched, and the corner the shadow moved away from.
1276
+ EXPECT_PIXEL(pixels, 35, 35, 255, 255, 255, 255);
1277
+ // Above and left of the view: nothing at all, where a shadow with its offset
1278
+ // negated would be.
1279
+ EXPECT_PIXEL(pixels, 25, 25, 0, 0, 0, 0);
1280
+ }
1281
+
1282
+ TEST(win32_paint_a_shadows_spread_grows_its_box) {
1283
+ Tree tree;
1284
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1285
+ shadowedBox(tree, root, blackShadow(0.0f, 0.0f, 0.0f, 6.0f));
1286
+
1287
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1288
+ // Four points out from the view's edge, inside the spread. With no spread
1289
+ // this is nothing, and a spread read as an offset would put it on one side
1290
+ // only.
1291
+ EXPECT_PIXEL(pixels, 26, 50, 0, 0, 0, 255);
1292
+ EXPECT_PIXEL(pixels, 74, 50, 0, 0, 0, 255);
1293
+ EXPECT_PIXEL(pixels, 50, 26, 0, 0, 0, 255);
1294
+ EXPECT_PIXEL(pixels, 50, 74, 0, 0, 0, 255);
1295
+ // And eight out, past it.
1296
+ EXPECT_PIXEL(pixels, 22, 50, 0, 0, 0, 0);
1297
+ }
1298
+
1299
+ TEST(win32_paint_a_shadows_blur_softens_its_edge) {
1300
+ const auto alphaAt = [](float blur, float spread, unsigned x, unsigned y) {
1301
+ Tree tree;
1302
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1303
+ shadowedBox(tree, root, blackShadow(0.0f, 0.0f, blur, spread));
1304
+ return static_cast<int>(basalt::win32::renderToPixels(*root).at(x, y).alpha);
1305
+ };
1306
+
1307
+ // Two points out past the hard edge of a six point spread, which is at 24:
1308
+ // nothing without a blur, and something with one.
1309
+ EXPECT_EQ(alphaAt(0.0f, 6.0f, 50, 22), 0);
1310
+ EXPECT(alphaAt(12.0f, 6.0f, 50, 22) > 20);
1311
+
1312
+ // And a blur is not simply a wider spread: well inside the shape it is still
1313
+ // opaque. Twelve points inside the edge of a twenty-four point spread, which
1314
+ // is two standard deviations of a twelve point blur -- and outside the view's
1315
+ // own box, since that is the only place an outset shadow shows at all.
1316
+ EXPECT(alphaAt(12.0f, 24.0f, 50, 18) > 200);
1317
+ }
1318
+
1319
+ // The shadow is not painted under the view that casts it, which a translucent
1320
+ // background is the only way to see -- and is the reason the clip is a shape
1321
+ // rather than a rectangle.
1322
+ TEST(win32_paint_an_outset_shadow_is_not_painted_under_its_own_box) {
1323
+ Tree tree;
1324
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1325
+ RnWin32View *view = shadowedBox(tree, root, blackShadow(0.0f, 0.0f, 0.0f, 10.0f));
1326
+ // Half-transparent white: a shadow underneath would show through it as grey.
1327
+ view->setBackgroundColor(1.0f, 1.0f, 1.0f, 0.5f, true);
1328
+
1329
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1330
+ // Inside the view: the background over nothing, so white at half alpha.
1331
+ EXPECT_PIXEL(pixels, 50, 50, 255, 255, 255, 128);
1332
+ // Outside it, where the spread reaches: the shadow.
1333
+ EXPECT_PIXEL(pixels, 25, 50, 0, 0, 0, 255);
1334
+ }
1335
+
1336
+ TEST(win32_paint_an_inset_shadow_stays_inside_the_box) {
1337
+ Tree tree;
1338
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1339
+ RnWin32View::BoxShadow shadow = blackShadow(0.0f, 0.0f, 0.0f, 6.0f);
1340
+ shadow.inset = true;
1341
+ shadowedBox(tree, root, shadow);
1342
+
1343
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1344
+ // A ring just inside the edge, where an outset shadow would be just outside
1345
+ // it: at (32,50) the shadow, at (26,50) nothing.
1346
+ EXPECT_PIXEL(pixels, 32, 50, 0, 0, 0, 255);
1347
+ EXPECT_PIXEL(pixels, 26, 50, 0, 0, 0, 0);
1348
+ // And the middle is the view's own white: an inset shadow with no offset is a
1349
+ // ring, not a fill.
1350
+ EXPECT_PIXEL(pixels, 50, 50, 255, 255, 255, 255);
1351
+ }
1352
+
1353
+ // An inset shadow with an offset is a crescent, which is what says the hole is
1354
+ // moved rather than the shape.
1355
+ TEST(win32_paint_an_inset_shadow_with_an_offset_is_a_crescent) {
1356
+ Tree tree;
1357
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1358
+ RnWin32View::BoxShadow shadow = blackShadow(0.0f, 8.0f, 0.0f, 0.0f);
1359
+ shadow.inset = true;
1360
+ shadowedBox(tree, root, shadow);
1361
+
1362
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1363
+ // The hole moved down by eight, so the top eight points inside the box are
1364
+ // shadow and the bottom edge is the view's own colour.
1365
+ EXPECT_PIXEL(pixels, 50, 34, 0, 0, 0, 255);
1366
+ EXPECT_PIXEL(pixels, 50, 65, 255, 255, 255, 255);
1367
+ }
1368
+
1369
+ // The first shadow in the list is the one on top, which CSS says and both other
1370
+ // hosts implement by painting the list backwards.
1371
+ TEST(win32_paint_the_first_shadow_in_the_list_is_on_top) {
1372
+ Tree tree;
1373
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1374
+ RnWin32View::BoxShadow red = blackShadow(10.0f, 10.0f, 0.0f, 0.0f);
1375
+ red.colour[0] = 1.0f;
1376
+ RnWin32View::BoxShadow blue = blackShadow(10.0f, 10.0f, 0.0f, 0.0f);
1377
+ blue.colour[2] = 1.0f;
1378
+ RnWin32View *view = tree.colouredBox(2, 30, 30, 40, 40, 1.0f, 1.0f, 1.0f);
1379
+ view->setBoxShadows({red, blue});
1380
+ root->insertChild(view, 0);
1381
+
1382
+ // Both shadows are in the same place, so what shows is whichever is on top.
1383
+ EXPECT_PIXEL(basalt::win32::renderToPixels(*root), 75, 75, 255, 0, 0, 255);
1384
+ }
1385
+
1386
+ TEST(win32_paint_a_shadow_with_no_colour_is_not_drawn) {
1387
+ Tree tree;
1388
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1389
+ // Alpha zero is what a shadow React Native could not parse arrives as, and
1390
+ // painting it black -- which is what the colour's other three components say
1391
+ // -- is the failure mode all three hosts had to avoid.
1392
+ RnWin32View::BoxShadow nothing;
1393
+ nothing.dx = 10.0f;
1394
+ nothing.dy = 10.0f;
1395
+ nothing.spread = 10.0f;
1396
+ shadowedBox(tree, root, nothing);
1397
+
1398
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1399
+ EXPECT_PIXEL(pixels, 75, 75, 0, 0, 0, 0);
1400
+ // And the view itself is still there: skipping the shadow must not skip the
1401
+ // view with it.
1402
+ EXPECT_PIXEL(pixels, 50, 50, 255, 255, 255, 255);
1403
+ }
1404
+
1405
+ // A negative blur radius, which CSS forbids and React Native parses anyway. GSK
1406
+ // asserts on one, so the GTK half clamps it; this host has to do the same, and
1407
+ // the spread beside it has to stay signed.
1408
+ TEST(win32_paint_a_negative_blur_is_clamped_and_a_negative_spread_is_not) {
1409
+ Tree tree;
1410
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1411
+ RnWin32View::BoxShadow shadow = blackShadow(0.0f, 0.0f, -8.0f, 6.0f);
1412
+ shadowedBox(tree, root, shadow);
1413
+
1414
+ // A clamped blur is no blur, so the spread's edge is hard and in the same
1415
+ // place it is without one.
1416
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1417
+ EXPECT_PIXEL(pixels, 26, 50, 0, 0, 0, 255);
1418
+ EXPECT_PIXEL(pixels, 22, 50, 0, 0, 0, 0);
1419
+
1420
+ // And a negative spread shrinks the shadow rather than growing it: with an
1421
+ // offset of twelve and a spread of minus six, the shadow's box is (46,46) to
1422
+ // (76,76), so the far corner is inside it and the near one is not.
1423
+ Tree shrunk;
1424
+ RnWin32View *root2 = shrunk.box(1, 0, 0, 100, 100);
1425
+ shadowedBox(shrunk, root2, blackShadow(12.0f, 12.0f, 0.0f, -6.0f));
1426
+ const RnPixels narrow = basalt::win32::renderToPixels(*root2);
1427
+ EXPECT_PIXEL(narrow, 74, 74, 0, 0, 0, 255);
1428
+ EXPECT_PIXEL(narrow, 79, 79, 0, 0, 0, 0);
1429
+ }
1430
+
1431
+ TEST(win32_describe_prints_a_box_shadow_as_the_other_hosts_do) {
1432
+ Tree tree;
1433
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1434
+ RnWin32View *view = tree.box(2, 30, 30, 40, 40);
1435
+ root->insertChild(view, 0);
1436
+
1437
+ EXPECT(root->describeTree().find("shadow=") == std::string::npos);
1438
+
1439
+ RnWin32View::BoxShadow outset = blackShadow(0.0f, 4.0f, 8.0f, 0.0f);
1440
+ outset.colour[3] = 0.25f;
1441
+ RnWin32View::BoxShadow inset = blackShadow(0.0f, 1.0f, 0.0f, 0.0f);
1442
+ inset.colour[0] = 1.0f;
1443
+ inset.colour[1] = 1.0f;
1444
+ inset.colour[2] = 1.0f;
1445
+ inset.inset = true;
1446
+ view->setBoxShadows({outset, inset});
1447
+
1448
+ // The same two the end-to-end scenario reads, spelled the same way: the
1449
+ // scenario greps for these strings on all three hosts.
1450
+ const std::string dump = root->describeTree();
1451
+ EXPECT(dump.find("shadow=(0,4,8,0,#00000040)") != std::string::npos);
1452
+ EXPECT(dump.find("shadow=(inset 0,1,0,0,#ffffffff)") != std::string::npos);
1453
+ }
1454
+
1455
+ // ---------------------------------------------------------------------------
1456
+ // `backgroundImage`: the gradients, in pixels.
1457
+ //
1458
+ // The arithmetic is not this host's: `core/Gradients.h` resolves CSS's gradient
1459
+ // line and its colour-stop fixup, and `core/BackgroundLayers.h` works out where
1460
+ // the image goes and the tile it repeats in. Both have their own tests in core,
1461
+ // shared with the other two hosts. What is this host's, and what these assert,
1462
+ // is that a brush is built from those numbers and lands in the right place.
1463
+ //
1464
+ // Red to blue is the instrument: the red and blue channels say how far along a
1465
+ // gradient a pixel is, and a gradient painted backwards or on the wrong axis
1466
+ // still looks like a gradient.
1467
+ // ---------------------------------------------------------------------------
1468
+ namespace {
1469
+
1470
+ RnWin32View::Gradient redToBlue(float x, float y, float width, float height) {
1471
+ RnWin32View::Gradient gradient;
1472
+ gradient.area[0] = x;
1473
+ gradient.area[1] = y;
1474
+ gradient.area[2] = width;
1475
+ gradient.area[3] = height;
1476
+ gradient.startX = x;
1477
+ gradient.startY = y;
1478
+ gradient.endX = x + width;
1479
+ gradient.endY = y;
1480
+ RnWin32View::GradientStop red;
1481
+ red.offset = 0.0f;
1482
+ red.colour[0] = 1.0f;
1483
+ red.colour[3] = 1.0f;
1484
+ RnWin32View::GradientStop blue;
1485
+ blue.offset = 1.0f;
1486
+ blue.colour[2] = 1.0f;
1487
+ blue.colour[3] = 1.0f;
1488
+ gradient.stops = {red, blue};
1489
+ return gradient;
1490
+ }
1491
+
1492
+ } // namespace
1493
+
1494
+ TEST(win32_paint_a_linear_gradient_runs_along_its_line) {
1495
+ Tree tree;
1496
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1497
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1498
+ view->setGradients({redToBlue(0.0f, 0.0f, 100.0f, 100.0f)});
1499
+ root->insertChild(view, 0);
1500
+
1501
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1502
+ // Along the line: nearly all red at the start, nearly all blue at the end,
1503
+ // and halfway between in the middle. Bounds rather than exact values, which
1504
+ // would pin where Direct2D samples a brush rather than the gradient; the far
1505
+ // end being blue is what a gradient painted backwards fails.
1506
+ EXPECT(pixels.at(2, 50).red > 235 && pixels.at(2, 50).blue < 20);
1507
+ EXPECT(pixels.at(97, 50).blue > 235 && pixels.at(97, 50).red < 20);
1508
+ EXPECT(pixels.at(50, 50).red > 100 && pixels.at(50, 50).red < 160);
1509
+ EXPECT(pixels.at(50, 50).blue > 100 && pixels.at(50, 50).blue < 160);
1510
+ // And across it, nothing changes: a gradient on the wrong axis fails here.
1511
+ EXPECT_NEAR(pixels.at(50, 10).red, pixels.at(50, 90).red, 2.0);
1512
+ }
1513
+
1514
+ // The background colour is underneath, which is the order CSS gives and the one
1515
+ // thing a gradient that replaced the fill would pass anyway -- so the gradient
1516
+ // here covers half the box and the colour shows through the other half.
1517
+ TEST(win32_paint_a_gradient_sits_over_the_background_colour) {
1518
+ Tree tree;
1519
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1520
+ RnWin32View *view = tree.colouredBox(2, 0, 0, 100, 100, 0.0f, 1.0f, 0.0f);
1521
+ view->setGradients({redToBlue(0.0f, 0.0f, 100.0f, 50.0f)});
1522
+ root->insertChild(view, 0);
1523
+
1524
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1525
+ // Inside the image's rectangle: the gradient.
1526
+ EXPECT(pixels.at(2, 25).red > 235 && pixels.at(2, 25).green < 20);
1527
+ // Below it: the background colour, which the gradient must not have painted
1528
+ // over and must not have replaced.
1529
+ EXPECT_PIXEL(pixels, 2, 75, 0, 255, 0, 255);
1530
+ }
1531
+
1532
+ // `backgroundSize` and `backgroundPosition` arrive as the image's rectangle, so
1533
+ // what this asserts is that the rectangle is honoured and the gradient is
1534
+ // resolved against it rather than against the view.
1535
+ TEST(win32_paint_a_gradient_fills_the_rectangle_it_was_given) {
1536
+ Tree tree;
1537
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1538
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1539
+ view->setGradients({redToBlue(20.0f, 20.0f, 40.0f, 40.0f)});
1540
+ root->insertChild(view, 0);
1541
+
1542
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1543
+ // The ends of the line are the ends of the rectangle, not of the view: two
1544
+ // points into a forty point rectangle is a twentieth along, where two points
1545
+ // into the view would be a fiftieth.
1546
+ EXPECT(pixels.at(22, 40).red > 230 && pixels.at(22, 40).blue < 30);
1547
+ EXPECT(pixels.at(57, 40).blue > 215 && pixels.at(57, 40).red < 40);
1548
+ // And nothing outside it: a `no-repeat` background with a size covers only
1549
+ // what it was given.
1550
+ EXPECT_PIXEL(pixels, 70, 40, 0, 0, 0, 0);
1551
+ EXPECT_PIXEL(pixels, 10, 40, 0, 0, 0, 0);
1552
+ }
1553
+
1554
+ TEST(win32_paint_a_repeating_gradient_tiles_across_the_box) {
1555
+ Tree tree;
1556
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1557
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1558
+ RnWin32View::Gradient gradient = redToBlue(0.0f, 0.0f, 25.0f, 100.0f);
1559
+ gradient.repeats = true;
1560
+ gradient.tile[0] = 0.0f;
1561
+ gradient.tile[1] = 0.0f;
1562
+ gradient.tile[2] = 25.0f;
1563
+ gradient.tile[3] = 100.0f;
1564
+ view->setGradients({gradient});
1565
+ root->insertChild(view, 0);
1566
+
1567
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1568
+ // Four tiles across, each red at its own left edge: a brush that was not
1569
+ // moved with the tile would be blue everywhere but the first.
1570
+ for (unsigned tile = 0; tile < 4; tile++) {
1571
+ const unsigned left = tile * 25 + 2;
1572
+ const unsigned right = tile * 25 + 22;
1573
+ EXPECT(pixels.at(left, 50).red > 200);
1574
+ EXPECT(pixels.at(right, 50).blue > 180);
1575
+ }
1576
+ }
1577
+
1578
+ // A tile whose first copy starts left of the box, which is what a
1579
+ // `backgroundPosition` of anything but zero does: the tiles before it still
1580
+ // have to cover the box.
1581
+ TEST(win32_paint_a_repeating_gradient_starts_before_the_box_when_it_has_to) {
1582
+ Tree tree;
1583
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1584
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1585
+ RnWin32View::Gradient gradient = redToBlue(10.0f, 0.0f, 25.0f, 100.0f);
1586
+ gradient.repeats = true;
1587
+ gradient.tile[0] = 10.0f;
1588
+ gradient.tile[1] = 0.0f;
1589
+ gradient.tile[2] = 25.0f;
1590
+ gradient.tile[3] = 100.0f;
1591
+ view->setGradients({gradient});
1592
+ root->insertChild(view, 0);
1593
+
1594
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1595
+ // The first tile starts at ten, so the one before it covers (-15, 10): at
1596
+ // x = 2 that tile is seventeen points along its own twenty-five, which is
1597
+ // mostly blue. A host that only tiled forwards leaves this transparent.
1598
+ EXPECT(pixels.at(2, 50).alpha > 250);
1599
+ EXPECT(pixels.at(2, 50).blue > 120);
1600
+ // And the named tile itself still starts red at ten.
1601
+ EXPECT(pixels.at(12, 50).red > 200);
1602
+ }
1603
+
1604
+ TEST(win32_paint_a_radial_gradient_runs_out_from_its_centre) {
1605
+ Tree tree;
1606
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1607
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1608
+ RnWin32View::Gradient gradient = redToBlue(0.0f, 0.0f, 100.0f, 100.0f);
1609
+ gradient.kind = RnWin32View::Gradient::Kind::Radial;
1610
+ gradient.centreX = 50.0f;
1611
+ gradient.centreY = 50.0f;
1612
+ gradient.radiusX = 40.0f;
1613
+ gradient.radiusY = 20.0f;
1614
+ view->setGradients({gradient});
1615
+ root->insertChild(view, 0);
1616
+
1617
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1618
+ // Red at the centre, blue at the ending shape, and the two radii are not the
1619
+ // same number: twenty up is the end of the ellipse where forty across is, so
1620
+ // a brush given one radius for both would differ at one of them.
1621
+ //
1622
+ // Nearly red rather than exactly: Direct2D samples the brush at the pixel's
1623
+ // centre, which is half a point out from the gradient's, and half a point of
1624
+ // a twenty point radius is a couple of percent of the way to blue. Measured
1625
+ // at (247, 0, 7) rather than guessed at.
1626
+ EXPECT(pixels.at(50, 50).red > 240 && pixels.at(50, 50).blue < 20);
1627
+ EXPECT_NEAR(pixels.at(50, 50).alpha, 255, 1.0);
1628
+ EXPECT(pixels.at(88, 50).blue > 200);
1629
+ EXPECT(pixels.at(50, 69).blue > 200);
1630
+ // Halfway out along each axis is halfway along the gradient, which is what
1631
+ // says the ellipse is an ellipse rather than a circle plus a stretch of the
1632
+ // whole picture.
1633
+ EXPECT(pixels.at(70, 50).blue > 100 && pixels.at(70, 50).blue < 180);
1634
+ EXPECT(pixels.at(50, 60).blue > 100 && pixels.at(50, 60).blue < 180);
1635
+ // Past the ending shape the last stop fills the rest of the rectangle, which
1636
+ // is CSS and is `D2D1_EXTEND_MODE_CLAMP`.
1637
+ EXPECT(pixels.at(97, 97).blue > 200);
1638
+ }
1639
+
1640
+ // The first gradient in the list is the one on top, which is CSS's order for
1641
+ // `background-image` and the order both other hosts paint in.
1642
+ TEST(win32_paint_the_first_gradient_in_the_list_is_on_top) {
1643
+ Tree tree;
1644
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1645
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1646
+ RnWin32View::Gradient top = redToBlue(0.0f, 0.0f, 100.0f, 100.0f);
1647
+ RnWin32View::Gradient under = redToBlue(0.0f, 0.0f, 100.0f, 100.0f);
1648
+ // The one underneath is solid green, so whichever is on top is unambiguous.
1649
+ for (auto &stop : under.stops) {
1650
+ stop.colour[0] = 0.0f;
1651
+ stop.colour[1] = 1.0f;
1652
+ stop.colour[2] = 0.0f;
1653
+ }
1654
+ view->setGradients({top, under});
1655
+ root->insertChild(view, 0);
1656
+
1657
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1658
+ EXPECT(pixels.at(2, 50).red > 235);
1659
+ EXPECT(pixels.at(2, 50).green < 20);
1660
+ }
1661
+
1662
+ // A gradient is clipped to the border box, radii included: CSS clips a
1663
+ // background to the painting area, and a gradient that squared off a rounded
1664
+ // corner would be a worse bug than no gradient.
1665
+ TEST(win32_paint_a_gradient_is_clipped_to_the_rounded_box) {
1666
+ Tree tree;
1667
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1668
+ RnWin32View *view = tree.box(2, 0, 0, 100, 100);
1669
+ const float radii[8] = {40.0f, 40.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
1670
+ view->setCornerRadii(radii);
1671
+ view->setGradients({redToBlue(0.0f, 0.0f, 100.0f, 100.0f)});
1672
+ root->insertChild(view, 0);
1673
+
1674
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
1675
+ // Inside the corner's curve: painted. Outside it: nothing.
1676
+ EXPECT(pixels.at(30, 30).alpha > 250);
1677
+ EXPECT_PIXEL(pixels, 2, 2, 0, 0, 0, 0);
1678
+ }
1679
+
1680
+ TEST(win32_describe_prints_a_gradient_as_the_other_hosts_do) {
1681
+ Tree tree;
1682
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
1683
+ RnWin32View *view = tree.box(2, 0, 0, 80, 40);
1684
+ root->insertChild(view, 0);
1685
+
1686
+ EXPECT(root->describeTree().find("gradient=") == std::string::npos);
1687
+
1688
+ // The 135 degree case the end-to-end scenario reads, whose line is the
1689
+ // perpendicular construction rather than the diagonal.
1690
+ RnWin32View::Gradient linear = redToBlue(0.0f, 0.0f, 80.0f, 40.0f);
1691
+ linear.startX = 10.0f;
1692
+ linear.startY = -10.0f;
1693
+ linear.endX = 70.0f;
1694
+ linear.endY = 50.0f;
1695
+ view->setGradients({linear});
1696
+ EXPECT(root->describeTree().find("gradient=((10,-10)-(70,50),2 stops,at=(0,0 80x40))")
1697
+ != std::string::npos);
1698
+
1699
+ RnWin32View::Gradient radial = redToBlue(0.0f, 0.0f, 80.0f, 40.0f);
1700
+ radial.kind = RnWin32View::Gradient::Kind::Radial;
1701
+ radial.centreX = 40.0f;
1702
+ radial.centreY = 20.0f;
1703
+ radial.radiusX = 40.0f;
1704
+ radial.radiusY = 20.0f;
1705
+ radial.repeats = true;
1706
+ radial.tile[2] = 80.0f;
1707
+ radial.tile[3] = 40.0f;
1708
+ view->setGradients({radial});
1709
+ EXPECT(root->describeTree().find(
1710
+ "gradient=(radial (40,20) 40x20,2 stops,at=(0,0 80x40),tile=(0,0 80x40))")
1711
+ != std::string::npos);
1712
+ }
1713
+
1714
+ // --- The overlay scrollbar's colour, which is `indicatorStyle` ---------------
1715
+ //
1716
+ // The Win32 half of the one prop in that group a picture can answer: a white
1717
+ // thumb, which is what a list over dark content asks for and what a black one
1718
+ // would be invisible against. Core decides the colour and every host draws it.
1719
+ namespace {
1720
+
1721
+ // Where the vertical thumb is in a view `width` wide: inset from the trailing
1722
+ // edge by core's own two numbers, so this moves if those do.
1723
+ unsigned thumbColumn(float width) {
1724
+ return static_cast<unsigned>(width - basalt::kScrollIndicatorInset
1725
+ - basalt::kScrollIndicatorThickness / 2.0);
1726
+ }
1727
+
1728
+ } // namespace
1729
+
1730
+ TEST(win32_paint_an_indicator_is_black_by_default) {
1731
+ auto root = std::make_unique<RnWin32View>(1);
1732
+ root->setFrame(0, 0, 100, 100);
1733
+ root->setScrollIndicators(0.0f, 50.0f, 0.0f, 0.0f);
1734
+
1735
+ const basalt::win32::RnPixels pixels = basalt::win32::renderToPixels(*root);
1736
+ const auto pixel = pixels.at(thumbColumn(100), 20);
1737
+ EXPECT_NEAR(pixel.red, 0, 3);
1738
+ EXPECT_NEAR(pixel.green, 0, 3);
1739
+ EXPECT_NEAR(pixel.blue, 0, 3);
1740
+ // Translucent, so the thumb sits over content rather than hiding it: 0.35 of
1741
+ // 255, which is the alpha core names.
1742
+ EXPECT_NEAR(pixel.alpha, 89, 3);
1743
+ }
1744
+
1745
+ // The hairline that tells `default` from `black`, which is the whole
1746
+ // difference between the two on iOS: `UIScrollViewIndicatorStyleDefault` is
1747
+ // "black with a white border" and `.black` is black alone. Without it a list
1748
+ // over dark content that never set `indicatorStyle` drew a black thumb on a
1749
+ // black ground.
1750
+ //
1751
+ // Read at the thumb's outermost column rather than its middle, which is where
1752
+ // the two differ: the fill is the same black either way and the test above
1753
+ // already pins that.
1754
+ TEST(win32_paint_a_default_indicator_has_a_white_edge_and_black_has_none) {
1755
+ const auto edgeColumn = [](float width) {
1756
+ return static_cast<unsigned>(width - basalt::kScrollIndicatorInset
1757
+ - basalt::kScrollIndicatorThickness);
1758
+ };
1759
+
1760
+ auto root = std::make_unique<RnWin32View>(1);
1761
+ root->setFrame(0, 0, 100, 100);
1762
+ root->setScrollIndicators(0.0f, 50.0f, 0.0f, 0.0f);
1763
+ const basalt::ScrollIndicatorColour edge =
1764
+ basalt::scrollIndicatorBorderColourFor(basalt::ScrollIndicatorStyle::Default);
1765
+ root->setScrollIndicatorBorderColour(edge.red, edge.green, edge.blue, edge.alpha);
1766
+
1767
+ const basalt::win32::RnPixels bordered = basalt::win32::renderToPixels(*root);
1768
+ const auto hairline = bordered.at(edgeColumn(100), 20);
1769
+ // White, where the fill two points inside it is black.
1770
+ EXPECT(hairline.red > 128);
1771
+ EXPECT(hairline.green > 128);
1772
+ EXPECT(hairline.blue > 128);
1773
+ EXPECT_NEAR(bordered.at(thumbColumn(100), 20).red, 0, 3);
1774
+
1775
+ // And `black`, which is the same fill with no edge at all.
1776
+ const basalt::ScrollIndicatorColour none =
1777
+ basalt::scrollIndicatorBorderColourFor(basalt::ScrollIndicatorStyle::Black);
1778
+ root->setScrollIndicatorBorderColour(none.red, none.green, none.blue, none.alpha);
1779
+ const basalt::win32::RnPixels plain = basalt::win32::renderToPixels(*root);
1780
+ EXPECT(plain.at(edgeColumn(100), 20).red < 128);
1781
+ }
1782
+
1783
+ TEST(win32_paint_an_indicator_takes_the_colour_it_was_given) {
1784
+ auto root = std::make_unique<RnWin32View>(1);
1785
+ root->setFrame(0, 0, 100, 100);
1786
+ root->setScrollIndicators(0.0f, 50.0f, 0.0f, 0.0f);
1787
+ const basalt::ScrollIndicatorColour white =
1788
+ basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::White);
1789
+ root->setScrollIndicatorColour(white.red, white.green, white.blue, white.alpha);
1790
+
1791
+ const basalt::win32::RnPixels pixels = basalt::win32::renderToPixels(*root);
1792
+ const auto pixel = pixels.at(thumbColumn(100), 20);
1793
+ EXPECT_NEAR(pixel.red, 255, 3);
1794
+ EXPECT_NEAR(pixel.green, 255, 3);
1795
+ EXPECT_NEAR(pixel.blue, 255, 3);
1796
+ EXPECT_NEAR(pixel.alpha, 89, 3);
1797
+ // And nowhere else: the colour is the thumb's, not the view's.
1798
+ EXPECT_EQ(static_cast<int>(pixels.at(20, 20).alpha), 0);
1799
+ }