basalt-win32 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +429 -0
- package/native/tests/test_win32_accessibility.cpp +585 -0
- package/native/tests/test_win32_animated_image.cpp +351 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +750 -0
- package/native/tests/test_win32_image.cpp +370 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +511 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +757 -0
- package/native/tests/test_win32_paint.cpp +1799 -0
- package/native/tests/test_win32_scrollview.cpp +647 -0
- package/native/tests/test_win32_selection.cpp +215 -0
- package/native/tests/test_win32_text.cpp +1456 -0
- package/native/tests/test_win32_textinput.cpp +1594 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +156 -0
- package/native/win32/DirectWriteLayout.cpp +485 -0
- package/native/win32/DirectWriteLayout.h +77 -0
- package/native/win32/DirectWriteLayoutManager.cpp +154 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +744 -0
- package/native/win32/RnWin32Accessible.cpp +343 -0
- package/native/win32/RnWin32Accessible.h +130 -0
- package/native/win32/RnWin32Image.cpp +588 -0
- package/native/win32/RnWin32Image.h +139 -0
- package/native/win32/RnWin32TextLayout.cpp +1379 -0
- package/native/win32/RnWin32TextLayout.h +390 -0
- package/native/win32/RnWin32View.cpp +2956 -0
- package/native/win32/RnWin32View.h +926 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +209 -0
- package/native/win32/Win32Cursors.cpp +309 -0
- package/native/win32/Win32Cursors.h +121 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +199 -0
- package/native/win32/Win32ImageLoader.h +133 -0
- package/native/win32/Win32InputScopes.h +92 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +382 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +1282 -0
- package/native/win32/Win32MountingManager.h +243 -0
- package/native/win32/Win32Offscreen.h +53 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +718 -0
- package/native/win32/Win32ScrollView.h +256 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +1296 -0
- package/native/win32/Win32TextInput.h +341 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +577 -0
- package/native/win32/Win32TouchDispatcher.h +201 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2722 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +25 -4
- package/react-native.config.js +22 -0
- package/README.md +0 -3
|
@@ -0,0 +1,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
|
+
}
|