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,2956 @@
|
|
|
1
|
+
#include "RnWin32View.h"
|
|
2
|
+
|
|
3
|
+
#include "Backface.h"
|
|
4
|
+
#include "ControlMetrics.h"
|
|
5
|
+
#include "FocusRing.h"
|
|
6
|
+
#include "RnWin32Image.h"
|
|
7
|
+
#include "RnWin32TextLayout.h"
|
|
8
|
+
#include "ScrollIndicator.h"
|
|
9
|
+
#include "Win32Clip.h"
|
|
10
|
+
#include "Win32Offscreen.h"
|
|
11
|
+
#include "Win32Strings.h"
|
|
12
|
+
|
|
13
|
+
// Which frame an animated image is showing, and the curve a box shadow's corner
|
|
14
|
+
// grows by. Both shared with the other two hosts.
|
|
15
|
+
#include "ImageAnimation.h"
|
|
16
|
+
#include "ShadowShape.h"
|
|
17
|
+
|
|
18
|
+
// Before d2d1.h, which wants the base Windows types and does not pull them in
|
|
19
|
+
// itself. NOMINMAX and WIN32_LEAN_AND_MEAN come from the package's CMakeLists;
|
|
20
|
+
// without the first, windows.h defines `min` and `max` as macros and breaks
|
|
21
|
+
// <algorithm> below at a distance.
|
|
22
|
+
#include <windows.h>
|
|
23
|
+
|
|
24
|
+
#include <d2d1.h>
|
|
25
|
+
#include <d2d1_1.h>
|
|
26
|
+
#include <d2d1effects.h>
|
|
27
|
+
#include <d2d1helper.h>
|
|
28
|
+
#include <wrl/client.h>
|
|
29
|
+
|
|
30
|
+
// `UiaRaiseNotificationEvent`, which is the only call in UI Automation that
|
|
31
|
+
// speaks a string, and the provider interface it raises the event on.
|
|
32
|
+
// `uiautomation.h` is the umbrella header the accessible half already uses.
|
|
33
|
+
#include <uiautomation.h>
|
|
34
|
+
|
|
35
|
+
#include <algorithm>
|
|
36
|
+
#include <chrono>
|
|
37
|
+
#include <optional>
|
|
38
|
+
#include <utility>
|
|
39
|
+
#include <cmath>
|
|
40
|
+
#include <cstdint>
|
|
41
|
+
#include <cstdio>
|
|
42
|
+
|
|
43
|
+
using Microsoft::WRL::ComPtr;
|
|
44
|
+
|
|
45
|
+
namespace basalt::win32 {
|
|
46
|
+
|
|
47
|
+
using basalt::kFocusRingAlpha;
|
|
48
|
+
using basalt::kFocusRingBlue;
|
|
49
|
+
using basalt::kFocusRingGreen;
|
|
50
|
+
using basalt::kFocusRingRed;
|
|
51
|
+
using basalt::kFocusRingWidth;
|
|
52
|
+
|
|
53
|
+
namespace {
|
|
54
|
+
|
|
55
|
+
// A scoped opacity layer, for `opacity` on a view.
|
|
56
|
+
//
|
|
57
|
+
// A layer rather than multiplying every brush's alpha: React Native's opacity
|
|
58
|
+
// composites the subtree as a unit, so two overlapping half-transparent
|
|
59
|
+
// children do not show through each other.
|
|
60
|
+
class ScopedOpacity {
|
|
61
|
+
public:
|
|
62
|
+
ScopedOpacity(ID2D1RenderTarget *target, float opacity) : target_(target) {
|
|
63
|
+
if (opacity >= 1.0f) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (FAILED(target_->CreateLayer(nullptr, layer_.GetAddressOf()))) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
auto parameters = D2D1::LayerParameters();
|
|
70
|
+
parameters.contentBounds = D2D1::InfiniteRect();
|
|
71
|
+
parameters.opacity = opacity;
|
|
72
|
+
target_->PushLayer(parameters, layer_.Get());
|
|
73
|
+
pushed_ = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
~ScopedOpacity() {
|
|
77
|
+
if (pushed_) {
|
|
78
|
+
target_->PopLayer();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ScopedOpacity(const ScopedOpacity &) = delete;
|
|
83
|
+
ScopedOpacity &operator=(const ScopedOpacity &) = delete;
|
|
84
|
+
|
|
85
|
+
private:
|
|
86
|
+
ID2D1RenderTarget *target_;
|
|
87
|
+
ComPtr<ID2D1Layer> layer_;
|
|
88
|
+
bool pushed_ = false;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Escaped the way g_strescape's output is on the GTK side, so a string with a
|
|
92
|
+
// quote or a newline in it stays one line and stays comparable.
|
|
93
|
+
void appendEscaped(std::string &out, const std::string &text) {
|
|
94
|
+
for (const char c : text) {
|
|
95
|
+
switch (c) {
|
|
96
|
+
case '\\':
|
|
97
|
+
out += "\\\\";
|
|
98
|
+
break;
|
|
99
|
+
case '"':
|
|
100
|
+
out += "\\\"";
|
|
101
|
+
break;
|
|
102
|
+
case '\n':
|
|
103
|
+
out += "\\n";
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
out += c;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
template <typename... Args>
|
|
113
|
+
void appendFormat(std::string &out, const char *format, Args... args) {
|
|
114
|
+
char buffer[256];
|
|
115
|
+
const int written = std::snprintf(buffer, sizeof(buffer), format, args...);
|
|
116
|
+
if (written > 0) {
|
|
117
|
+
// snprintf returns what it *would* have written, so a truncated field is
|
|
118
|
+
// clamped here rather than read past the buffer.
|
|
119
|
+
const int limit = static_cast<int>(sizeof(buffer)) - 1;
|
|
120
|
+
out.append(buffer, static_cast<size_t>(written < limit ? written : limit));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
unsigned toByte(float component) {
|
|
125
|
+
return static_cast<unsigned>(component * 255.0f + 0.5f);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Two 2D affine matrices, in Direct2D's Matrix3x2F order and its row-vector
|
|
129
|
+
// convention: `compose(a, b)` is "apply a, then b", which is what
|
|
130
|
+
// `Matrix3x2F::SetProduct(a, b)` computes. Written out in floats rather than
|
|
131
|
+
// built with D2D1::Matrix3x2F so that hit testing -- which wants none of
|
|
132
|
+
// Direct2D -- can use the same composition painting does.
|
|
133
|
+
void compose(const float a[6], const float b[6], float out[6]) {
|
|
134
|
+
const float r[6] = {
|
|
135
|
+
a[0] * b[0] + a[1] * b[2],
|
|
136
|
+
a[0] * b[1] + a[1] * b[3],
|
|
137
|
+
a[2] * b[0] + a[3] * b[2],
|
|
138
|
+
a[2] * b[1] + a[3] * b[3],
|
|
139
|
+
a[4] * b[0] + a[5] * b[2] + b[4],
|
|
140
|
+
a[4] * b[1] + a[5] * b[3] + b[5],
|
|
141
|
+
};
|
|
142
|
+
for (int i = 0; i < 6; i++) {
|
|
143
|
+
out[i] = r[i];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Maps a point through the inverse of `m`. False when `m` is singular, which is
|
|
148
|
+
// a view scaled to nothing: it paints no pixels, so nothing can be over it.
|
|
149
|
+
bool invertPoint(const float m[6], float x, float y, float &outX, float &outY) {
|
|
150
|
+
const float determinant = m[0] * m[3] - m[1] * m[2];
|
|
151
|
+
if (std::fabs(determinant) < 1e-6f) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const float shiftedX = x - m[4];
|
|
155
|
+
const float shiftedY = y - m[5];
|
|
156
|
+
outX = (shiftedX * m[3] - shiftedY * m[2]) / determinant;
|
|
157
|
+
outY = (shiftedY * m[0] - shiftedX * m[1]) / determinant;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
} // namespace
|
|
162
|
+
|
|
163
|
+
RnWin32View::RnWin32View(int32_t tag) : tag_(tag) {}
|
|
164
|
+
|
|
165
|
+
RnWin32View::~RnWin32View() {
|
|
166
|
+
// A view owns neither its children nor its parent; the registry owns every
|
|
167
|
+
// view. What a destructor does owe is that nothing is left pointing at it.
|
|
168
|
+
if (parent_ != nullptr) {
|
|
169
|
+
parent_->removeChild(this);
|
|
170
|
+
}
|
|
171
|
+
for (RnWin32View *child : children_) {
|
|
172
|
+
child->parent_ = nullptr;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Geometry --------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
void RnWin32View::setFrame(float x, float y, float width, float height) {
|
|
179
|
+
frame_ = RnRect{x, y, width, height};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
void RnWin32View::setScrollOffset(float x, float y) {
|
|
183
|
+
scrollX_ = x;
|
|
184
|
+
scrollY_ = y;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void RnWin32View::setScrollIndicators(float verticalOffset,
|
|
188
|
+
float verticalLength,
|
|
189
|
+
float horizontalOffset,
|
|
190
|
+
float horizontalLength) {
|
|
191
|
+
indicatorVerticalOffset_ = verticalOffset;
|
|
192
|
+
indicatorVerticalLength_ = verticalLength;
|
|
193
|
+
indicatorHorizontalOffset_ = horizontalOffset;
|
|
194
|
+
indicatorHorizontalLength_ = horizontalLength;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void RnWin32View::setScrollIndicatorBorderColour(float red, float green, float blue, float alpha) {
|
|
198
|
+
indicatorBorderColour_[0] = red;
|
|
199
|
+
indicatorBorderColour_[1] = green;
|
|
200
|
+
indicatorBorderColour_[2] = blue;
|
|
201
|
+
indicatorBorderColour_[3] = alpha;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
void RnWin32View::setScrollIndicatorColour(float red, float green, float blue, float alpha) {
|
|
205
|
+
indicatorColour_[0] = red;
|
|
206
|
+
indicatorColour_[1] = green;
|
|
207
|
+
indicatorColour_[2] = blue;
|
|
208
|
+
indicatorColour_[3] = alpha;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Appearance ------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
void RnWin32View::setBackgroundColor(float red,
|
|
214
|
+
float green,
|
|
215
|
+
float blue,
|
|
216
|
+
float alpha,
|
|
217
|
+
bool hasColor) {
|
|
218
|
+
hasBackgroundColor_ = hasColor;
|
|
219
|
+
backgroundColor_[0] = red;
|
|
220
|
+
backgroundColor_[1] = green;
|
|
221
|
+
backgroundColor_[2] = blue;
|
|
222
|
+
backgroundColor_[3] = alpha;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void RnWin32View::setOpacity(float opacity) {
|
|
226
|
+
opacity_ = opacity;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
void RnWin32View::setTransform(const float *matrix16) {
|
|
230
|
+
if (matrix16 == nullptr) {
|
|
231
|
+
hasTransform_ = false;
|
|
232
|
+
transform_[0] = 1.0f;
|
|
233
|
+
transform_[1] = 0.0f;
|
|
234
|
+
transform_[2] = 0.0f;
|
|
235
|
+
transform_[3] = 1.0f;
|
|
236
|
+
transform_[4] = 0.0f;
|
|
237
|
+
transform_[5] = 0.0f;
|
|
238
|
+
updateBackFace();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// CSS matrix3d order, which is column-major: m11 m12 m13 m14 m21 ... So the
|
|
243
|
+
// 2D affine part is elements 0, 1, 4, 5, 12 and 13, and those are the same
|
|
244
|
+
// six the GTK side pulls out of its graphene matrix and the macOS side out of
|
|
245
|
+
// its CATransform3D.
|
|
246
|
+
hasTransform_ = true;
|
|
247
|
+
transform_[0] = matrix16[0];
|
|
248
|
+
transform_[1] = matrix16[1];
|
|
249
|
+
transform_[2] = matrix16[4];
|
|
250
|
+
transform_[3] = matrix16[5];
|
|
251
|
+
transform_[4] = matrix16[12];
|
|
252
|
+
transform_[5] = matrix16[13];
|
|
253
|
+
updateBackFace();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
void RnWin32View::setHidesBackFace(bool hides) {
|
|
257
|
+
if (hidesBackFace_ == hides) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
hidesBackFace_ = hides;
|
|
261
|
+
updateBackFace();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Hidden the same way the app's own `display: none` hides a view, because the
|
|
265
|
+
// effect is the same -- not painted, not hit tested, children included.
|
|
266
|
+
//
|
|
267
|
+
// The two reasons are kept apart and combined here. `hidden_` is one flag and
|
|
268
|
+
// `display: none` writes it too, so whichever of the two spoke last used to
|
|
269
|
+
// answer for both -- and layout metrics are applied after props, so a card
|
|
270
|
+
// turned away from the viewer was reliably un-hidden a moment later.
|
|
271
|
+
void RnWin32View::applyVisibility() {
|
|
272
|
+
hidden_ = hiddenByApp_ || hiddenByBackFace_;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
void RnWin32View::updateBackFace() {
|
|
276
|
+
// Recomputed rather than returned early on, so that turning the prop off
|
|
277
|
+
// shows a view it had hidden instead of leaving it hidden forever.
|
|
278
|
+
hiddenByBackFace_ =
|
|
279
|
+
hidesBackFace_ &&
|
|
280
|
+
basalt::facesAway(transform_[0], transform_[1], transform_[2], transform_[3]);
|
|
281
|
+
applyVisibility();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
void RnWin32View::setClipsChildren(bool clips) {
|
|
285
|
+
clipsChildren_ = clips;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
void RnWin32View::setCornerRadius(float radius) {
|
|
289
|
+
const float radii[8] = {radius, radius, radius, radius, radius, radius, radius, radius};
|
|
290
|
+
setCornerRadii(radii);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
void RnWin32View::setCornerRadii(const float radii[8]) {
|
|
294
|
+
hasCornerRadii_ = false;
|
|
295
|
+
for (int i = 0; i < 8; i++) {
|
|
296
|
+
cornerRadii_[i] = radii != nullptr ? radii[i] : 0.0f;
|
|
297
|
+
if (cornerRadii_[i] > 0.0f) {
|
|
298
|
+
hasCornerRadii_ = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
void RnWin32View::setBorders(const float widths[4], const float colours[16]) {
|
|
304
|
+
// "Has a border" is decided here, once, the way rn_view_set_borders decides
|
|
305
|
+
// it on GTK: some edge both wide and visible. Paint and the tree dump both
|
|
306
|
+
// read this flag, so they cannot disagree about whether a border exists.
|
|
307
|
+
hasBorders_ = false;
|
|
308
|
+
for (int i = 0; i < 4; i++) {
|
|
309
|
+
borderWidths_[i] = widths != nullptr ? widths[i] : 0.0f;
|
|
310
|
+
for (int c = 0; c < 4; c++) {
|
|
311
|
+
borderColours_[i * 4 + c] = colours != nullptr ? colours[i * 4 + c] : 0.0f;
|
|
312
|
+
}
|
|
313
|
+
if (borderWidths_[i] > 0.0f && borderColours_[i * 4 + 3] > 0.0f) {
|
|
314
|
+
hasBorders_ = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
void RnWin32View::setZIndex(int zIndex) {
|
|
320
|
+
zIndex_ = zIndex;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
void RnWin32View::setHidden(bool hidden) {
|
|
324
|
+
hiddenByApp_ = hidden;
|
|
325
|
+
applyVisibility();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// --- Content ----------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
void RnWin32View::setTextSelectable(bool selectable) {
|
|
331
|
+
if (textSelectable_ == selectable) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
textSelectable_ = selectable;
|
|
335
|
+
// A paragraph that stops being selectable keeps no highlight: the prop going
|
|
336
|
+
// false is an app saying this text is not to be selected, and leaving the
|
|
337
|
+
// last selection drawn would be the one state nothing can clear.
|
|
338
|
+
// No repaint request here, for the reason `setImage` makes none: this host
|
|
339
|
+
// invalidates once per mounted transaction rather than per view, and a
|
|
340
|
+
// selection changed by the pointer is repainted by whoever changed it. See
|
|
341
|
+
// Win32TouchDispatcher's repaint requester.
|
|
342
|
+
if (!selectable && textLayout_ != nullptr && textLayout_->selectionLength() > 0) {
|
|
343
|
+
textLayout_->setSelection(0, 0);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
int RnWin32View::textIndexAtPoint(float x, float y) const {
|
|
348
|
+
if (textLayout_ == nullptr) {
|
|
349
|
+
return -1;
|
|
350
|
+
}
|
|
351
|
+
// In the paragraph's own coordinates: the view's, minus its padding. The same
|
|
352
|
+
// move the draw makes, or a drag would select the character beside the one
|
|
353
|
+
// under the pointer.
|
|
354
|
+
return static_cast<int>(textLayout_->indexAtPoint(x - textInsetLeft_,
|
|
355
|
+
y - textInsetTop_,
|
|
356
|
+
frame_.width - textInsetLeft_ -
|
|
357
|
+
textInsetRight_,
|
|
358
|
+
frame_.height - textInsetTop_ -
|
|
359
|
+
textInsetBottom_));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
float RnWin32View::textVerticalOffset() const {
|
|
363
|
+
if (textLayout_ == nullptr) {
|
|
364
|
+
return 0.0f;
|
|
365
|
+
}
|
|
366
|
+
return textLayout_->verticalOffsetFor(frame_.width - textInsetLeft_ - textInsetRight_,
|
|
367
|
+
frame_.height - textInsetTop_ - textInsetBottom_);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
void RnWin32View::setScrollSnap(std::string description) {
|
|
371
|
+
scrollSnap_ = std::move(description);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
void RnWin32View::setTextInset(float left, float top, float right, float bottom) {
|
|
375
|
+
// No repaint request here, for the reason `setImage` makes none: this host
|
|
376
|
+
// invalidates once per mounted transaction, and this is only ever set during
|
|
377
|
+
// one.
|
|
378
|
+
textInsetLeft_ = left;
|
|
379
|
+
textInsetTop_ = top;
|
|
380
|
+
textInsetRight_ = right;
|
|
381
|
+
textInsetBottom_ = bottom;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
void RnWin32View::setTextSelection(int start, int length) {
|
|
385
|
+
if (textLayout_ == nullptr) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const unsigned from = static_cast<unsigned>(std::max(0, start));
|
|
389
|
+
const unsigned count = static_cast<unsigned>(std::max(0, length));
|
|
390
|
+
if (textLayout_->selectionStart() == from && textLayout_->selectionLength() == count) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
textLayout_->setSelection(from, count);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
int RnWin32View::textSelectionStart() const {
|
|
397
|
+
return textLayout_ == nullptr ? 0 : static_cast<int>(textLayout_->selectionStart());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
int RnWin32View::textSelectionLength() const {
|
|
401
|
+
return textLayout_ == nullptr ? 0 : static_cast<int>(textLayout_->selectionLength());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
std::string RnWin32View::selectedText() const {
|
|
405
|
+
return textLayout_ == nullptr ? std::string{} : textLayout_->selectedText();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
void RnWin32View::setParagraphNames(const char *writingDirection,
|
|
409
|
+
const char *textAlign,
|
|
410
|
+
const char *textVerticalAlign) {
|
|
411
|
+
// Literals from core, not copies, the way the GTK host stores the same three.
|
|
412
|
+
writingDirectionName_ = writingDirection;
|
|
413
|
+
textAlignName_ = textAlign;
|
|
414
|
+
textVerticalAlignName_ = textVerticalAlign;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
void RnWin32View::setTextLayout(std::shared_ptr<RnWin32TextLayout> layout) {
|
|
418
|
+
// A selection survives a rebuild of the same text and not a change to it.
|
|
419
|
+
// Every mutation that touches a paragraph builds a new layout -- a parent
|
|
420
|
+
// re-rendering is enough -- so dropping the selection each time would make it
|
|
421
|
+
// impossible to keep one in a live app; keeping it across *different* text
|
|
422
|
+
// would highlight whatever now sits at those offsets. The other two hosts
|
|
423
|
+
// draw the same line in their own setters.
|
|
424
|
+
if (textLayout_ != nullptr && layout != nullptr && textLayout_->selectionLength() > 0 &&
|
|
425
|
+
textLayout_->text() == layout->text()) {
|
|
426
|
+
layout->setSelection(textLayout_->selectionStart(), textLayout_->selectionLength());
|
|
427
|
+
}
|
|
428
|
+
textLayout_ = std::move(layout);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
void RnWin32View::setImageTint(bool hasTint, const float components[4]) {
|
|
432
|
+
// No repaint request here, for the same reason `setImage` makes none: this
|
|
433
|
+
// host invalidates once per mounted transaction rather than per view. See
|
|
434
|
+
// requestRepaint in main_win32.cpp.
|
|
435
|
+
hasImageTint_ = hasTint;
|
|
436
|
+
if (hasTint && components != nullptr) {
|
|
437
|
+
for (int i = 0; i < 4; i++) {
|
|
438
|
+
imageTint_[i] = components[i];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
void RnWin32View::setImageBlur(float radius) {
|
|
444
|
+
// Negative is no blur rather than a crash, which nothing stops an app from
|
|
445
|
+
// sending: the other two hosts clamp it for the same reason, and GSK would
|
|
446
|
+
// otherwise take it.
|
|
447
|
+
imageBlur_ = radius > 0.0f ? radius : 0.0f;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
void RnWin32View::setImage(std::shared_ptr<RnWin32Image> image, RnImageFit fit) {
|
|
451
|
+
imageFit_ = fit;
|
|
452
|
+
// An animation owns `image_`, and the mounting manager re-applies the first
|
|
453
|
+
// frame on every mutation that touched this view, a layout-only one included.
|
|
454
|
+
// Taking it would restart the GIF on every resize, so the fit is all that is
|
|
455
|
+
// taken here.
|
|
456
|
+
if (imageFrames_.size() > 1 && image == imageFrames_.front()) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
image_ = std::move(image);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
void RnWin32View::setImageFrames(std::vector<std::shared_ptr<RnWin32Image>> frames,
|
|
463
|
+
std::vector<unsigned> delaysMs,
|
|
464
|
+
unsigned loopCount) {
|
|
465
|
+
const bool same = frames.size() == imageFrames_.size() && !frames.empty()
|
|
466
|
+
&& frames.front() == imageFrames_.front() && loopCount == imageLoopCount_;
|
|
467
|
+
if (same) {
|
|
468
|
+
// The same animation again, which is what a layout-only mutation produces:
|
|
469
|
+
// the loader hands back the vector it cached, so this compares the first
|
|
470
|
+
// frame and keeps the animation's place rather than starting over.
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
imageFrames_ = std::move(frames);
|
|
475
|
+
imageDelays_ = std::move(delaysMs);
|
|
476
|
+
imageLoopCount_ = loopCount;
|
|
477
|
+
imageFrame_ = 0;
|
|
478
|
+
imageElapsedMs_ = 0.0;
|
|
479
|
+
if (imageFrames_.size() < 2 || imageDelays_.size() != imageFrames_.size()) {
|
|
480
|
+
imageFrames_.clear();
|
|
481
|
+
imageDelays_.clear();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
image_ = imageFrames_.front();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
double RnWin32View::advanceImageAnimation(double milliseconds) {
|
|
488
|
+
if (imageFrames_.size() < 2) {
|
|
489
|
+
return 0.0;
|
|
490
|
+
}
|
|
491
|
+
if (milliseconds > 0.0) {
|
|
492
|
+
imageElapsedMs_ += milliseconds;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const basalt::ImageAnimationStep step = basalt::imageAnimationStep(
|
|
496
|
+
imageDelays_, imageLoopCount_, static_cast<std::uint64_t>(imageElapsedMs_));
|
|
497
|
+
if (step.frame != imageFrame_ && step.frame < imageFrames_.size()) {
|
|
498
|
+
imageFrame_ = step.frame;
|
|
499
|
+
image_ = imageFrames_[step.frame];
|
|
500
|
+
}
|
|
501
|
+
return step.nextInMs;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
bool RnWin32View::advanceImageAnimations(double milliseconds) {
|
|
505
|
+
bool animating = advanceImageAnimation(milliseconds) > 0.0;
|
|
506
|
+
for (RnWin32View *child : children_) {
|
|
507
|
+
if (child != nullptr && child->advanceImageAnimations(milliseconds)) {
|
|
508
|
+
animating = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return animating;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
bool RnWin32View::hasAnimatedImage() const {
|
|
515
|
+
if (imageFrames_.size() > 1) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
for (const RnWin32View *child : children_) {
|
|
519
|
+
if (child != nullptr && child->hasAnimatedImage()) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
void RnWin32View::setPainter(std::shared_ptr<RnWin32Painter> painter) {
|
|
527
|
+
painter_ = std::move(painter);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// --- Accessibility ----------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
void RnWin32View::setAccessibleInfo(const RnAccessibleInfo &info) {
|
|
533
|
+
accessible_ = info;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
IRawElementProviderSimple *RnWin32View::createAccessibleProvider() const {
|
|
537
|
+
// The relation is resolved here rather than stored resolved, because the
|
|
538
|
+
// provider holds a snapshot and this is the moment it is taken: the view it
|
|
539
|
+
// points at is alive now, and whatever its label says now is what a client
|
|
540
|
+
// asking now should be told.
|
|
541
|
+
if (!labelledBy_.empty() && labelledBy_.front() != nullptr) {
|
|
542
|
+
RnAccessibleInfo info = accessible_;
|
|
543
|
+
info.labelledBy =
|
|
544
|
+
std::make_shared<const RnAccessibleInfo>(labelledBy_.front()->accessibleInfo());
|
|
545
|
+
return basalt::win32::createAccessibleProvider(info);
|
|
546
|
+
}
|
|
547
|
+
return basalt::win32::createAccessibleProvider(accessible_);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
void RnWin32View::setTextChecking(const char *spellCheck, const char *autoCorrect) {
|
|
551
|
+
spellCheck_ = spellCheck != nullptr ? spellCheck : "";
|
|
552
|
+
autoCorrect_ = autoCorrect != nullptr ? autoCorrect : "";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
void RnWin32View::setInputHiding(bool caretHidden, bool contextMenuHidden) {
|
|
556
|
+
caretHidden_ = caretHidden;
|
|
557
|
+
contextMenuHidden_ = contextMenuHidden;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
void RnWin32View::setInputKinds(const char *autoCapitalize, const char *keyboardType) {
|
|
561
|
+
autoCapitalize_ = autoCapitalize != nullptr ? autoCapitalize : "";
|
|
562
|
+
keyboardType_ = keyboardType != nullptr ? keyboardType : "";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
void RnWin32View::setLabelledBy(std::vector<RnWin32View *> labels) {
|
|
566
|
+
labelledBy_ = std::move(labels);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
void RnWin32View::collectTextInto(std::string &out) const {
|
|
570
|
+
if (textLayout_ != nullptr) {
|
|
571
|
+
const std::string &text = textLayout_->text();
|
|
572
|
+
if (!text.empty()) {
|
|
573
|
+
if (!out.empty()) {
|
|
574
|
+
out += ' ';
|
|
575
|
+
}
|
|
576
|
+
out += text;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const RnWin32View *child : children_) {
|
|
580
|
+
if (child != nullptr) {
|
|
581
|
+
child->collectTextInto(out);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
std::string RnWin32View::collectText() const {
|
|
587
|
+
std::string out;
|
|
588
|
+
collectTextInto(out);
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
void RnWin32View::announce(const std::string &text, bool assertive) {
|
|
593
|
+
if (text.empty()) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
lastAnnouncement_ = text;
|
|
597
|
+
|
|
598
|
+
// A provider to raise it on, which is also the one thing UIA needs that the
|
|
599
|
+
// other two hosts do not: there is no view object a client knows about, only
|
|
600
|
+
// what a provider says. Released straight after -- the event carries the
|
|
601
|
+
// string, and nothing keeps the element.
|
|
602
|
+
ComPtr<IRawElementProviderSimple> provider;
|
|
603
|
+
provider.Attach(createAccessibleProvider());
|
|
604
|
+
if (!provider) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Real BSTRs rather than a cast wide pointer: a BSTR carries its length ahead
|
|
609
|
+
// of its characters, and UIA reads that length. A `wchar_t *` cast to BSTR
|
|
610
|
+
// works until something asks how long it is.
|
|
611
|
+
const BSTR spoken = SysAllocString(widen(text).c_str());
|
|
612
|
+
const BSTR activity = SysAllocString(std::to_wstring(tag_).c_str());
|
|
613
|
+
if (spoken != nullptr && activity != nullptr) {
|
|
614
|
+
// `NotificationProcessing_ImportantAll` for an assertive region and
|
|
615
|
+
// `NotificationProcessing_All` for a polite one, which is as close as UIA's
|
|
616
|
+
// four processing hints come to ARIA's two politeness levels: both say
|
|
617
|
+
// "read every one of these", and the important variant jumps the queue.
|
|
618
|
+
//
|
|
619
|
+
// `NotificationKind_ActionCompleted` is the kind for "something finished
|
|
620
|
+
// and here is the result", which is what a status line saying `Saved` is.
|
|
621
|
+
// The activity id groups notifications from one source; this host uses the
|
|
622
|
+
// view's tag, so two regions do not cancel each other out.
|
|
623
|
+
UiaRaiseNotificationEvent(
|
|
624
|
+
provider.Get(),
|
|
625
|
+
NotificationKind_ActionCompleted,
|
|
626
|
+
assertive ? NotificationProcessing_ImportantAll : NotificationProcessing_All,
|
|
627
|
+
spoken,
|
|
628
|
+
activity);
|
|
629
|
+
}
|
|
630
|
+
SysFreeString(spoken);
|
|
631
|
+
SysFreeString(activity);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// --- Geometry, resolved -----------------------------------------------------
|
|
635
|
+
|
|
636
|
+
void RnWin32View::localToParent(float out[6]) const {
|
|
637
|
+
const float translation[6] = {1.0f, 0.0f, 0.0f, 1.0f, frame_.x, frame_.y};
|
|
638
|
+
if (!hasTransform_) {
|
|
639
|
+
for (int i = 0; i < 6; i++) {
|
|
640
|
+
out[i] = translation[i];
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Anchored at the view's centre by moving the centre to the origin and back
|
|
646
|
+
// around the transform, which is what React Native means by an untouched
|
|
647
|
+
// `transformOrigin` and what every other platform here does. The frame's
|
|
648
|
+
// translation comes last because this composes "apply a, then b".
|
|
649
|
+
const float centreX = frame_.width / 2.0f;
|
|
650
|
+
const float centreY = frame_.height / 2.0f;
|
|
651
|
+
const float toOrigin[6] = {1.0f, 0.0f, 0.0f, 1.0f, -centreX, -centreY};
|
|
652
|
+
const float fromOrigin[6] = {1.0f, 0.0f, 0.0f, 1.0f, centreX, centreY};
|
|
653
|
+
|
|
654
|
+
float composed[6];
|
|
655
|
+
compose(toOrigin, transform_, composed);
|
|
656
|
+
compose(composed, fromOrigin, composed);
|
|
657
|
+
compose(composed, translation, out);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
bool RnWin32View::pageToLocal(const RnWin32View *root,
|
|
661
|
+
float pageX,
|
|
662
|
+
float pageY,
|
|
663
|
+
float &outX,
|
|
664
|
+
float &outY) const {
|
|
665
|
+
float toPage[6] = {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f};
|
|
666
|
+
|
|
667
|
+
for (const RnWin32View *node = this; node != nullptr && node != root;) {
|
|
668
|
+
float local[6];
|
|
669
|
+
node->localToParent(local);
|
|
670
|
+
compose(toPage, local, toPage);
|
|
671
|
+
|
|
672
|
+
const RnWin32View *parent = node->parent();
|
|
673
|
+
if (parent == nullptr) {
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
// A scrolled ancestor moves its children by its offset, which is not part
|
|
677
|
+
// of any child's own placement -- the same offset `paintChildren`
|
|
678
|
+
// subtracts and `hitTest` adds back.
|
|
679
|
+
const float scroll[6] = {1.0f,
|
|
680
|
+
0.0f,
|
|
681
|
+
0.0f,
|
|
682
|
+
1.0f,
|
|
683
|
+
-parent->scrollX(),
|
|
684
|
+
-parent->scrollY() + parent->textVerticalOffset()};
|
|
685
|
+
compose(toPage, scroll, toPage);
|
|
686
|
+
node = parent;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return invertPoint(toPage, pageX, pageY, outX, outY);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
std::vector<RnWin32View *> RnWin32View::childrenInPaintOrder() const {
|
|
693
|
+
std::vector<RnWin32View *> ordered = children_;
|
|
694
|
+
const bool needsSorting =
|
|
695
|
+
std::any_of(ordered.begin(), ordered.end(), [](const RnWin32View *child) {
|
|
696
|
+
return child->zIndex() != 0;
|
|
697
|
+
});
|
|
698
|
+
if (needsSorting) {
|
|
699
|
+
// Stable, so equal zIndex keeps document order -- which is what CSS and
|
|
700
|
+
// React Native both promise.
|
|
701
|
+
std::stable_sort(
|
|
702
|
+
ordered.begin(), ordered.end(), [](const RnWin32View *a, const RnWin32View *b) {
|
|
703
|
+
return a->zIndex() < b->zIndex();
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return ordered;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// --- Tree ------------------------------------------------------------------
|
|
710
|
+
|
|
711
|
+
void RnWin32View::insertChild(RnWin32View *child, int index) {
|
|
712
|
+
if (child == nullptr || child == this) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (child->parent_ != nullptr) {
|
|
716
|
+
child->parent_->removeChild(child);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Fabric's index counts positions in the parent's *final* child list, so a
|
|
720
|
+
// later mutation lands between two existing children. Clamped rather than
|
|
721
|
+
// trusted: a transaction racing a surface teardown can name a position that
|
|
722
|
+
// no longer exists, and that has to be survivable.
|
|
723
|
+
const int count = static_cast<int>(children_.size());
|
|
724
|
+
const int at = std::clamp(index, 0, count);
|
|
725
|
+
children_.insert(children_.begin() + at, child);
|
|
726
|
+
child->parent_ = this;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
void RnWin32View::removeChild(RnWin32View *child) {
|
|
730
|
+
const auto it = std::find(children_.begin(), children_.end(), child);
|
|
731
|
+
if (it == children_.end()) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
(*it)->parent_ = nullptr;
|
|
735
|
+
children_.erase(it);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// --- Painting --------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
void RnWin32View::paint(ID2D1RenderTarget *target) const {
|
|
741
|
+
if (target == nullptr || hidden_) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
D2D1::Matrix3x2F parentTransform;
|
|
746
|
+
target->GetTransform(&parentTransform);
|
|
747
|
+
|
|
748
|
+
// From localToParent rather than composed here, so that hit testing -- which
|
|
749
|
+
// inverts the same six numbers -- cannot end up with a different idea of
|
|
750
|
+
// where this view is.
|
|
751
|
+
float localValues[6];
|
|
752
|
+
localToParent(localValues);
|
|
753
|
+
const D2D1::Matrix3x2F local(localValues[0],
|
|
754
|
+
localValues[1],
|
|
755
|
+
localValues[2],
|
|
756
|
+
localValues[3],
|
|
757
|
+
localValues[4],
|
|
758
|
+
localValues[5]);
|
|
759
|
+
target->SetTransform(local * parentTransform);
|
|
760
|
+
|
|
761
|
+
// `filter`, which applies to this view and everything inside it, as CSS says.
|
|
762
|
+
// That needs the subtree as a picture, so it goes through an offscreen bitmap
|
|
763
|
+
// and an effect graph; everything else draws straight onto the target.
|
|
764
|
+
//
|
|
765
|
+
// A blended child needs the same picture for a different reason: a blend
|
|
766
|
+
// takes the backdrop as an input, and the backdrop is this view's content
|
|
767
|
+
// plus whatever was painted before that child. Both cases are "the subtree
|
|
768
|
+
// into a bitmap first", which is what made `paintContents` worth splitting
|
|
769
|
+
// out; a view with both takes the filter's path and the blend is skipped
|
|
770
|
+
// inside it, see paintFiltered.
|
|
771
|
+
if (filters_.needsEffects()) {
|
|
772
|
+
paintFiltered(target);
|
|
773
|
+
} else if (hasBlendedChild()) {
|
|
774
|
+
paintBlendLayer(target);
|
|
775
|
+
} else {
|
|
776
|
+
paintContents(target);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
target->SetTransform(parentTransform);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
void RnWin32View::paintContents(ID2D1RenderTarget *target,
|
|
783
|
+
ID2D1BitmapRenderTarget *blendLayer) const {
|
|
784
|
+
const D2D1_RECT_F bounds = D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height);
|
|
785
|
+
|
|
786
|
+
// The transform this view's contents are drawn in, read rather than composed
|
|
787
|
+
// because the two callers set it differently: `paint` has applied this view's
|
|
788
|
+
// own matrix to the window's target, and `paintFiltered` draws into an
|
|
789
|
+
// offscreen bitmap that starts at the identity because the whole subtree goes
|
|
790
|
+
// there. Kept because `paintChildren` leaves a <ScrollView>'s offset applied
|
|
791
|
+
// and what comes after it does not scroll.
|
|
792
|
+
D2D1::Matrix3x2F contentTransform;
|
|
793
|
+
target->GetTransform(&contentTransform);
|
|
794
|
+
|
|
795
|
+
{
|
|
796
|
+
// `filter`'s own `opacity()` multiplies the view's: CSS has two ways to ask
|
|
797
|
+
// for the same thing and an app can use both. Folded in here rather than
|
|
798
|
+
// being an effect of its own, which is what the GTK host does too.
|
|
799
|
+
// No layer when the subtree is going through one of its own: a pushed
|
|
800
|
+
// layer holds everything drawn inside it in an intermediate surface, and
|
|
801
|
+
// the blended children below read the target's bitmap. `paintBlendLayer`
|
|
802
|
+
// applies the opacity to that bitmap instead, which is the same picture.
|
|
803
|
+
const ScopedOpacity fade(target,
|
|
804
|
+
blendLayer != nullptr ? 1.0f : opacity_ * filters_.opacity);
|
|
805
|
+
|
|
806
|
+
// The outset shadows, behind everything this view draws: CSS puts them
|
|
807
|
+
// under the background, which is also the only place they can go without
|
|
808
|
+
// showing through a translucent one.
|
|
809
|
+
paintBoxShadows(target, false);
|
|
810
|
+
|
|
811
|
+
// The background is always clipped to the rounded box, even when children
|
|
812
|
+
// are not: `overflow: visible` lets a child escape the corner, but the
|
|
813
|
+
// view's own fill still has to respect its border radius.
|
|
814
|
+
if (hasBackgroundColor_) {
|
|
815
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
816
|
+
// An explicit sRGB colour. Letting the target pick a device space shifts
|
|
817
|
+
// every colour slightly on a wide-gamut display -- not wrong exactly, but
|
|
818
|
+
// different from the same app on Linux, which is the one thing this
|
|
819
|
+
// project is trying not to be.
|
|
820
|
+
const D2D1_COLOR_F colour = D2D1::ColorF(
|
|
821
|
+
backgroundColor_[0], backgroundColor_[1], backgroundColor_[2], backgroundColor_[3]);
|
|
822
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
823
|
+
if (hasCornerRadii_) {
|
|
824
|
+
// The real shape: each corner its own, elliptical where React Native
|
|
825
|
+
// says so. See roundedBoxGeometry in Win32Clip.h.
|
|
826
|
+
ComPtr<ID2D1Factory> factory;
|
|
827
|
+
target->GetFactory(factory.GetAddressOf());
|
|
828
|
+
const ComPtr<ID2D1Geometry> box = roundedBoxGeometry(factory.Get(), bounds, cornerRadii_);
|
|
829
|
+
if (box) {
|
|
830
|
+
target->FillGeometry(box.Get(), brush.Get());
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
target->FillRectangle(bounds, brush.Get());
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// The `backgroundImage` gradients, over the background colour and under the
|
|
839
|
+
// content: CSS's background-image is above its background-color.
|
|
840
|
+
paintGradients(target);
|
|
841
|
+
|
|
842
|
+
// The inset shadows, over the background and under the content, which is
|
|
843
|
+
// where CSS puts them. (AppKit gets this one wrong and says so: a sublayer
|
|
844
|
+
// is above the view's own text.)
|
|
845
|
+
paintBoxShadows(target, true);
|
|
846
|
+
|
|
847
|
+
// Then the image, then the text, then the children -- the order
|
|
848
|
+
// `rn_view_snapshot` uses on GTK. Nothing in React Native puts two of these
|
|
849
|
+
// on one view, but the order still has to be decided somewhere, and it is
|
|
850
|
+
// cheaper to match than to argue about later.
|
|
851
|
+
if (image_ != nullptr) {
|
|
852
|
+
// The four floats as stored; RnWin32Image builds the Direct2D colour,
|
|
853
|
+
// where d2d1.h is in scope. See its header for why it takes floats.
|
|
854
|
+
image_->draw(target,
|
|
855
|
+
frame_.width,
|
|
856
|
+
frame_.height,
|
|
857
|
+
imageFit_,
|
|
858
|
+
hasImageTint_ ? imageTint_ : nullptr,
|
|
859
|
+
imageBlur_);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Then anything this layer cannot draw itself -- the Skia surface behind a
|
|
863
|
+
// `<Canvas>`, and nothing else so far. Here rather than in its own pass so
|
|
864
|
+
// that it is inside the clip, the transform and the opacity this view has
|
|
865
|
+
// already applied, which is the whole point of drawing it in the walk.
|
|
866
|
+
if (painter_ != nullptr) {
|
|
867
|
+
painter_->draw(target, frame_.width, frame_.height);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Text sits above the background and below any children, which is the
|
|
871
|
+
// order `<Text>` with nested views expects. The paragraph draws itself at
|
|
872
|
+
// the view's *content* origin, in the box Yoga gave it minus its padding
|
|
873
|
+
// and border -- the same box it was measured against, because both go
|
|
874
|
+
// through RnWin32TextLayout. See setTextInset.
|
|
875
|
+
if (textLayout_ != nullptr) {
|
|
876
|
+
const bool inset = textInsetLeft_ != 0.0f || textInsetTop_ != 0.0f;
|
|
877
|
+
if (inset) {
|
|
878
|
+
target->SetTransform(
|
|
879
|
+
D2D1::Matrix3x2F::Translation(textInsetLeft_, textInsetTop_) * contentTransform);
|
|
880
|
+
}
|
|
881
|
+
textLayout_->draw(target,
|
|
882
|
+
frame_.width - textInsetLeft_ - textInsetRight_,
|
|
883
|
+
frame_.height - textInsetTop_ - textInsetBottom_);
|
|
884
|
+
if (inset) {
|
|
885
|
+
target->SetTransform(contentTransform);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// A control sits where the text would: above the background, below the
|
|
890
|
+
// children. Nothing in React Native puts children inside a <Switch>, but
|
|
891
|
+
// the order still has to be decided somewhere.
|
|
892
|
+
if (control_ != Control::None) {
|
|
893
|
+
paintControl(target);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
paintChildren(target, blendLayer);
|
|
897
|
+
|
|
898
|
+
// paintChildren leaves a ScrollView's offset applied, and nothing below
|
|
899
|
+
// here scrolls with the content -- so put the transform back to this view's
|
|
900
|
+
// own before drawing any of it.
|
|
901
|
+
target->SetTransform(contentTransform);
|
|
902
|
+
|
|
903
|
+
// The scrollbars: an overlay, so above the content and above any children,
|
|
904
|
+
// which is what "overlay indicator" means -- and below the DevTools overlay
|
|
905
|
+
// and the focus ring, neither of which belongs to the app.
|
|
906
|
+
if (indicatorVerticalLength_ > 0.0f || indicatorHorizontalLength_ > 0.0f) {
|
|
907
|
+
paintScrollIndicators(target);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// The border: over the content, as GTK and AppKit draw it, and inside the
|
|
911
|
+
// view's own rounded box rather than around it -- React Native's border is
|
|
912
|
+
// part of the box, which is why Yoga has already inset the content by it.
|
|
913
|
+
// Below the DevTools overlay and the focus ring, which are not the app's.
|
|
914
|
+
if (hasBorders_) {
|
|
915
|
+
paintBorders(target);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// The outline, outside the box and over the border: it is not a border, and
|
|
919
|
+
// it sits `offset` away from the box's edge. After the children for the
|
|
920
|
+
// clipping reason in paintOutline, and below the DevTools overlay and the
|
|
921
|
+
// focus ring, which are not the app's.
|
|
922
|
+
paintOutline(target);
|
|
923
|
+
|
|
924
|
+
// React DevTools' overlay, over everything including the children. Above
|
|
925
|
+
// the app on purpose: it is not part of it, and an inspected element half
|
|
926
|
+
// hidden behind a card would be pointing at the wrong thing.
|
|
927
|
+
if (!highlights_.empty()) {
|
|
928
|
+
paintHighlights(target);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// The focus ring, over everything including the children, because it is the
|
|
932
|
+
// answer to "where am I" and must not be hidden by what it is drawn on.
|
|
933
|
+
//
|
|
934
|
+
// Inside the view's own bounds rather than around them, which is where both
|
|
935
|
+
// other hosts draw it -- AppKit's is clipped to the view and GTK's matches
|
|
936
|
+
// that on purpose. Stroked down the middle of the line, so the rectangle is
|
|
937
|
+
// inset by half of it to keep the whole ring inside.
|
|
938
|
+
if (showsFocusRing_) {
|
|
939
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
940
|
+
const D2D1_COLOR_F colour = D2D1::ColorF(
|
|
941
|
+
kFocusRingRed, kFocusRingGreen, kFocusRingBlue, kFocusRingAlpha);
|
|
942
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
943
|
+
const float inset = kFocusRingWidth / 2.0f;
|
|
944
|
+
const D2D1_RECT_F ring = D2D1::RectF(inset,
|
|
945
|
+
inset,
|
|
946
|
+
(std::max)(inset, frame_.width - inset),
|
|
947
|
+
(std::max)(inset, frame_.height - inset));
|
|
948
|
+
if (hasCornerRadii_) {
|
|
949
|
+
// The view's own shape, each radius pulled in by the inset so the
|
|
950
|
+
// ring follows the corner rather than cutting across it.
|
|
951
|
+
float insetRadii[8];
|
|
952
|
+
for (int i = 0; i < 8; i++) {
|
|
953
|
+
insetRadii[i] = (std::max)(0.0f, cornerRadii_[i] - inset);
|
|
954
|
+
}
|
|
955
|
+
ComPtr<ID2D1Factory> factory;
|
|
956
|
+
target->GetFactory(factory.GetAddressOf());
|
|
957
|
+
const ComPtr<ID2D1Geometry> outline =
|
|
958
|
+
roundedBoxGeometry(factory.Get(), ring, insetRadii);
|
|
959
|
+
if (outline) {
|
|
960
|
+
target->DrawGeometry(outline.Get(), brush.Get(), kFocusRingWidth);
|
|
961
|
+
}
|
|
962
|
+
} else {
|
|
963
|
+
target->DrawRectangle(ring, brush.Get(), kFocusRingWidth);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// The border, as the ring between the view's rounded box and that box inset by
|
|
971
|
+
// each edge's width.
|
|
972
|
+
//
|
|
973
|
+
// The inner corners follow CSS: each outer radius less the width of the edge
|
|
974
|
+
// it meets, and never below zero -- which is why a thick border on a small
|
|
975
|
+
// radius has a square inside, on every platform.
|
|
976
|
+
//
|
|
977
|
+
// One colour all round is one fill. Different colours per edge are how CSS
|
|
978
|
+
// draws them: the ring is split along the diagonals from each outer corner to
|
|
979
|
+
// the matching inner one, and each side is filled in its own colour. Those
|
|
980
|
+
// sides are filled aliased inside a layer whose mask is the anti-aliased ring:
|
|
981
|
+
// adjacent anti-aliased fills leave a faint seam down each diagonal where their
|
|
982
|
+
// coverages meet, and the mask already gives the ring's own edges their
|
|
983
|
+
// smoothing.
|
|
984
|
+
// The dash pattern for a style, in Direct2D's units.
|
|
985
|
+
//
|
|
986
|
+
// Direct2D counts dash lengths in multiples of the stroke width, where GSK and
|
|
987
|
+
// Core Animation take absolute ones -- so the other two hosts' {3w, 2w} is
|
|
988
|
+
// {3, 2} here, and theirs is scaled to the width for the reason a browser
|
|
989
|
+
// scales it: a fixed pattern reads as a hairline on a thick border and as a
|
|
990
|
+
// solid line on a thin one. Dotted is a zero-length dash with round caps,
|
|
991
|
+
// which is what makes a dot a dot rather than a short dash.
|
|
992
|
+
//
|
|
993
|
+
// Answers with the stroke style, or null for a solid line and for a failure,
|
|
994
|
+
// which draws solid: a ring drawn solid is wrong in a way a reader can see, and
|
|
995
|
+
// no ring at all looks like the prop being ignored.
|
|
996
|
+
static ComPtr<ID2D1StrokeStyle> dashStyleFor(ID2D1Factory *factory,
|
|
997
|
+
RnWin32View::LineStyle style) {
|
|
998
|
+
if (factory == nullptr || style == RnWin32View::LineStyle::Solid) {
|
|
999
|
+
return nullptr;
|
|
1000
|
+
}
|
|
1001
|
+
const bool dots = style == RnWin32View::LineStyle::Dotted;
|
|
1002
|
+
const D2D1_CAP_STYLE cap = dots ? D2D1_CAP_STYLE_ROUND : D2D1_CAP_STYLE_FLAT;
|
|
1003
|
+
const float pattern[2] = {dots ? 0.0f : 3.0f, 2.0f};
|
|
1004
|
+
const D2D1_STROKE_STYLE_PROPERTIES properties = D2D1::StrokeStyleProperties(
|
|
1005
|
+
cap, cap, cap, D2D1_LINE_JOIN_MITER, 10.0f, D2D1_DASH_STYLE_CUSTOM, 0.0f);
|
|
1006
|
+
|
|
1007
|
+
ComPtr<ID2D1StrokeStyle> stroke;
|
|
1008
|
+
if (FAILED(factory->CreateStrokeStyle(properties, pattern, 2, stroke.GetAddressOf()))) {
|
|
1009
|
+
return nullptr;
|
|
1010
|
+
}
|
|
1011
|
+
return stroke;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
void RnWin32View::setBorderStyle(LineStyle style) {
|
|
1015
|
+
borderStyle_ = style;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
void RnWin32View::setOutline(float width,
|
|
1019
|
+
float offset,
|
|
1020
|
+
const float colour[4],
|
|
1021
|
+
LineStyle style) {
|
|
1022
|
+
outlineWidth_ = width > 0.0f ? width : 0.0f;
|
|
1023
|
+
outlineOffset_ = offset;
|
|
1024
|
+
for (int component = 0; component < 4; component++) {
|
|
1025
|
+
outlineColour_[component] = colour != nullptr ? colour[component] : 0.0f;
|
|
1026
|
+
}
|
|
1027
|
+
outlineStyle_ = style;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
void RnWin32View::setFilters(const Filters &filters) {
|
|
1031
|
+
filters_ = filters;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// `filter`, as an effect graph over this subtree's own picture.
|
|
1035
|
+
//
|
|
1036
|
+
// CSS applies a filter to an element *and its descendants*, so the subtree has
|
|
1037
|
+
// to exist as an image before anything can be done to it: the contents go into
|
|
1038
|
+
// a compatible bitmap, the effects run over that, and the result is drawn where
|
|
1039
|
+
// the view is. The other two hosts get the same shape from their compositors,
|
|
1040
|
+
// GSK by pushing nodes and Core Animation by holding filters on the layer.
|
|
1041
|
+
//
|
|
1042
|
+
// The order is the one `core/Filters.h` settled and backlog/correctness.md
|
|
1043
|
+
// records: the colour matrix and the blur commute, so either way round is the
|
|
1044
|
+
// same picture, and the drop shadows are outermost, cast by the blurred and
|
|
1045
|
+
// recoloured result rather than recoloured themselves. That is CSS's
|
|
1046
|
+
// `grayscale(1) drop-shadow(...)` and not the other order, which is the limit
|
|
1047
|
+
// that header names.
|
|
1048
|
+
//
|
|
1049
|
+
// **A child that overflows a filtered view is cropped to the view's box here**,
|
|
1050
|
+
// because the bitmap is the view's own size. GSK blurs the overflow because its
|
|
1051
|
+
// node tree carries the subtree's real extent, which nothing in this host
|
|
1052
|
+
// computes; backlog/platform-windows.md records it rather than leaving it to be
|
|
1053
|
+
// discovered.
|
|
1054
|
+
void RnWin32View::paintFiltered(ID2D1RenderTarget *target) const {
|
|
1055
|
+
ComPtr<ID2D1DeviceContext> context;
|
|
1056
|
+
// With a format that keeps alpha rather than the window's, which has none:
|
|
1057
|
+
// every one of these effects is defined on what is *not* covered as much as
|
|
1058
|
+
// on what is. See Win32Offscreen.h.
|
|
1059
|
+
const ComPtr<ID2D1BitmapRenderTarget> offscreen =
|
|
1060
|
+
createOffscreen(target, frame_.width, frame_.height);
|
|
1061
|
+
if (FAILED(target->QueryInterface(IID_PPV_ARGS(&context))) || !offscreen) {
|
|
1062
|
+
// No device context, no effects: the unfiltered picture is the honest
|
|
1063
|
+
// answer, since a filter that cannot be applied should not take the view
|
|
1064
|
+
// with it.
|
|
1065
|
+
paintContents(target);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
offscreen->BeginDraw();
|
|
1070
|
+
offscreen->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
1071
|
+
paintContents(offscreen.Get());
|
|
1072
|
+
if (FAILED(offscreen->EndDraw())) {
|
|
1073
|
+
paintContents(target);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
ComPtr<ID2D1Bitmap> picture;
|
|
1078
|
+
if (FAILED(offscreen->GetBitmap(picture.GetAddressOf())) || !picture) {
|
|
1079
|
+
paintContents(target);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// The chain, built from the bitmap outwards. `source` is whatever the last
|
|
1084
|
+
// effect produced, or the bitmap when there is none. Each effect is held for
|
|
1085
|
+
// as long as the graph is drawn, which is why they are declared out here.
|
|
1086
|
+
ComPtr<ID2D1Effect> straighten;
|
|
1087
|
+
ComPtr<ID2D1Effect> matrix;
|
|
1088
|
+
ComPtr<ID2D1Effect> blur;
|
|
1089
|
+
ComPtr<ID2D1Image> source;
|
|
1090
|
+
picture->QueryInterface(IID_PPV_ARGS(&source));
|
|
1091
|
+
|
|
1092
|
+
if (filters_.hasMatrix
|
|
1093
|
+
&& SUCCEEDED(context->CreateEffect(CLSID_D2D1ColorMatrix, matrix.GetAddressOf()))
|
|
1094
|
+
&& matrix) {
|
|
1095
|
+
// Direct2D multiplies a *row* vector by the matrix, so its m(i, j) is the
|
|
1096
|
+
// weight of input channel i in output channel j -- the transpose of core's,
|
|
1097
|
+
// whose rows are outputs. The fifth row is the offset.
|
|
1098
|
+
D2D1_MATRIX_5X4_F wanted{};
|
|
1099
|
+
float *const cells = &wanted._11;
|
|
1100
|
+
for (int input = 0; input < 4; input++) {
|
|
1101
|
+
for (int output = 0; output < 4; output++) {
|
|
1102
|
+
cells[input * 4 + output] = filters_.matrix[output * 4 + input];
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
for (int output = 0; output < 4; output++) {
|
|
1106
|
+
cells[16 + output] = filters_.offset[output];
|
|
1107
|
+
}
|
|
1108
|
+
matrix->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, wanted);
|
|
1109
|
+
// Clamped, which is what every browser does with a matrix that leaves the
|
|
1110
|
+
// range.
|
|
1111
|
+
matrix->SetValue(D2D1_COLORMATRIX_PROP_CLAMP_OUTPUT, TRUE);
|
|
1112
|
+
|
|
1113
|
+
// **CSS's filters are defined on unpremultiplied colour**, and getting that
|
|
1114
|
+
// took two measurements rather than a reading of the documentation.
|
|
1115
|
+
//
|
|
1116
|
+
// `D2D1_COLORMATRIX_ALPHA_MODE_STRAIGHT` sounds like the answer and is not:
|
|
1117
|
+
// with it set, a half-transparent red whose alpha the matrix forces opaque
|
|
1118
|
+
// came out dark red, which is 0.5 read as a colour rather than as a colour
|
|
1119
|
+
// times a coverage. So that setting does not mean "convert for me"; it
|
|
1120
|
+
// means "the data is already straight, leave the alpha alone".
|
|
1121
|
+
//
|
|
1122
|
+
// What does convert is `CLSID_D2D1UnPremultiply` ahead of the matrix, with
|
|
1123
|
+
// the matrix then told PREMULTIPLIED -- which, read the same way, means
|
|
1124
|
+
// "premultiply the result on the way out". An explicit `Premultiply` effect
|
|
1125
|
+
// after it premultiplied a second time, which the second measurement
|
|
1126
|
+
// caught: a half-transparent view with an *identity* matrix came back at a
|
|
1127
|
+
// quarter of its colour.
|
|
1128
|
+
//
|
|
1129
|
+
// Both tests are kept, and they pin the pair from opposite ends: one has an
|
|
1130
|
+
// opaque result, where premultiplying is a no-op and only the conversion on
|
|
1131
|
+
// the way in shows; the other is transparent throughout, where only the
|
|
1132
|
+
// conversion on the way out does.
|
|
1133
|
+
matrix->SetValue(D2D1_COLORMATRIX_PROP_ALPHA_MODE,
|
|
1134
|
+
D2D1_COLORMATRIX_ALPHA_MODE_PREMULTIPLIED);
|
|
1135
|
+
if (SUCCEEDED(context->CreateEffect(CLSID_D2D1UnPremultiply, straighten.GetAddressOf()))
|
|
1136
|
+
&& straighten) {
|
|
1137
|
+
straighten->SetInput(0, source.Get());
|
|
1138
|
+
source.Reset();
|
|
1139
|
+
straighten->GetOutput(source.GetAddressOf());
|
|
1140
|
+
}
|
|
1141
|
+
matrix->SetInput(0, source.Get());
|
|
1142
|
+
source.Reset();
|
|
1143
|
+
matrix->GetOutput(source.GetAddressOf());
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (filters_.blurRadius > 0.0f
|
|
1147
|
+
&& SUCCEEDED(context->CreateEffect(CLSID_D2D1GaussianBlur, blur.GetAddressOf()))
|
|
1148
|
+
&& blur) {
|
|
1149
|
+
// Half the radius is the standard deviation, which is the conversion both
|
|
1150
|
+
// other hosts make and `core/Filters.h` documents.
|
|
1151
|
+
blur->SetValue(D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, filters_.blurRadius / 2.0f);
|
|
1152
|
+
blur->SetInput(0, source.Get());
|
|
1153
|
+
source.Reset();
|
|
1154
|
+
blur->GetOutput(source.GetAddressOf());
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// The shadows first, so they are behind the picture, and in the order they
|
|
1158
|
+
// were written: the first is the one nearest the content, which is the order
|
|
1159
|
+
// `core/Filters.h` keeps them in.
|
|
1160
|
+
for (const FilterShadow &shadow : filters_.shadows) {
|
|
1161
|
+
if (shadow.colour[3] <= 0.0f) {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
ComPtr<ID2D1Effect> cast;
|
|
1165
|
+
if (FAILED(context->CreateEffect(CLSID_D2D1Shadow, cast.GetAddressOf())) || !cast) {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
cast->SetInput(0, source.Get());
|
|
1169
|
+
cast->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, shadow.standardDeviation);
|
|
1170
|
+
const D2D1_VECTOR_4F tint{
|
|
1171
|
+
shadow.colour[0], shadow.colour[1], shadow.colour[2], shadow.colour[3]};
|
|
1172
|
+
cast->SetValue(D2D1_SHADOW_PROP_COLOR, tint);
|
|
1173
|
+
context->DrawImage(cast.Get(), D2D1::Point2F(shadow.dx, shadow.dy));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
context->DrawImage(source.Get(), D2D1::Point2F(0.0f, 0.0f));
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
void RnWin32View::setGradients(std::vector<Gradient> gradients) {
|
|
1180
|
+
gradients_ = std::move(gradients);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// The `backgroundImage` gradients, above the background colour and below the
|
|
1184
|
+
// content, which is where CSS paints a background image.
|
|
1185
|
+
//
|
|
1186
|
+
// Back to front, the first in the list being the one on top, and clipped to the
|
|
1187
|
+
// view's border box -- CSS's painting area -- which is also what keeps a
|
|
1188
|
+
// gradient from squaring off a rounded corner.
|
|
1189
|
+
//
|
|
1190
|
+
// The geometry and the stops were resolved in the mounting manager against the
|
|
1191
|
+
// *image's* size and offset to where the image goes, so this draws a gradient
|
|
1192
|
+
// and does no CSS: `area` is the rectangle it fills and `tile` is the period it
|
|
1193
|
+
// repeats in. An axis that does not repeat arrives with the painting area as
|
|
1194
|
+
// its tile, which comes out drawn once; see core/BackgroundLayers.h.
|
|
1195
|
+
void RnWin32View::paintGradients(ID2D1RenderTarget *target) const {
|
|
1196
|
+
if (gradients_.empty()) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const D2D1_RECT_F box = D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height);
|
|
1200
|
+
|
|
1201
|
+
for (size_t index = gradients_.size(); index > 0; index--) {
|
|
1202
|
+
const Gradient &gradient = gradients_[index - 1];
|
|
1203
|
+
if (gradient.stops.empty() || gradient.area[2] <= 0.0f || gradient.area[3] <= 0.0f) {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
std::vector<D2D1_GRADIENT_STOP> stops;
|
|
1208
|
+
stops.reserve(gradient.stops.size());
|
|
1209
|
+
for (const GradientStop &stop : gradient.stops) {
|
|
1210
|
+
stops.push_back(D2D1_GRADIENT_STOP{
|
|
1211
|
+
stop.offset,
|
|
1212
|
+
D2D1::ColorF(stop.colour[0], stop.colour[1], stop.colour[2], stop.colour[3])});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// `D2D1_GAMMA_2_2` interpolates in sRGB rather than in linear light, which
|
|
1216
|
+
// is what CSS says and what the other two hosts do: GSK and Core Graphics
|
|
1217
|
+
// both interpolate the values as given. Linear light would be defensible
|
|
1218
|
+
// and would make the same stylesheet a different picture here.
|
|
1219
|
+
//
|
|
1220
|
+
// Clamped at both ends, which is also CSS: past the last stop the gradient
|
|
1221
|
+
// is that stop's colour, and that is what fills the rest of the image's
|
|
1222
|
+
// rectangle -- the ending shape of a radial gradient is the radius, not the
|
|
1223
|
+
// edge of the box.
|
|
1224
|
+
ComPtr<ID2D1GradientStopCollection> collection;
|
|
1225
|
+
if (FAILED(target->CreateGradientStopCollection(stops.data(),
|
|
1226
|
+
static_cast<UINT32>(stops.size()),
|
|
1227
|
+
D2D1_GAMMA_2_2,
|
|
1228
|
+
D2D1_EXTEND_MODE_CLAMP,
|
|
1229
|
+
collection.GetAddressOf()))
|
|
1230
|
+
|| !collection) {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
ComPtr<ID2D1Brush> brush;
|
|
1235
|
+
if (gradient.kind == Gradient::Kind::Radial) {
|
|
1236
|
+
ComPtr<ID2D1RadialGradientBrush> radial;
|
|
1237
|
+
// The centre and two radii straight from `core/Gradients.h`: Direct2D's
|
|
1238
|
+
// brush takes an ellipse where Core Graphics takes a circle and a
|
|
1239
|
+
// transform, so this is the host that needs no coordinate scaling.
|
|
1240
|
+
const D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES properties =
|
|
1241
|
+
D2D1::RadialGradientBrushProperties(
|
|
1242
|
+
D2D1::Point2F(gradient.centreX, gradient.centreY),
|
|
1243
|
+
D2D1::Point2F(0.0f, 0.0f),
|
|
1244
|
+
gradient.radiusX,
|
|
1245
|
+
gradient.radiusY);
|
|
1246
|
+
if (SUCCEEDED(target->CreateRadialGradientBrush(
|
|
1247
|
+
properties, collection.Get(), radial.GetAddressOf()))) {
|
|
1248
|
+
brush = radial;
|
|
1249
|
+
}
|
|
1250
|
+
} else {
|
|
1251
|
+
ComPtr<ID2D1LinearGradientBrush> linear;
|
|
1252
|
+
const D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES properties =
|
|
1253
|
+
D2D1::LinearGradientBrushProperties(
|
|
1254
|
+
D2D1::Point2F(gradient.startX, gradient.startY),
|
|
1255
|
+
D2D1::Point2F(gradient.endX, gradient.endY));
|
|
1256
|
+
if (SUCCEEDED(target->CreateLinearGradientBrush(
|
|
1257
|
+
properties, collection.Get(), linear.GetAddressOf()))) {
|
|
1258
|
+
brush = linear;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (!brush) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Always clipped, radii or not: CSS clips a background to the border box,
|
|
1266
|
+
// and `backgroundSize` and `backgroundPosition` can both put the image
|
|
1267
|
+
// outside it.
|
|
1268
|
+
const ScopedGeometryClip clip(target, box, cornerRadii_);
|
|
1269
|
+
|
|
1270
|
+
const D2D1_RECT_F area = D2D1::RectF(gradient.area[0],
|
|
1271
|
+
gradient.area[1],
|
|
1272
|
+
gradient.area[0] + gradient.area[2],
|
|
1273
|
+
gradient.area[1] + gradient.area[3]);
|
|
1274
|
+
if (!gradient.repeats || gradient.tile[2] <= 0.0f || gradient.tile[3] <= 0.0f) {
|
|
1275
|
+
target->FillRectangle(area, brush.Get());
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Tiled. The gradient's own geometry sits where the first tile is, so each
|
|
1280
|
+
// tile moves the *brush* rather than the gradient: one brush, one fill per
|
|
1281
|
+
// tile, which is the shape the AppKit host settled on. GSK has a repeat
|
|
1282
|
+
// node and needs none of this.
|
|
1283
|
+
//
|
|
1284
|
+
// From the first tile that can reach the box, which may be above and left
|
|
1285
|
+
// of it: `backgroundPosition` can put the first tile anywhere, and the ones
|
|
1286
|
+
// before it still cover the box.
|
|
1287
|
+
const float width = gradient.tile[2];
|
|
1288
|
+
const float height = gradient.tile[3];
|
|
1289
|
+
const float firstX =
|
|
1290
|
+
gradient.tile[0] - std::ceil((gradient.tile[0] - box.left) / width) * width;
|
|
1291
|
+
const float firstY =
|
|
1292
|
+
gradient.tile[1] - std::ceil((gradient.tile[1] - box.top) / height) * height;
|
|
1293
|
+
for (float y = firstY; y < box.bottom; y += height) {
|
|
1294
|
+
for (float x = firstX; x < box.right; x += width) {
|
|
1295
|
+
brush->SetTransform(
|
|
1296
|
+
D2D1::Matrix3x2F::Translation(x - gradient.tile[0], y - gradient.tile[1]));
|
|
1297
|
+
// The image's rectangle inside this tile, moved with it: a tile is the
|
|
1298
|
+
// period and the image need not fill it.
|
|
1299
|
+
target->FillRectangle(
|
|
1300
|
+
D2D1::RectF(area.left + (x - gradient.tile[0]),
|
|
1301
|
+
area.top + (y - gradient.tile[1]),
|
|
1302
|
+
area.right + (x - gradient.tile[0]),
|
|
1303
|
+
area.bottom + (y - gradient.tile[1])),
|
|
1304
|
+
brush.Get());
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
brush->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
void RnWin32View::setBoxShadows(std::vector<BoxShadow> shadows) {
|
|
1312
|
+
boxShadows_ = std::move(shadows);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
void RnWin32View::paintBoxShadows(ID2D1RenderTarget *target, bool inset) const {
|
|
1316
|
+
if (boxShadows_.empty()) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
// Back to front: CSS's first shadow is the one on top, so the list is walked
|
|
1320
|
+
// in reverse and the first one ends up painted last. Both other hosts do the
|
|
1321
|
+
// same with the same list.
|
|
1322
|
+
for (size_t index = boxShadows_.size(); index > 0; index--) {
|
|
1323
|
+
const BoxShadow &shadow = boxShadows_[index - 1];
|
|
1324
|
+
if (shadow.inset != inset) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
// A shadow with no colour at all is one React Native could not parse, and
|
|
1328
|
+
// painting it black is worse than painting nothing. The other two hosts
|
|
1329
|
+
// skip it for the same reason.
|
|
1330
|
+
if (shadow.colour[3] <= 0.0f) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
paintBoxShadow(target, shadow);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// One box shadow: a shape, a blur of that shape's alpha, and a clip.
|
|
1338
|
+
//
|
|
1339
|
+
// GSK has a shadow node per kind whose arguments are CSS's, and AppKit has
|
|
1340
|
+
// CALayer's shadow properties with a path; Direct2D has `CLSID_D2D1Shadow`,
|
|
1341
|
+
// which takes what is drawn and hands back its alpha blurred and coloured. So
|
|
1342
|
+
// the blur is the platform's and the work here is the geometry, which is the
|
|
1343
|
+
// same geometry the AppKit half builds as a CGPath -- and the corner radii grow
|
|
1344
|
+
// through `core/ShadowShape.h`'s curve on both, because a spread is not an
|
|
1345
|
+
// addition.
|
|
1346
|
+
//
|
|
1347
|
+
// **An outset shadow never paints inside the box that casts it and an inset one
|
|
1348
|
+
// never outside it**, which is CSS and is also what keeps a translucent
|
|
1349
|
+
// background from showing the shadow underneath it. That is the clip, and for
|
|
1350
|
+
// the outset case it is a shape no rectangle can express: everything around a
|
|
1351
|
+
// rounded box. See `geometryWithHole`.
|
|
1352
|
+
void RnWin32View::paintBoxShadow(ID2D1RenderTarget *target, const BoxShadow &shadow) const {
|
|
1353
|
+
ComPtr<ID2D1Factory> factory;
|
|
1354
|
+
target->GetFactory(factory.GetAddressOf());
|
|
1355
|
+
if (!factory) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// CSS does not allow a negative blur radius and React Native parses one
|
|
1360
|
+
// anyway, so it is clamped here. The spread may be negative and is not: GSK
|
|
1361
|
+
// asserts the same pair, and a sabotage run that swapped the two took that
|
|
1362
|
+
// host's suite down with it.
|
|
1363
|
+
const float blur = shadow.blur > 0.0f ? shadow.blur : 0.0f;
|
|
1364
|
+
// Room around the box for everything the shadow can reach. A gaussian is
|
|
1365
|
+
// truncated at about three standard deviations, which is one and a half
|
|
1366
|
+
// radii; twice the radius is the usual slack and is what the room below is.
|
|
1367
|
+
const float room = std::abs(shadow.dx) + std::abs(shadow.dy)
|
|
1368
|
+
+ std::abs(shadow.spread) + 2.0f * blur + 2.0f;
|
|
1369
|
+
const D2D1_RECT_F box = D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height);
|
|
1370
|
+
const D2D1_RECT_F around =
|
|
1371
|
+
D2D1::RectF(-room, -room, frame_.width + room, frame_.height + room);
|
|
1372
|
+
const float square[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
|
|
1373
|
+
|
|
1374
|
+
// The shape that casts the shadow, and the clip that keeps it where CSS puts
|
|
1375
|
+
// it.
|
|
1376
|
+
ComPtr<ID2D1Geometry> caster;
|
|
1377
|
+
ComPtr<ID2D1Geometry> mask;
|
|
1378
|
+
float radii[8];
|
|
1379
|
+
if (!shadow.inset) {
|
|
1380
|
+
for (int corner = 0; corner < 8; corner++) {
|
|
1381
|
+
radii[corner] =
|
|
1382
|
+
static_cast<float>(basalt::spreadRadius(cornerRadii_[corner], shadow.spread));
|
|
1383
|
+
}
|
|
1384
|
+
const D2D1_RECT_F grown = D2D1::RectF(box.left - shadow.spread + shadow.dx,
|
|
1385
|
+
box.top - shadow.spread + shadow.dy,
|
|
1386
|
+
box.right + shadow.spread + shadow.dx,
|
|
1387
|
+
box.bottom + shadow.spread + shadow.dy);
|
|
1388
|
+
caster = roundedBoxGeometry(factory.Get(), grown, radii);
|
|
1389
|
+
mask = geometryWithHole(factory.Get(),
|
|
1390
|
+
roundedBoxGeometry(factory.Get(), around, square).Get(),
|
|
1391
|
+
roundedBoxGeometry(factory.Get(), box, cornerRadii_).Get());
|
|
1392
|
+
} else {
|
|
1393
|
+
// The hole the shadow spills in from: the box moved by the offset and
|
|
1394
|
+
// shrunk by the spread. An inset shadow with no offset is a ring and one
|
|
1395
|
+
// with an offset is a crescent, which falls out of this rather than being
|
|
1396
|
+
// arranged.
|
|
1397
|
+
for (int corner = 0; corner < 8; corner++) {
|
|
1398
|
+
radii[corner] =
|
|
1399
|
+
static_cast<float>(basalt::spreadRadius(cornerRadii_[corner], -shadow.spread));
|
|
1400
|
+
}
|
|
1401
|
+
D2D1_RECT_F hole = D2D1::RectF(box.left + shadow.spread + shadow.dx,
|
|
1402
|
+
box.top + shadow.spread + shadow.dy,
|
|
1403
|
+
box.right - shadow.spread + shadow.dx,
|
|
1404
|
+
box.bottom - shadow.spread + shadow.dy);
|
|
1405
|
+
// A spread wider than the box turns the hole inside out, which would draw
|
|
1406
|
+
// the shape rather than the hole.
|
|
1407
|
+
if (hole.right < hole.left) {
|
|
1408
|
+
hole.right = hole.left;
|
|
1409
|
+
}
|
|
1410
|
+
if (hole.bottom < hole.top) {
|
|
1411
|
+
hole.bottom = hole.top;
|
|
1412
|
+
}
|
|
1413
|
+
caster = geometryWithHole(factory.Get(),
|
|
1414
|
+
roundedBoxGeometry(factory.Get(), around, square).Get(),
|
|
1415
|
+
roundedBoxGeometry(factory.Get(), hole, radii).Get());
|
|
1416
|
+
mask = roundedBoxGeometry(factory.Get(), box, cornerRadii_);
|
|
1417
|
+
}
|
|
1418
|
+
if (!caster) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const ScopedGeometryClip clip(mask ? target : nullptr, mask.Get());
|
|
1423
|
+
|
|
1424
|
+
// The blur, which is an effect and so needs a device context and a bitmap to
|
|
1425
|
+
// run over: the shape goes into an offscreen the size of the box plus the
|
|
1426
|
+
// room, and `CLSID_D2D1Shadow` blurs that bitmap's alpha and colours it. The
|
|
1427
|
+
// shape is drawn in opaque black because only its alpha is read.
|
|
1428
|
+
ComPtr<ID2D1DeviceContext> context;
|
|
1429
|
+
if (blur > 0.0f && SUCCEEDED(target->QueryInterface(IID_PPV_ARGS(&context)))) {
|
|
1430
|
+
const ComPtr<ID2D1BitmapRenderTarget> offscreen =
|
|
1431
|
+
createOffscreen(target, frame_.width + 2.0f * room, frame_.height + 2.0f * room);
|
|
1432
|
+
ComPtr<ID2D1SolidColorBrush> ink;
|
|
1433
|
+
if (offscreen
|
|
1434
|
+
&& SUCCEEDED(offscreen->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 1.0f),
|
|
1435
|
+
ink.GetAddressOf()))) {
|
|
1436
|
+
offscreen->BeginDraw();
|
|
1437
|
+
offscreen->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
1438
|
+
offscreen->SetTransform(D2D1::Matrix3x2F::Translation(room, room));
|
|
1439
|
+
offscreen->FillGeometry(caster.Get(), ink.Get());
|
|
1440
|
+
offscreen->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
1441
|
+
ComPtr<ID2D1Bitmap> drawn;
|
|
1442
|
+
ComPtr<ID2D1Effect> effect;
|
|
1443
|
+
if (SUCCEEDED(offscreen->EndDraw())
|
|
1444
|
+
&& SUCCEEDED(offscreen->GetBitmap(drawn.GetAddressOf())) && drawn
|
|
1445
|
+
&& SUCCEEDED(context->CreateEffect(CLSID_D2D1Shadow, effect.GetAddressOf()))
|
|
1446
|
+
&& effect) {
|
|
1447
|
+
effect->SetInput(0, drawn.Get());
|
|
1448
|
+
// Half the radius is the standard deviation, which is the conversion
|
|
1449
|
+
// every blur in this host makes and the one React Native's iOS half
|
|
1450
|
+
// settled on.
|
|
1451
|
+
effect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, blur / 2.0f);
|
|
1452
|
+
const D2D1_VECTOR_4F tint{
|
|
1453
|
+
shadow.colour[0], shadow.colour[1], shadow.colour[2], shadow.colour[3]};
|
|
1454
|
+
effect->SetValue(D2D1_SHADOW_PROP_COLOR, tint);
|
|
1455
|
+
// Back where the shape was: it was drawn `room` in from the bitmap's
|
|
1456
|
+
// own origin so the blur had somewhere to go.
|
|
1457
|
+
context->DrawImage(effect.Get(), D2D1::Point2F(-room, -room));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// No blur asked for, or no effect to be had: the shape filled in the
|
|
1464
|
+
// shadow's colour. For a blur of zero that is the whole picture, and
|
|
1465
|
+
// otherwise it is the same fallback the text shadow takes -- a hard shadow
|
|
1466
|
+
// rather than none, since a prop that cannot be honoured should not take the
|
|
1467
|
+
// view with it.
|
|
1468
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1469
|
+
const D2D1_COLOR_F colour =
|
|
1470
|
+
D2D1::ColorF(shadow.colour[0], shadow.colour[1], shadow.colour[2], shadow.colour[3]);
|
|
1471
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
1472
|
+
target->FillGeometry(caster.Get(), brush.Get());
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// CSS's outline, outside the box and over everything.
|
|
1477
|
+
//
|
|
1478
|
+
// **Nothing clips it, and that is the decision rather than an oversight.** CSS
|
|
1479
|
+
// does not clip an element's own outline for `overflow: hidden`, so a ring
|
|
1480
|
+
// drawn inside the clip would vanish on exactly the views that most often carry
|
|
1481
|
+
// one. Here that is free: `paintChildren` scopes its own clip, so by the time
|
|
1482
|
+
// this runs the clip is already popped. GTK gets it the same way, by appending
|
|
1483
|
+
// after the children; AppKit has to move the ring into the parent's layer.
|
|
1484
|
+
//
|
|
1485
|
+
// Stroked rather than filled like the border, for the two reasons
|
|
1486
|
+
// backlog/correctness.md records: a stroke takes a dash pattern, so dotted and
|
|
1487
|
+
// dashed need no second mechanism, and it takes an arbitrary path, so
|
|
1488
|
+
// elliptical radii need no special case. A stroke straddles its path, so the
|
|
1489
|
+
// path is the ring's centre line: offset plus half the width out from the
|
|
1490
|
+
// border edge, which is where the other two hosts put theirs.
|
|
1491
|
+
void RnWin32View::paintOutline(ID2D1RenderTarget *target) const {
|
|
1492
|
+
if (outlineWidth_ <= 0.0f || outlineColour_[3] <= 0.0f) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
ComPtr<ID2D1Factory> factory;
|
|
1497
|
+
target->GetFactory(factory.GetAddressOf());
|
|
1498
|
+
if (!factory) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// The ring's outer edge is the box grown by the offset plus the width; its
|
|
1503
|
+
// centre line is half a width back from that.
|
|
1504
|
+
const float centre = outlineOffset_ + outlineWidth_ / 2.0f;
|
|
1505
|
+
const D2D1_RECT_F rect = D2D1::RectF(
|
|
1506
|
+
-centre, -centre, frame_.width + centre, frame_.height + centre);
|
|
1507
|
+
|
|
1508
|
+
// Each non-zero radius grows by what the ring moved out, so the ring stays
|
|
1509
|
+
// concentric with a rounded card; a corner that was square stays square,
|
|
1510
|
+
// which is what React Native's iOS half does and what both other hosts do.
|
|
1511
|
+
float radii[8];
|
|
1512
|
+
for (int index = 0; index < 8; index++) {
|
|
1513
|
+
radii[index] = cornerRadii_[index] > 0.0f ? cornerRadii_[index] + centre : 0.0f;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const ComPtr<ID2D1Geometry> ring = roundedBoxGeometry(factory.Get(), rect, radii);
|
|
1517
|
+
if (!ring) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1522
|
+
const D2D1_COLOR_F colour = D2D1::ColorF(
|
|
1523
|
+
outlineColour_[0], outlineColour_[1], outlineColour_[2], outlineColour_[3]);
|
|
1524
|
+
if (FAILED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
const ComPtr<ID2D1StrokeStyle> dashed = dashStyleFor(factory.Get(), outlineStyle_);
|
|
1529
|
+
target->DrawGeometry(ring.Get(), brush.Get(), outlineWidth_, dashed.Get());
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// A dotted or dashed border: one stroked path around the rounded box rather
|
|
1533
|
+
// than four filled edges.
|
|
1534
|
+
//
|
|
1535
|
+
// Which is why the style belongs to the whole border and not to a side, the
|
|
1536
|
+
// same decision both other hosts made: a stroked path carries one dash pattern.
|
|
1537
|
+
// The width and the colour are the first side's, for the same reason.
|
|
1538
|
+
//
|
|
1539
|
+
// Inset by half the width, because a stroke straddles its path where the filled
|
|
1540
|
+
// ring sits inside the box. Without that a 4pt dashed border would paint two
|
|
1541
|
+
// points outside the view and overlap its neighbour.
|
|
1542
|
+
void RnWin32View::paintStrokedBorder(ID2D1RenderTarget *target) const {
|
|
1543
|
+
ComPtr<ID2D1Factory> factory;
|
|
1544
|
+
target->GetFactory(factory.GetAddressOf());
|
|
1545
|
+
if (!factory) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const float width = borderWidths_[0] > 0.0f ? borderWidths_[0] : 1.0f;
|
|
1550
|
+
const D2D1_RECT_F rect = D2D1::RectF(width / 2.0f,
|
|
1551
|
+
width / 2.0f,
|
|
1552
|
+
(std::max)(width / 2.0f, frame_.width - width / 2.0f),
|
|
1553
|
+
(std::max)(width / 2.0f, frame_.height - width / 2.0f));
|
|
1554
|
+
// The radii shrink with the path, so a dashed border on a rounded card keeps
|
|
1555
|
+
// the card's corner rather than cutting across it.
|
|
1556
|
+
float radii[8];
|
|
1557
|
+
for (int index = 0; index < 8; index++) {
|
|
1558
|
+
radii[index] = cornerRadii_[index] > width / 2.0f ? cornerRadii_[index] - width / 2.0f : 0.0f;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const ComPtr<ID2D1Geometry> path = roundedBoxGeometry(factory.Get(), rect, radii);
|
|
1562
|
+
if (!path) {
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1567
|
+
const D2D1_COLOR_F colour = D2D1::ColorF(
|
|
1568
|
+
borderColours_[0], borderColours_[1], borderColours_[2], borderColours_[3]);
|
|
1569
|
+
if (FAILED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const ComPtr<ID2D1StrokeStyle> dashed = dashStyleFor(factory.Get(), borderStyle_);
|
|
1574
|
+
target->DrawGeometry(path.Get(), brush.Get(), width, dashed.Get());
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
void RnWin32View::paintBorders(ID2D1RenderTarget *target) const {
|
|
1578
|
+
// Dotted and dashed are a different drawing entirely; see paintStrokedBorder.
|
|
1579
|
+
if (borderStyle_ != LineStyle::Solid) {
|
|
1580
|
+
paintStrokedBorder(target);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
ComPtr<ID2D1Factory> factory;
|
|
1585
|
+
target->GetFactory(factory.GetAddressOf());
|
|
1586
|
+
if (!factory) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const float width = frame_.width;
|
|
1591
|
+
const float height = frame_.height;
|
|
1592
|
+
const float top = borderWidths_[0];
|
|
1593
|
+
const float right = borderWidths_[1];
|
|
1594
|
+
const float bottom = borderWidths_[2];
|
|
1595
|
+
const float left = borderWidths_[3];
|
|
1596
|
+
|
|
1597
|
+
const auto floor0 = [](float value) { return (std::max)(0.0f, value); };
|
|
1598
|
+
const D2D1_RECT_F outerRect = D2D1::RectF(0.0f, 0.0f, width, height);
|
|
1599
|
+
const D2D1_RECT_F innerRect = D2D1::RectF(
|
|
1600
|
+
left, top, (std::max)(left, width - right), (std::max)(top, height - bottom));
|
|
1601
|
+
const float innerRadii[8] = {
|
|
1602
|
+
floor0(cornerRadii_[0] - left),
|
|
1603
|
+
floor0(cornerRadii_[1] - top),
|
|
1604
|
+
floor0(cornerRadii_[2] - right),
|
|
1605
|
+
floor0(cornerRadii_[3] - top),
|
|
1606
|
+
floor0(cornerRadii_[4] - right),
|
|
1607
|
+
floor0(cornerRadii_[5] - bottom),
|
|
1608
|
+
floor0(cornerRadii_[6] - left),
|
|
1609
|
+
floor0(cornerRadii_[7] - bottom),
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
const ComPtr<ID2D1Geometry> outer = roundedBoxGeometry(factory.Get(), outerRect, cornerRadii_);
|
|
1613
|
+
const ComPtr<ID2D1Geometry> inner = roundedBoxGeometry(factory.Get(), innerRect, innerRadii);
|
|
1614
|
+
if (!outer || !inner) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
ComPtr<ID2D1PathGeometry> ring;
|
|
1619
|
+
ComPtr<ID2D1GeometrySink> sink;
|
|
1620
|
+
if (FAILED(factory->CreatePathGeometry(ring.GetAddressOf())) ||
|
|
1621
|
+
FAILED(ring->Open(sink.GetAddressOf())) ||
|
|
1622
|
+
FAILED(outer->CombineWithGeometry(
|
|
1623
|
+
inner.Get(), D2D1_COMBINE_MODE_EXCLUDE, nullptr, sink.Get())) ||
|
|
1624
|
+
FAILED(sink->Close())) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const auto colourOf = [this](int edge) {
|
|
1629
|
+
const float *c = &borderColours_[edge * 4];
|
|
1630
|
+
return D2D1::ColorF(c[0], c[1], c[2], c[3]);
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
bool oneColour = true;
|
|
1634
|
+
for (int edge = 1; edge < 4; edge++) {
|
|
1635
|
+
for (int c = 0; c < 4; c++) {
|
|
1636
|
+
if (borderColours_[edge * 4 + c] != borderColours_[c]) {
|
|
1637
|
+
oneColour = false;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (oneColour) {
|
|
1643
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1644
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colourOf(0), brush.GetAddressOf()))) {
|
|
1645
|
+
target->FillGeometry(ring.Get(), brush.Get());
|
|
1646
|
+
}
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
ComPtr<ID2D1Layer> layer;
|
|
1651
|
+
if (FAILED(target->CreateLayer(nullptr, layer.GetAddressOf()))) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
auto parameters = D2D1::LayerParameters();
|
|
1655
|
+
parameters.contentBounds = D2D1::InfiniteRect();
|
|
1656
|
+
parameters.geometricMask = ring.Get();
|
|
1657
|
+
target->PushLayer(parameters, layer.Get());
|
|
1658
|
+
|
|
1659
|
+
const D2D1_ANTIALIAS_MODE previous = target->GetAntialiasMode();
|
|
1660
|
+
target->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
|
|
1661
|
+
|
|
1662
|
+
// Each side, from its two outer corners to the matching inner ones. Top,
|
|
1663
|
+
// right, bottom, left, as the widths and colours are kept.
|
|
1664
|
+
const D2D1_POINT_2F sides[4][4] = {
|
|
1665
|
+
{{0.0f, 0.0f}, {width, 0.0f}, {width - right, top}, {left, top}},
|
|
1666
|
+
{{width, 0.0f}, {width, height}, {width - right, height - bottom}, {width - right, top}},
|
|
1667
|
+
{{width, height}, {0.0f, height}, {left, height - bottom}, {width - right, height - bottom}},
|
|
1668
|
+
{{0.0f, height}, {0.0f, 0.0f}, {left, top}, {left, height - bottom}},
|
|
1669
|
+
};
|
|
1670
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
1671
|
+
if (borderWidths_[edge] <= 0.0f || borderColours_[edge * 4 + 3] <= 0.0f) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
ComPtr<ID2D1PathGeometry> side;
|
|
1675
|
+
ComPtr<ID2D1GeometrySink> sideSink;
|
|
1676
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1677
|
+
if (FAILED(factory->CreatePathGeometry(side.GetAddressOf())) ||
|
|
1678
|
+
FAILED(side->Open(sideSink.GetAddressOf())) ||
|
|
1679
|
+
FAILED(target->CreateSolidColorBrush(colourOf(edge), brush.GetAddressOf()))) {
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
sideSink->BeginFigure(sides[edge][0], D2D1_FIGURE_BEGIN_FILLED);
|
|
1683
|
+
sideSink->AddLines(&sides[edge][1], 3);
|
|
1684
|
+
sideSink->EndFigure(D2D1_FIGURE_END_CLOSED);
|
|
1685
|
+
if (SUCCEEDED(sideSink->Close())) {
|
|
1686
|
+
target->FillGeometry(side.Get(), brush.Get());
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
target->SetAntialiasMode(previous);
|
|
1691
|
+
target->PopLayer();
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Reads the current transform rather than being handed one, which is what keeps
|
|
1695
|
+
// D2D1_MATRIX_3X2_F -- and windows.h behind it -- out of the header.
|
|
1696
|
+
|
|
1697
|
+
// ---------------------------------------------------------------------------
|
|
1698
|
+
// Controls
|
|
1699
|
+
// ---------------------------------------------------------------------------
|
|
1700
|
+
|
|
1701
|
+
namespace {
|
|
1702
|
+
|
|
1703
|
+
// Where a spinner is in its turn, 0..1, from a monotonic clock.
|
|
1704
|
+
//
|
|
1705
|
+
// A clock rather than per-view state: every spinner on screen turns together,
|
|
1706
|
+
// which is what a real toolkit spinner does, and it means a view needs no
|
|
1707
|
+
// animation bookkeeping of its own. One turn a second, which is roughly what
|
|
1708
|
+
// GtkSpinner and NSProgressIndicator do.
|
|
1709
|
+
//
|
|
1710
|
+
// Here rather than in core/DesktopControls.h because this target deliberately
|
|
1711
|
+
// does not link React Native, and that header does.
|
|
1712
|
+
float spinnerTurn() {
|
|
1713
|
+
using namespace std::chrono;
|
|
1714
|
+
const double seconds = duration<double>(steady_clock::now().time_since_epoch()).count();
|
|
1715
|
+
return static_cast<float>(seconds - std::floor(seconds));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
D2D1_COLOR_F colourOr(const float rgba[4], bool has, D2D1_COLOR_F fallback) {
|
|
1719
|
+
return has ? D2D1::ColorF(rgba[0], rgba[1], rgba[2], rgba[3]) : fallback;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// The greys the rest of this host draws controls in. Windows' own accent colour
|
|
1723
|
+
// would be more native and is a per-user setting read through UISettings, which
|
|
1724
|
+
// is a WinRT dependency this package does not have; React Native's own default
|
|
1725
|
+
// switch is not accent-coloured either.
|
|
1726
|
+
//
|
|
1727
|
+
// Which is why the on colour is *green*, React Native's own, where GTK and
|
|
1728
|
+
// AppKit take the desktop's accent and draw a blue one. It was called
|
|
1729
|
+
// `kTrackOnBlue` until 2026-10-10, when a scenario measuring "is this switch
|
|
1730
|
+
// accent-coloured" measured blueness, passed on two hosts and failed here --
|
|
1731
|
+
// the name having been the only blue thing about it.
|
|
1732
|
+
constexpr float kTrackOffGrey = 0.78f;
|
|
1733
|
+
constexpr float kTrackOnGreen[3] = {0.20f, 0.60f, 0.35f};
|
|
1734
|
+
constexpr float kSpinnerGrey = 0.45f;
|
|
1735
|
+
|
|
1736
|
+
} // namespace
|
|
1737
|
+
|
|
1738
|
+
void RnWin32View::setControl(Control kind, const ControlStyle &style, std::string description) {
|
|
1739
|
+
control_ = kind;
|
|
1740
|
+
controlStyle_ = style;
|
|
1741
|
+
controlDescription_ = std::move(description);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
bool RnWin32View::hasAnimatingSpinner() const {
|
|
1745
|
+
if (control_ == Control::Spinner && controlStyle_.on) {
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
if (hidden_) {
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
for (const RnWin32View *child : children_) {
|
|
1752
|
+
if (child != nullptr && child->hasAnimatingSpinner()) {
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
void RnWin32View::setHighlights(std::vector<Highlight> highlights) {
|
|
1760
|
+
highlights_ = std::move(highlights);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
void RnWin32View::paintHighlights(ID2D1RenderTarget *target) const {
|
|
1764
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1765
|
+
if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0, 1.0f), brush.GetAddressOf()))) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
for (const Highlight &highlight : highlights_) {
|
|
1769
|
+
const D2D1_RECT_F area = D2D1::RectF(highlight.x,
|
|
1770
|
+
highlight.y,
|
|
1771
|
+
highlight.x + highlight.width,
|
|
1772
|
+
highlight.y + highlight.height);
|
|
1773
|
+
if (highlight.filled) {
|
|
1774
|
+
brush->SetColor(
|
|
1775
|
+
D2D1::ColorF(highlight.color[0], highlight.color[1], highlight.color[2]));
|
|
1776
|
+
brush->SetOpacity(highlight.color[3]);
|
|
1777
|
+
target->FillRectangle(area, brush.Get());
|
|
1778
|
+
}
|
|
1779
|
+
// Opaque on the outline even when the fill is not, so the edge of an
|
|
1780
|
+
// inspected element is a line rather than a suggestion. Stroked down the
|
|
1781
|
+
// middle, so the rectangle is inset by half the width to keep it inside.
|
|
1782
|
+
brush->SetColor(D2D1::ColorF(highlight.color[0], highlight.color[1], highlight.color[2]));
|
|
1783
|
+
brush->SetOpacity(1.0f);
|
|
1784
|
+
const float inset = basalt::kHighlightBorderWidth / 2.0f;
|
|
1785
|
+
target->DrawRectangle(
|
|
1786
|
+
D2D1::RectF(area.left + inset, area.top + inset, area.right - inset, area.bottom - inset),
|
|
1787
|
+
brush.Get(),
|
|
1788
|
+
basalt::kHighlightBorderWidth);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
void RnWin32View::paintControl(ID2D1RenderTarget *target) const {
|
|
1793
|
+
if (control_ == Control::Spinner) {
|
|
1794
|
+
// A stopped indicator draws nothing at all, which is what
|
|
1795
|
+
// `hidesWhenStopped` means and what a <RefreshControl> that is not
|
|
1796
|
+
// refreshing does.
|
|
1797
|
+
if (!controlStyle_.on && controlStyle_.hidesWhenStopped) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Eight dots around a circle, the leading one opaque and the rest fading
|
|
1802
|
+
// behind it. The same shape GtkSpinner and NSProgressIndicator draw, which
|
|
1803
|
+
// matters because these three hosts are screenshotted side by side.
|
|
1804
|
+
const float diameter = std::min(
|
|
1805
|
+
{controlStyle_.large ? basalt::kSpinnerLarge : basalt::kSpinnerSmall,
|
|
1806
|
+
frame_.width,
|
|
1807
|
+
frame_.height});
|
|
1808
|
+
if (diameter <= 0.0f) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const float centreX = frame_.width / 2.0f;
|
|
1812
|
+
const float centreY = frame_.height / 2.0f;
|
|
1813
|
+
const float radius = diameter / 2.0f;
|
|
1814
|
+
const float dotRadius = std::max(radius / 6.0f, 1.0f);
|
|
1815
|
+
|
|
1816
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1817
|
+
const D2D1_COLOR_F base = colourOr(controlStyle_.thumb,
|
|
1818
|
+
controlStyle_.hasThumb,
|
|
1819
|
+
D2D1::ColorF(kSpinnerGrey, kSpinnerGrey, kSpinnerGrey, 1.0f));
|
|
1820
|
+
if (FAILED(target->CreateSolidColorBrush(base, brush.GetAddressOf()))) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
constexpr int kDots = 8;
|
|
1825
|
+
// A stopped indicator that is still shown is drawn still, at phase zero,
|
|
1826
|
+
// rather than at whatever the clock happens to say -- so a screenshot of a
|
|
1827
|
+
// stopped spinner is the same picture every time.
|
|
1828
|
+
const float turn = controlStyle_.on ? spinnerTurn() : 0.0f;
|
|
1829
|
+
const int leading = static_cast<int>(turn * kDots) % kDots;
|
|
1830
|
+
for (int i = 0; i < kDots; i++) {
|
|
1831
|
+
const float angle = 6.2831853f * static_cast<float>(i) / static_cast<float>(kDots);
|
|
1832
|
+
const int behind = (leading - i + kDots) % kDots;
|
|
1833
|
+
brush->SetOpacity(1.0f - static_cast<float>(behind) / static_cast<float>(kDots));
|
|
1834
|
+
const D2D1_ELLIPSE dot = D2D1::Ellipse(
|
|
1835
|
+
D2D1::Point2F(centreX + std::cos(angle) * (radius - dotRadius),
|
|
1836
|
+
centreY + std::sin(angle) * (radius - dotRadius)),
|
|
1837
|
+
dotRadius,
|
|
1838
|
+
dotRadius);
|
|
1839
|
+
target->FillEllipse(dot, brush.Get());
|
|
1840
|
+
}
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// A switch: a rounded track with a circular thumb at one end. Sized from
|
|
1845
|
+
// core/ControlMetrics.h -- the same numbers the shadow node measures to --
|
|
1846
|
+
// and centred, so a <Switch> given a bigger box keeps its shape rather than
|
|
1847
|
+
// stretching.
|
|
1848
|
+
const float width = std::min(basalt::kSwitchWidth, frame_.width);
|
|
1849
|
+
const float height = std::min(basalt::kSwitchHeight, frame_.height);
|
|
1850
|
+
if (width <= 0.0f || height <= 0.0f) {
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const float left = (frame_.width - width) / 2.0f;
|
|
1854
|
+
const float top = (frame_.height - height) / 2.0f;
|
|
1855
|
+
|
|
1856
|
+
const D2D1_COLOR_F trackColour = controlStyle_.on
|
|
1857
|
+
? colourOr(controlStyle_.trackOn,
|
|
1858
|
+
controlStyle_.hasTrackOn,
|
|
1859
|
+
D2D1::ColorF(kTrackOnGreen[0], kTrackOnGreen[1], kTrackOnGreen[2], 1.0f))
|
|
1860
|
+
: colourOr(controlStyle_.trackOff,
|
|
1861
|
+
controlStyle_.hasTrackOff,
|
|
1862
|
+
D2D1::ColorF(kTrackOffGrey, kTrackOffGrey, kTrackOffGrey, 1.0f));
|
|
1863
|
+
|
|
1864
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1865
|
+
if (FAILED(target->CreateSolidColorBrush(trackColour, brush.GetAddressOf()))) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
// Half opacity for a disabled switch, which is what every platform does and
|
|
1869
|
+
// is the only thing `disabled` can mean to something that draws itself.
|
|
1870
|
+
brush->SetOpacity(controlStyle_.disabled ? 0.4f : 1.0f);
|
|
1871
|
+
const float radius = height / 2.0f;
|
|
1872
|
+
target->FillRoundedRectangle(
|
|
1873
|
+
D2D1::RoundedRect(D2D1::RectF(left, top, left + width, top + height), radius, radius),
|
|
1874
|
+
brush.Get());
|
|
1875
|
+
|
|
1876
|
+
const float inset = 2.0f;
|
|
1877
|
+
const float thumbRadius = radius - inset;
|
|
1878
|
+
const float thumbX = controlStyle_.on ? left + width - radius : left + radius;
|
|
1879
|
+
ComPtr<ID2D1SolidColorBrush> thumbBrush;
|
|
1880
|
+
const D2D1_COLOR_F thumbColour =
|
|
1881
|
+
colourOr(controlStyle_.thumb, controlStyle_.hasThumb, D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f));
|
|
1882
|
+
if (FAILED(target->CreateSolidColorBrush(thumbColour, thumbBrush.GetAddressOf()))) {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
thumbBrush->SetOpacity(controlStyle_.disabled ? 0.4f : 1.0f);
|
|
1886
|
+
target->FillEllipse(D2D1::Ellipse(D2D1::Point2F(thumbX, top + radius), thumbRadius, thumbRadius),
|
|
1887
|
+
thumbBrush.Get());
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Always drawn, rather than faded in while scrolling and out after: there is no
|
|
1891
|
+
// timer here and no animation, which is why `flashScrollIndicators` stays a
|
|
1892
|
+
// no-op -- there is nothing to flash something already on screen.
|
|
1893
|
+
void RnWin32View::paintScrollIndicators(ID2D1RenderTarget *target) const {
|
|
1894
|
+
const float thickness = static_cast<float>(basalt::kScrollIndicatorThickness);
|
|
1895
|
+
const float inset = static_cast<float>(basalt::kScrollIndicatorInset);
|
|
1896
|
+
|
|
1897
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
1898
|
+
// Neutral and translucent, so it reads over light and dark content alike, and
|
|
1899
|
+
// white instead when `indicatorStyle` asked for it. Core decides which, so
|
|
1900
|
+
// the GTK and AppKit hosts answer the prop with the same colour.
|
|
1901
|
+
if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(indicatorColour_[0],
|
|
1902
|
+
indicatorColour_[1],
|
|
1903
|
+
indicatorColour_[2],
|
|
1904
|
+
indicatorColour_[3]),
|
|
1905
|
+
brush.GetAddressOf()))) {
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// The hairline around the thumb, which `default` has and the other two do
|
|
1910
|
+
// not. A stroke straddles its own rectangle, so the rounded rect is inset by
|
|
1911
|
+
// half the width and the line lands on the thumb rather than half outside
|
|
1912
|
+
// it -- the same arithmetic the AppKit host uses.
|
|
1913
|
+
ComPtr<ID2D1SolidColorBrush> edgeBrush;
|
|
1914
|
+
const float edgeWidth = static_cast<float>(basalt::kScrollIndicatorBorderWidth);
|
|
1915
|
+
const bool hasEdge = indicatorBorderColour_[3] > 0.0f
|
|
1916
|
+
&& SUCCEEDED(target->CreateSolidColorBrush(D2D1::ColorF(indicatorBorderColour_[0],
|
|
1917
|
+
indicatorBorderColour_[1],
|
|
1918
|
+
indicatorBorderColour_[2],
|
|
1919
|
+
indicatorBorderColour_[3]),
|
|
1920
|
+
edgeBrush.GetAddressOf()));
|
|
1921
|
+
|
|
1922
|
+
const float radius = thickness / 2.0f;
|
|
1923
|
+
const float edgeRadius = radius - edgeWidth / 2.0f > 0.0f ? radius - edgeWidth / 2.0f : 0.0f;
|
|
1924
|
+
if (indicatorVerticalLength_ > 0.0f) {
|
|
1925
|
+
const float left = frame_.width - thickness - inset;
|
|
1926
|
+
const D2D1_RECT_F bar = D2D1::RectF(left,
|
|
1927
|
+
indicatorVerticalOffset_,
|
|
1928
|
+
left + thickness,
|
|
1929
|
+
indicatorVerticalOffset_ + indicatorVerticalLength_);
|
|
1930
|
+
target->FillRoundedRectangle(D2D1::RoundedRect(bar, radius, radius), brush.Get());
|
|
1931
|
+
if (hasEdge) {
|
|
1932
|
+
const D2D1_RECT_F inner = D2D1::RectF(bar.left + edgeWidth / 2.0f,
|
|
1933
|
+
bar.top + edgeWidth / 2.0f,
|
|
1934
|
+
bar.right - edgeWidth / 2.0f,
|
|
1935
|
+
bar.bottom - edgeWidth / 2.0f);
|
|
1936
|
+
target->DrawRoundedRectangle(
|
|
1937
|
+
D2D1::RoundedRect(inner, edgeRadius, edgeRadius), edgeBrush.Get(), edgeWidth);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (indicatorHorizontalLength_ > 0.0f) {
|
|
1941
|
+
const float top = frame_.height - thickness - inset;
|
|
1942
|
+
const D2D1_RECT_F bar = D2D1::RectF(indicatorHorizontalOffset_,
|
|
1943
|
+
top,
|
|
1944
|
+
indicatorHorizontalOffset_ + indicatorHorizontalLength_,
|
|
1945
|
+
top + thickness);
|
|
1946
|
+
target->FillRoundedRectangle(D2D1::RoundedRect(bar, radius, radius), brush.Get());
|
|
1947
|
+
if (hasEdge) {
|
|
1948
|
+
const D2D1_RECT_F inner = D2D1::RectF(bar.left + edgeWidth / 2.0f,
|
|
1949
|
+
bar.top + edgeWidth / 2.0f,
|
|
1950
|
+
bar.right - edgeWidth / 2.0f,
|
|
1951
|
+
bar.bottom - edgeWidth / 2.0f);
|
|
1952
|
+
target->DrawRoundedRectangle(
|
|
1953
|
+
D2D1::RoundedRect(inner, edgeRadius, edgeRadius), edgeBrush.Get(), edgeWidth);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
namespace {
|
|
1959
|
+
|
|
1960
|
+
// CSS's blend keyword as a Direct2D blend mode, or nothing for a keyword this
|
|
1961
|
+
// host cannot blend.
|
|
1962
|
+
//
|
|
1963
|
+
// The keyword is what crosses the seam -- see core/BlendModes.h -- so this is
|
|
1964
|
+
// the one place that turns it into Direct2D's vocabulary, as the GTK host turns
|
|
1965
|
+
// it into a GskBlendMode and AppKit into a Core Image filter. Sixteen of the
|
|
1966
|
+
// seventeen are the same word in both lists, which is why they are written out
|
|
1967
|
+
// rather than mapped by position: the enum and CSS's list are in different
|
|
1968
|
+
// orders, and a table by position reports a neighbour's blend.
|
|
1969
|
+
//
|
|
1970
|
+
// **`plus-lighter` is `LINEAR_DODGE`**, which is the choice AppKit made with
|
|
1971
|
+
// `CILinearDodgeBlendMode` and for the same reason: the keyword means clamped
|
|
1972
|
+
// addition and linear dodge is addition. That makes this the only one of the
|
|
1973
|
+
// three hosts with all seventeen; GSK has no node for it and paints unblended.
|
|
1974
|
+
std::optional<D2D1_BLEND_MODE> blendModeFor(const std::string &keyword) {
|
|
1975
|
+
if (keyword.empty() || keyword == "normal") {
|
|
1976
|
+
return std::nullopt;
|
|
1977
|
+
}
|
|
1978
|
+
if (keyword == "multiply") {
|
|
1979
|
+
return D2D1_BLEND_MODE_MULTIPLY;
|
|
1980
|
+
}
|
|
1981
|
+
if (keyword == "screen") {
|
|
1982
|
+
return D2D1_BLEND_MODE_SCREEN;
|
|
1983
|
+
}
|
|
1984
|
+
if (keyword == "overlay") {
|
|
1985
|
+
return D2D1_BLEND_MODE_OVERLAY;
|
|
1986
|
+
}
|
|
1987
|
+
if (keyword == "darken") {
|
|
1988
|
+
return D2D1_BLEND_MODE_DARKEN;
|
|
1989
|
+
}
|
|
1990
|
+
if (keyword == "lighten") {
|
|
1991
|
+
return D2D1_BLEND_MODE_LIGHTEN;
|
|
1992
|
+
}
|
|
1993
|
+
if (keyword == "color-dodge") {
|
|
1994
|
+
return D2D1_BLEND_MODE_COLOR_DODGE;
|
|
1995
|
+
}
|
|
1996
|
+
if (keyword == "color-burn") {
|
|
1997
|
+
return D2D1_BLEND_MODE_COLOR_BURN;
|
|
1998
|
+
}
|
|
1999
|
+
if (keyword == "hard-light") {
|
|
2000
|
+
return D2D1_BLEND_MODE_HARD_LIGHT;
|
|
2001
|
+
}
|
|
2002
|
+
if (keyword == "soft-light") {
|
|
2003
|
+
return D2D1_BLEND_MODE_SOFT_LIGHT;
|
|
2004
|
+
}
|
|
2005
|
+
if (keyword == "difference") {
|
|
2006
|
+
return D2D1_BLEND_MODE_DIFFERENCE;
|
|
2007
|
+
}
|
|
2008
|
+
if (keyword == "exclusion") {
|
|
2009
|
+
return D2D1_BLEND_MODE_EXCLUSION;
|
|
2010
|
+
}
|
|
2011
|
+
if (keyword == "hue") {
|
|
2012
|
+
return D2D1_BLEND_MODE_HUE;
|
|
2013
|
+
}
|
|
2014
|
+
if (keyword == "saturation") {
|
|
2015
|
+
return D2D1_BLEND_MODE_SATURATION;
|
|
2016
|
+
}
|
|
2017
|
+
if (keyword == "color") {
|
|
2018
|
+
return D2D1_BLEND_MODE_COLOR;
|
|
2019
|
+
}
|
|
2020
|
+
if (keyword == "luminosity") {
|
|
2021
|
+
return D2D1_BLEND_MODE_LUMINOSITY;
|
|
2022
|
+
}
|
|
2023
|
+
if (keyword == "plus-lighter") {
|
|
2024
|
+
return D2D1_BLEND_MODE_LINEAR_DODGE;
|
|
2025
|
+
}
|
|
2026
|
+
return std::nullopt;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
} // namespace
|
|
2030
|
+
|
|
2031
|
+
void RnWin32View::setBlendMode(const char *name) {
|
|
2032
|
+
blendMode_ = name != nullptr ? name : "";
|
|
2033
|
+
// `normal` is "no blending", which is also what no keyword at all means. Held
|
|
2034
|
+
// as nothing so that the dump says nothing: `core/BlendModes.h` answers null
|
|
2035
|
+
// for it, so this is only reachable from a direct call, and the three hosts'
|
|
2036
|
+
// lines have to agree either way.
|
|
2037
|
+
if (blendMode_ == "normal") {
|
|
2038
|
+
blendMode_.clear();
|
|
2039
|
+
}
|
|
2040
|
+
blends_ = blendModeFor(blendMode_).has_value();
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
void RnWin32View::paintChildren(ID2D1RenderTarget *target,
|
|
2044
|
+
ID2D1BitmapRenderTarget *blendLayer) const {
|
|
2045
|
+
if (children_.empty()) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
D2D1::Matrix3x2F worldTransform;
|
|
2050
|
+
target->GetTransform(&worldTransform);
|
|
2051
|
+
|
|
2052
|
+
// `overflow: hidden`, and only that: the background above is clipped whether
|
|
2053
|
+
// or not this is set.
|
|
2054
|
+
//
|
|
2055
|
+
// **With a blended child it cannot be one clip around the whole walk.** A
|
|
2056
|
+
// clip here is a layer with a geometric mask -- see Win32Clip.h -- and a
|
|
2057
|
+
// pushed layer holds everything drawn inside it in an intermediate surface,
|
|
2058
|
+
// where the bitmap a blend reads as its backdrop cannot see it. So each child
|
|
2059
|
+
// gets its own clip instead, which paints the same picture: clipping a group
|
|
2060
|
+
// and clipping each of its members to the same box are the same thing. The
|
|
2061
|
+
// GTK half had to do this too, for a different reason -- there the pops that
|
|
2062
|
+
// close each blend happen between the children.
|
|
2063
|
+
const D2D1_RECT_F bounds = D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height);
|
|
2064
|
+
const bool clipsEachChild = blendLayer != nullptr && clipsChildren_;
|
|
2065
|
+
const ScopedGeometryClip clip(
|
|
2066
|
+
clipsChildren_ && !clipsEachChild ? target : nullptr, bounds, cornerRadii_);
|
|
2067
|
+
|
|
2068
|
+
// A ScrollView's offset moves its children and nothing else, so it belongs
|
|
2069
|
+
// between this view's transform and theirs.
|
|
2070
|
+
//
|
|
2071
|
+
// A paragraph's vertical alignment moves the views inline in it for the same
|
|
2072
|
+
// reason and in the same place: see textVerticalOffset.
|
|
2073
|
+
const float inlineOffset = textVerticalOffset();
|
|
2074
|
+
const bool movesChildren =
|
|
2075
|
+
scrollX_ != 0.0f || scrollY_ != 0.0f || inlineOffset != 0.0f;
|
|
2076
|
+
const D2D1::Matrix3x2F childTransform = movesChildren
|
|
2077
|
+
? D2D1::Matrix3x2F::Translation(-scrollX_, -scrollY_ + inlineOffset) * worldTransform
|
|
2078
|
+
: worldTransform;
|
|
2079
|
+
if (movesChildren) {
|
|
2080
|
+
target->SetTransform(childTransform);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Forwards, so the last child painted is on top. Hit testing walks the same
|
|
2084
|
+
// list backwards.
|
|
2085
|
+
//
|
|
2086
|
+
// In paint order rather than insertion order, which matters twice over for a
|
|
2087
|
+
// blend: zIndex can differ from the list, and a blend's backdrop is what was
|
|
2088
|
+
// painted beneath it. The GTK half has to sort the blended children for the
|
|
2089
|
+
// same reason; here the order is simply the order they are blended in.
|
|
2090
|
+
for (const RnWin32View *child : childrenInPaintOrder()) {
|
|
2091
|
+
if (child == nullptr) {
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (blendLayer != nullptr && child->blends() && !child->hidden_) {
|
|
2095
|
+
blendChildIntoLayer(blendLayer, child);
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (clipsEachChild) {
|
|
2099
|
+
// The clip is in this view's own coordinates, so it is pushed with the
|
|
2100
|
+
// scroll offset off and the offset put back for the child itself.
|
|
2101
|
+
target->SetTransform(worldTransform);
|
|
2102
|
+
const ScopedGeometryClip own(target, bounds, cornerRadii_);
|
|
2103
|
+
target->SetTransform(childTransform);
|
|
2104
|
+
child->paint(target);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
child->paint(target);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
bool RnWin32View::hasBlendedChild() const {
|
|
2112
|
+
for (const RnWin32View *child : children_) {
|
|
2113
|
+
if (child != nullptr && child->blends() && !child->hidden_) {
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// This view's subtree into an offscreen bitmap, so that a blended child has a
|
|
2121
|
+
// backdrop to read, and then that bitmap onto the target.
|
|
2122
|
+
//
|
|
2123
|
+
// The backdrop is the one thing a blend needs and the one thing a view cannot
|
|
2124
|
+
// see: `CLSID_D2D1Blend` takes two images, and what is beneath a child is
|
|
2125
|
+
// whatever its parent painted before it. On GTK the parent pushes a blend node
|
|
2126
|
+
// per blended child before it paints anything; here the parent paints into a
|
|
2127
|
+
// bitmap and each blended child reads it. Same arrangement, different compositor.
|
|
2128
|
+
//
|
|
2129
|
+
// **The backdrop stops at this view**, which is the deviation CSS would not
|
|
2130
|
+
// make and backlog/correctness.md records for GTK as well: CSS blends with the
|
|
2131
|
+
// backdrop of the nearest stacking context, which for a plain <View> reaches
|
|
2132
|
+
// past its parent. What is beneath this view is on the window and cannot be
|
|
2133
|
+
// read back without copying the window; what is inside it is this bitmap. The
|
|
2134
|
+
// AppKit host does blend with the whole layer tree, because Core Animation
|
|
2135
|
+
// composites it, so the two differ and the end-to-end scenario is deliberately
|
|
2136
|
+
// an arrangement where they agree.
|
|
2137
|
+
void RnWin32View::paintBlendLayer(ID2D1RenderTarget *target) const {
|
|
2138
|
+
const ComPtr<ID2D1BitmapRenderTarget> layer =
|
|
2139
|
+
createOffscreen(target, frame_.width, frame_.height);
|
|
2140
|
+
if (!layer) {
|
|
2141
|
+
// No offscreen, no backdrop: the children paint unblended, which is the
|
|
2142
|
+
// same fallback a keyword this host cannot blend takes.
|
|
2143
|
+
paintContents(target);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
layer->BeginDraw();
|
|
2148
|
+
layer->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
2149
|
+
paintContents(layer.Get(), layer.Get());
|
|
2150
|
+
if (FAILED(layer->EndDraw())) {
|
|
2151
|
+
paintContents(target);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
ComPtr<ID2D1Bitmap> picture;
|
|
2156
|
+
if (FAILED(layer->GetBitmap(picture.GetAddressOf())) || !picture) {
|
|
2157
|
+
paintContents(target);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// The view's own opacity, which `paintContents` did not apply because a
|
|
2162
|
+
// pushed layer would have hidden the backdrop from the children. On the
|
|
2163
|
+
// bitmap it is the same picture and one multiply rather than a layer.
|
|
2164
|
+
target->DrawBitmap(picture.Get(),
|
|
2165
|
+
D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height),
|
|
2166
|
+
opacity_ * filters_.opacity,
|
|
2167
|
+
D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
|
|
2168
|
+
nullptr);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// One blended child, in place of painting it onto the layer.
|
|
2172
|
+
//
|
|
2173
|
+
// Three surfaces, and each is there for a reason Direct2D imposes. The child
|
|
2174
|
+
// goes into one of its own because the blend wants it as an image rather than
|
|
2175
|
+
// as a draw. The result goes into another because the backdrop is the layer's
|
|
2176
|
+
// own bitmap, and Direct2D will not read a bitmap it is drawing into. And the
|
|
2177
|
+
// layer is then cleared and the result drawn back, rather than drawn over,
|
|
2178
|
+
// because the result already contains the backdrop: drawing it over would
|
|
2179
|
+
// composite the backdrop with itself.
|
|
2180
|
+
void RnWin32View::blendChildIntoLayer(ID2D1BitmapRenderTarget *layer,
|
|
2181
|
+
const RnWin32View *child) const {
|
|
2182
|
+
const std::optional<D2D1_BLEND_MODE> mode = blendModeFor(child->blendMode());
|
|
2183
|
+
ComPtr<ID2D1DeviceContext> context;
|
|
2184
|
+
if (!mode.has_value() || FAILED(layer->QueryInterface(IID_PPV_ARGS(&context)))) {
|
|
2185
|
+
// A keyword Direct2D has no mode for, or no device context to run an effect
|
|
2186
|
+
// on: the child paints unblended rather than not at all. The dump still
|
|
2187
|
+
// reports the keyword, which is the rule GTK follows for `plus-lighter`.
|
|
2188
|
+
child->paint(layer);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// What is beneath the child: this view's own content and the children painted
|
|
2193
|
+
// before it, which is exactly what the layer holds at this point.
|
|
2194
|
+
layer->Flush();
|
|
2195
|
+
ComPtr<ID2D1Bitmap> backdrop;
|
|
2196
|
+
if (FAILED(layer->GetBitmap(backdrop.GetAddressOf())) || !backdrop) {
|
|
2197
|
+
child->paint(layer);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// The child's own picture, in this view's coordinates: the transform the
|
|
2202
|
+
// layer is in, which carries a <ScrollView>'s offset, so the child lands
|
|
2203
|
+
// where it would have been painted.
|
|
2204
|
+
D2D1::Matrix3x2F transform;
|
|
2205
|
+
layer->GetTransform(&transform);
|
|
2206
|
+
const ComPtr<ID2D1BitmapRenderTarget> top =
|
|
2207
|
+
createOffscreen(layer, frame_.width, frame_.height);
|
|
2208
|
+
ComPtr<ID2D1Bitmap> topBitmap;
|
|
2209
|
+
if (!top) {
|
|
2210
|
+
child->paint(layer);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
top->BeginDraw();
|
|
2214
|
+
top->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
2215
|
+
{
|
|
2216
|
+
// `overflow: hidden` clips the child here rather than where the blend
|
|
2217
|
+
// lands, which is what lets the result be drawn back over the whole layer:
|
|
2218
|
+
// outside the clip the child is transparent, a blend with nothing on top is
|
|
2219
|
+
// the backdrop, and the result is the layer unchanged there. The clip goes
|
|
2220
|
+
// on with the scroll offset off, as it does in paintChildren, and the
|
|
2221
|
+
// layer's own transform is what that offset was applied to.
|
|
2222
|
+
const D2D1::Matrix3x2F unscrolled =
|
|
2223
|
+
D2D1::Matrix3x2F::Translation(scrollX_, scrollY_ - textVerticalOffset()) * transform;
|
|
2224
|
+
top->SetTransform(unscrolled);
|
|
2225
|
+
const ScopedGeometryClip clip(clipsChildren_ ? top.Get() : nullptr,
|
|
2226
|
+
D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height),
|
|
2227
|
+
cornerRadii_);
|
|
2228
|
+
top->SetTransform(transform);
|
|
2229
|
+
child->paint(top.Get());
|
|
2230
|
+
}
|
|
2231
|
+
if (FAILED(top->EndDraw()) || FAILED(top->GetBitmap(topBitmap.GetAddressOf()))
|
|
2232
|
+
|| !topBitmap) {
|
|
2233
|
+
child->paint(layer);
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
const ComPtr<ID2D1BitmapRenderTarget> blended =
|
|
2238
|
+
createOffscreen(layer, frame_.width, frame_.height);
|
|
2239
|
+
ComPtr<ID2D1DeviceContext> blendedContext;
|
|
2240
|
+
ComPtr<ID2D1Effect> blend;
|
|
2241
|
+
ComPtr<ID2D1Bitmap> result;
|
|
2242
|
+
if (!blended || FAILED(blended->QueryInterface(IID_PPV_ARGS(&blendedContext)))
|
|
2243
|
+
|| FAILED(blendedContext->CreateEffect(CLSID_D2D1Blend, blend.GetAddressOf()))
|
|
2244
|
+
|| !blend) {
|
|
2245
|
+
child->paint(layer);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
blend->SetInput(0, backdrop.Get());
|
|
2249
|
+
blend->SetInput(1, topBitmap.Get());
|
|
2250
|
+
blend->SetValue(D2D1_BLEND_PROP_MODE, mode.value());
|
|
2251
|
+
blended->BeginDraw();
|
|
2252
|
+
blended->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
2253
|
+
blendedContext->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
2254
|
+
blendedContext->DrawImage(blend.Get(), D2D1::Point2F(0.0f, 0.0f));
|
|
2255
|
+
if (FAILED(blended->EndDraw()) || FAILED(blended->GetBitmap(result.GetAddressOf()))
|
|
2256
|
+
|| !result) {
|
|
2257
|
+
child->paint(layer);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Back onto the layer, in its own coordinates rather than the scrolled ones:
|
|
2262
|
+
// the result is already in this view's space, and it is the whole layer --
|
|
2263
|
+
// backdrop included -- which is why the layer is cleared first rather than
|
|
2264
|
+
// drawn over.
|
|
2265
|
+
layer->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
2266
|
+
layer->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
2267
|
+
layer->DrawBitmap(result.Get(),
|
|
2268
|
+
D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height),
|
|
2269
|
+
1.0f,
|
|
2270
|
+
D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
|
|
2271
|
+
nullptr);
|
|
2272
|
+
layer->SetTransform(transform);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// --- Hit testing ------------------------------------------------------------
|
|
2276
|
+
|
|
2277
|
+
namespace {
|
|
2278
|
+
|
|
2279
|
+
// The spelling React Native uses for the prop, which is also CSS's, so the
|
|
2280
|
+
// three hosts' dumps say the same words.
|
|
2281
|
+
const char *pointerEventsName(RnWin32View::PointerEvents mode) {
|
|
2282
|
+
switch (mode) {
|
|
2283
|
+
case RnWin32View::PointerEvents::None:
|
|
2284
|
+
return "none";
|
|
2285
|
+
case RnWin32View::PointerEvents::BoxNone:
|
|
2286
|
+
return "box-none";
|
|
2287
|
+
case RnWin32View::PointerEvents::BoxOnly:
|
|
2288
|
+
return "box-only";
|
|
2289
|
+
case RnWin32View::PointerEvents::Auto:
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
return "auto";
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
} // namespace
|
|
2296
|
+
|
|
2297
|
+
void RnWin32View::setHitSlop(const float insets[4]) {
|
|
2298
|
+
bool any = false;
|
|
2299
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
2300
|
+
hitSlop_[edge] = insets != nullptr ? insets[edge] : 0.0f;
|
|
2301
|
+
if (hitSlop_[edge] != 0.0f) {
|
|
2302
|
+
any = true;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
hasHitSlop_ = any;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
RnWin32View *hitTest(RnWin32View *root, float x, float y) {
|
|
2309
|
+
// `pointerEvents: none` takes the view and everything inside it out of hit
|
|
2310
|
+
// testing entirely, so the caller's loop carries on to whatever is behind.
|
|
2311
|
+
if (root == nullptr || root->hidden() ||
|
|
2312
|
+
root->pointerEvents() == RnWin32View::PointerEvents::None) {
|
|
2313
|
+
return nullptr;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const RnRect &frame = root->frame();
|
|
2317
|
+
// `hitSlop` widens this test and only this test, so a press just outside the
|
|
2318
|
+
// box still lands on it. The slop is in this view's own coordinates, which is
|
|
2319
|
+
// where the point already is: a transform was inverted on the way down.
|
|
2320
|
+
//
|
|
2321
|
+
// The parent's answer is unchanged, so a slop reaching outside the parent is
|
|
2322
|
+
// only reachable where the parent is -- the same bound the other two hosts
|
|
2323
|
+
// have, and the same one iOS has, because each of them widens one view's test
|
|
2324
|
+
// rather than the walk that got here.
|
|
2325
|
+
const float *const slop = root->hitSlop();
|
|
2326
|
+
const float left = root->hasHitSlop() ? -slop[3] : 0.0f;
|
|
2327
|
+
const float top = root->hasHitSlop() ? -slop[0] : 0.0f;
|
|
2328
|
+
const float right = frame.width + (root->hasHitSlop() ? slop[1] : 0.0f);
|
|
2329
|
+
const float bottom = frame.height + (root->hasHitSlop() ? slop[2] : 0.0f);
|
|
2330
|
+
if (x < left || y < top || x >= right || y >= bottom) {
|
|
2331
|
+
return nullptr;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Children are placed in this view's content space, which a scroll offset
|
|
2335
|
+
// shifts. Adding it back here is what makes hit testing follow a scroll with
|
|
2336
|
+
// nothing in this function knowing what a ScrollView is -- the same offset
|
|
2337
|
+
// `paintChildren` subtracts, from the same two fields.
|
|
2338
|
+
const float contentX = x + root->scrollX();
|
|
2339
|
+
const float contentY = y + root->scrollY() - root->textVerticalOffset();
|
|
2340
|
+
|
|
2341
|
+
// Backwards: the last child painted is the topmost, and the topmost is what a
|
|
2342
|
+
// press should land on.
|
|
2343
|
+
//
|
|
2344
|
+
// `box-only` is the one mode that skips this: the box is the target and
|
|
2345
|
+
// nothing inside it is, which is what makes an overlay swallow a press meant
|
|
2346
|
+
// for a button drawn on top of it.
|
|
2347
|
+
if (root->pointerEvents() != RnWin32View::PointerEvents::BoxOnly) {
|
|
2348
|
+
const std::vector<RnWin32View *> ordered = root->childrenInPaintOrder();
|
|
2349
|
+
for (auto it = ordered.rbegin(); it != ordered.rend(); ++it) {
|
|
2350
|
+
RnWin32View *child = *it;
|
|
2351
|
+
float local[6];
|
|
2352
|
+
child->localToParent(local);
|
|
2353
|
+
|
|
2354
|
+
float childX = 0.0f;
|
|
2355
|
+
float childY = 0.0f;
|
|
2356
|
+
if (!invertPoint(local, contentX, contentY, childX, childY)) {
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
if (RnWin32View *hit = hitTest(child, childX, childY)) {
|
|
2360
|
+
return hit;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// `box-none` is transparent to a press that misses everything inside it.
|
|
2366
|
+
// Returning null rather than the parent is the whole of it: the caller is
|
|
2367
|
+
// partway through its own list of children, so the press carries on to the
|
|
2368
|
+
// sibling *behind* this view -- which is what the absolutely-positioned
|
|
2369
|
+
// overlay this mode exists for is asking for.
|
|
2370
|
+
if (root->pointerEvents() == RnWin32View::PointerEvents::BoxNone) {
|
|
2371
|
+
return nullptr;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// A point inside this view but over none of its children is this view. React
|
|
2375
|
+
// Native's responder system needs a target for every press inside the
|
|
2376
|
+
// surface, and the root is the honest answer for one that missed everything.
|
|
2377
|
+
return root;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
void RnWin32View::setCursor(const char *name) {
|
|
2381
|
+
cursor_ = name != nullptr ? name : "";
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
std::string cursorNameAt(RnWin32View *root, float x, float y) {
|
|
2385
|
+
// The innermost view under the point, which is the same answer a press gets:
|
|
2386
|
+
// one hit test for both means a cursor cannot disagree with what a click will
|
|
2387
|
+
// do. `pointerEvents: none` is skipped by it, which is right here too -- a
|
|
2388
|
+
// view a press passes through should not be changing the pointer either.
|
|
2389
|
+
RnWin32View *view = hitTest(root, x, y);
|
|
2390
|
+
// And then up, because CSS's cursor inherits: a <Text> inside a button that
|
|
2391
|
+
// asked for `pointer` shows the hand, having asked for nothing itself.
|
|
2392
|
+
while (view != nullptr) {
|
|
2393
|
+
if (!view->cursor().empty()) {
|
|
2394
|
+
return view->cursor();
|
|
2395
|
+
}
|
|
2396
|
+
view = view->parent();
|
|
2397
|
+
}
|
|
2398
|
+
return {};
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// --- Reporting -------------------------------------------------------------
|
|
2402
|
+
|
|
2403
|
+
std::string RnWin32View::describeTree() const {
|
|
2404
|
+
std::string out;
|
|
2405
|
+
describeInto(out, 0);
|
|
2406
|
+
return out;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
void RnWin32View::describeInto(std::string &out, int depth) const {
|
|
2410
|
+
for (int i = 0; i < depth; i++) {
|
|
2411
|
+
out += " ";
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// The y a paragraph moved this view to, when it is inline in one: the frame
|
|
2415
|
+
// is what React Native decided and `textAlignVertical` is the host's own
|
|
2416
|
+
// answer to a box taller than its text, so the two differ there and the other
|
|
2417
|
+
// hosts print the same number.
|
|
2418
|
+
const float dumpY =
|
|
2419
|
+
frame_.y + (parent_ != nullptr ? parent_->textVerticalOffset() : 0.0f);
|
|
2420
|
+
appendFormat(out,
|
|
2421
|
+
"view tag=%d frame=(%g,%g %gx%g)",
|
|
2422
|
+
tag_,
|
|
2423
|
+
static_cast<double>(frame_.x),
|
|
2424
|
+
static_cast<double>(dumpY),
|
|
2425
|
+
static_cast<double>(frame_.width),
|
|
2426
|
+
static_cast<double>(frame_.height));
|
|
2427
|
+
|
|
2428
|
+
if (hasBackgroundColor_) {
|
|
2429
|
+
appendFormat(out,
|
|
2430
|
+
" bg=#%02x%02x%02x%02x",
|
|
2431
|
+
toByte(backgroundColor_[0]),
|
|
2432
|
+
toByte(backgroundColor_[1]),
|
|
2433
|
+
toByte(backgroundColor_[2]),
|
|
2434
|
+
toByte(backgroundColor_[3]));
|
|
2435
|
+
}
|
|
2436
|
+
// Field order matches the GTK and AppKit sides exactly -- bg, opacity, clip,
|
|
2437
|
+
// transform, scroll, then the content fields -- because
|
|
2438
|
+
// scripts/compare_hosts.sh diffs the dumps line by line and a reordering
|
|
2439
|
+
// would read as every line differing.
|
|
2440
|
+
if (opacity_ < 1.0f) {
|
|
2441
|
+
appendFormat(out, " opacity=%g", static_cast<double>(opacity_));
|
|
2442
|
+
}
|
|
2443
|
+
if (clipsChildren_) {
|
|
2444
|
+
out += " clip";
|
|
2445
|
+
}
|
|
2446
|
+
// Whether anything is drawn at all. Without this a view hidden by
|
|
2447
|
+
// `display: none` or by a back face turned away reads exactly like a visible
|
|
2448
|
+
// one, and the only prop in this dump that removes a view entirely was the
|
|
2449
|
+
// only one it could not show.
|
|
2450
|
+
if (hidden_) {
|
|
2451
|
+
out += " hidden";
|
|
2452
|
+
}
|
|
2453
|
+
// Per-corner radii and per-edge borders, in the fields, the order and the
|
|
2454
|
+
// formatting GTK prints them -- RnView.cpp's describe -- so that
|
|
2455
|
+
// scripts/compare_hosts.sh compares what each host actually draws.
|
|
2456
|
+
//
|
|
2457
|
+
// Both were missing here until this host drew them: it printed one circular
|
|
2458
|
+
// radius eight times and no border at all, and printing nothing was what let
|
|
2459
|
+
// the comparison say so rather than hide it. Radii are eight numbers because
|
|
2460
|
+
// React Native's are elliptical: top-left, top-right, bottom-right,
|
|
2461
|
+
// bottom-left, horizontal then vertical.
|
|
2462
|
+
if (hasCornerRadii_) {
|
|
2463
|
+
appendFormat(out,
|
|
2464
|
+
" radii=(%g,%g,%g,%g,%g,%g,%g,%g)",
|
|
2465
|
+
static_cast<double>(cornerRadii_[0]),
|
|
2466
|
+
static_cast<double>(cornerRadii_[1]),
|
|
2467
|
+
static_cast<double>(cornerRadii_[2]),
|
|
2468
|
+
static_cast<double>(cornerRadii_[3]),
|
|
2469
|
+
static_cast<double>(cornerRadii_[4]),
|
|
2470
|
+
static_cast<double>(cornerRadii_[5]),
|
|
2471
|
+
static_cast<double>(cornerRadii_[6]),
|
|
2472
|
+
static_cast<double>(cornerRadii_[7]));
|
|
2473
|
+
}
|
|
2474
|
+
// Top, right, bottom, left, for both.
|
|
2475
|
+
if (hasBorders_) {
|
|
2476
|
+
appendFormat(out,
|
|
2477
|
+
" borderw=(%g,%g,%g,%g)",
|
|
2478
|
+
static_cast<double>(borderWidths_[0]),
|
|
2479
|
+
static_cast<double>(borderWidths_[1]),
|
|
2480
|
+
static_cast<double>(borderWidths_[2]),
|
|
2481
|
+
static_cast<double>(borderWidths_[3]));
|
|
2482
|
+
out += " borderc=(";
|
|
2483
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
2484
|
+
const float *c = &borderColours_[edge * 4];
|
|
2485
|
+
appendFormat(out,
|
|
2486
|
+
"%s#%02x%02x%02x%02x",
|
|
2487
|
+
edge == 0 ? "" : ",",
|
|
2488
|
+
toByte(c[0]),
|
|
2489
|
+
toByte(c[1]),
|
|
2490
|
+
toByte(c[2]),
|
|
2491
|
+
toByte(c[3]));
|
|
2492
|
+
}
|
|
2493
|
+
out += ")";
|
|
2494
|
+
}
|
|
2495
|
+
// Gradients: how many, each one's line and stop count, where the image goes
|
|
2496
|
+
// and the tile it repeats in. Not every stop, which a transition hint can
|
|
2497
|
+
// turn into eleven of: what an end-to-end run needs is that the same gradient
|
|
2498
|
+
// arrived with the same geometry, and the stop fixup itself is asserted in
|
|
2499
|
+
// core's own tests. Spelled exactly as the other two hosts spell it.
|
|
2500
|
+
for (const Gradient &gradient : gradients_) {
|
|
2501
|
+
if (gradient.kind == Gradient::Kind::Radial) {
|
|
2502
|
+
appendFormat(out,
|
|
2503
|
+
" gradient=(radial (%g,%g) %gx%g,%u stops",
|
|
2504
|
+
static_cast<double>(gradient.centreX),
|
|
2505
|
+
static_cast<double>(gradient.centreY),
|
|
2506
|
+
static_cast<double>(gradient.radiusX),
|
|
2507
|
+
static_cast<double>(gradient.radiusY),
|
|
2508
|
+
static_cast<unsigned>(gradient.stops.size()));
|
|
2509
|
+
} else {
|
|
2510
|
+
appendFormat(out,
|
|
2511
|
+
" gradient=((%g,%g)-(%g,%g),%u stops",
|
|
2512
|
+
static_cast<double>(gradient.startX),
|
|
2513
|
+
static_cast<double>(gradient.startY),
|
|
2514
|
+
static_cast<double>(gradient.endX),
|
|
2515
|
+
static_cast<double>(gradient.endY),
|
|
2516
|
+
static_cast<unsigned>(gradient.stops.size()));
|
|
2517
|
+
}
|
|
2518
|
+
// `at=` is the rectangle the image fills and `tile=` is the period, printed
|
|
2519
|
+
// only when it repeats -- so a `no-repeat` background is the line without a
|
|
2520
|
+
// tile. The three background props are invisible in every other line here:
|
|
2521
|
+
// they move and repeat the image without changing the view at all.
|
|
2522
|
+
appendFormat(out,
|
|
2523
|
+
",at=(%g,%g %gx%g)",
|
|
2524
|
+
static_cast<double>(gradient.area[0]),
|
|
2525
|
+
static_cast<double>(gradient.area[1]),
|
|
2526
|
+
static_cast<double>(gradient.area[2]),
|
|
2527
|
+
static_cast<double>(gradient.area[3]));
|
|
2528
|
+
if (gradient.repeats) {
|
|
2529
|
+
appendFormat(out,
|
|
2530
|
+
",tile=(%g,%g %gx%g)",
|
|
2531
|
+
static_cast<double>(gradient.tile[0]),
|
|
2532
|
+
static_cast<double>(gradient.tile[1]),
|
|
2533
|
+
static_cast<double>(gradient.tile[2]),
|
|
2534
|
+
static_cast<double>(gradient.tile[3]));
|
|
2535
|
+
}
|
|
2536
|
+
out += ")";
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Box shadows, each in full and in the order the app wrote them. Nothing else
|
|
2540
|
+
// in this dump can say a shadow is there, and the numbers are the whole
|
|
2541
|
+
// feature: an offset that went to the wrong axis or a spread read as a blur
|
|
2542
|
+
// still draws a plausible shadow. Spelled exactly as the other two hosts
|
|
2543
|
+
// spell it, `inset ` included, because this line is read by one end-to-end
|
|
2544
|
+
// scenario on all three.
|
|
2545
|
+
for (const BoxShadow &shadow : boxShadows_) {
|
|
2546
|
+
appendFormat(out,
|
|
2547
|
+
" shadow=(%s%g,%g,%g,%g,#%02x%02x%02x%02x)",
|
|
2548
|
+
shadow.inset ? "inset " : "",
|
|
2549
|
+
static_cast<double>(shadow.dx),
|
|
2550
|
+
static_cast<double>(shadow.dy),
|
|
2551
|
+
static_cast<double>(shadow.blur),
|
|
2552
|
+
static_cast<double>(shadow.spread),
|
|
2553
|
+
toByte(shadow.colour[0]),
|
|
2554
|
+
toByte(shadow.colour[1]),
|
|
2555
|
+
toByte(shadow.colour[2]),
|
|
2556
|
+
toByte(shadow.colour[3]));
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
if (hasTransform_) {
|
|
2560
|
+
// The 2D affine part, in the order CSS writes a matrix(): a, b, c, d, tx,
|
|
2561
|
+
// ty. The other two platforms print the same six from their own matrix
|
|
2562
|
+
// types, so a transform is comparable across all three -- without which a
|
|
2563
|
+
// view rotated on one desktop and not on another looks identical here.
|
|
2564
|
+
appendFormat(out,
|
|
2565
|
+
" transform=(%g,%g,%g,%g,%g,%g)",
|
|
2566
|
+
static_cast<double>(transform_[0]),
|
|
2567
|
+
static_cast<double>(transform_[1]),
|
|
2568
|
+
static_cast<double>(transform_[2]),
|
|
2569
|
+
static_cast<double>(transform_[3]),
|
|
2570
|
+
static_cast<double>(transform_[4]),
|
|
2571
|
+
static_cast<double>(transform_[5]));
|
|
2572
|
+
}
|
|
2573
|
+
if (scrollX_ != 0.0f || scrollY_ != 0.0f) {
|
|
2574
|
+
appendFormat(out,
|
|
2575
|
+
" scroll=(%g,%g)",
|
|
2576
|
+
static_cast<double>(scrollX_),
|
|
2577
|
+
static_cast<double>(scrollY_));
|
|
2578
|
+
}
|
|
2579
|
+
// `pagingEnabled`, `snapToInterval`, `snapToOffsets` and the rest, which
|
|
2580
|
+
// decide where a fling ends and so are invisible to a dump taken while
|
|
2581
|
+
// nothing moves. Formatted in core, so all three hosts print the same words.
|
|
2582
|
+
out += scrollSnap_;
|
|
2583
|
+
// The overlay scrollbars, which are otherwise pure paint and so invisible to
|
|
2584
|
+
// every test this project has. Printed only when there is one, so a view that
|
|
2585
|
+
// does not scroll stays as short as it was.
|
|
2586
|
+
if (indicatorVerticalLength_ > 0.0f) {
|
|
2587
|
+
appendFormat(out,
|
|
2588
|
+
" scrollbar-v=(%g,%g)",
|
|
2589
|
+
static_cast<double>(indicatorVerticalOffset_),
|
|
2590
|
+
static_cast<double>(indicatorVerticalLength_));
|
|
2591
|
+
}
|
|
2592
|
+
if (indicatorHorizontalLength_ > 0.0f) {
|
|
2593
|
+
appendFormat(out,
|
|
2594
|
+
" scrollbar-h=(%g,%g)",
|
|
2595
|
+
static_cast<double>(indicatorHorizontalOffset_),
|
|
2596
|
+
static_cast<double>(indicatorHorizontalLength_));
|
|
2597
|
+
}
|
|
2598
|
+
// The thumb's colour, printed only when it is not the default black: a white
|
|
2599
|
+
// one is `indicatorStyle` having arrived, and the colour is the only part of
|
|
2600
|
+
// that a tree dump can see.
|
|
2601
|
+
if (indicatorColour_[0] != 0.0f || indicatorColour_[1] != 0.0f || indicatorColour_[2] != 0.0f) {
|
|
2602
|
+
appendFormat(out,
|
|
2603
|
+
" scrollbar-colour=(%g,%g,%g,%g)",
|
|
2604
|
+
static_cast<double>(indicatorColour_[0]),
|
|
2605
|
+
static_cast<double>(indicatorColour_[1]),
|
|
2606
|
+
static_cast<double>(indicatorColour_[2]),
|
|
2607
|
+
static_cast<double>(indicatorColour_[3]));
|
|
2608
|
+
}
|
|
2609
|
+
// Printed only when it is not the default, like every other field here.
|
|
2610
|
+
// Worth printing at all because it is invisible: a view with
|
|
2611
|
+
// `pointerEvents: none` is drawn exactly like one without, and the only way
|
|
2612
|
+
// the cross-host diff can say the prop arrived on all three is if each one
|
|
2613
|
+
// reports it.
|
|
2614
|
+
if (pointerEvents_ != PointerEvents::Auto) {
|
|
2615
|
+
appendFormat(out, " pe=%s", pointerEventsName(pointerEvents_));
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
if (image_ != nullptr) {
|
|
2619
|
+
// The same `texture=WxH fit=<name>` the other two hosts emit. The fit is
|
|
2620
|
+
// here because it is the only thing about a drawn image that a frame cannot
|
|
2621
|
+
// show: two views the same size holding the same picture are identical in
|
|
2622
|
+
// every other field of this dump and different on screen.
|
|
2623
|
+
appendFormat(out, " texture=%ux%u", image_->width(), image_->height());
|
|
2624
|
+
appendFormat(out, " fit=%s", imageFitName(imageFit_));
|
|
2625
|
+
// That this image moves, which no other line can show: an animated GIF and
|
|
2626
|
+
// its first frame are the same size and the same picture in a snapshot. Not
|
|
2627
|
+
// which frame, deliberately: the three hosts tick on their own clocks, so a
|
|
2628
|
+
// cross-host diff of that would be a race, and each suite asserts the
|
|
2629
|
+
// frames itself.
|
|
2630
|
+
if (imageFrames_.size() > 1) {
|
|
2631
|
+
out += " animated=1";
|
|
2632
|
+
}
|
|
2633
|
+
// And the blur, spelled as the other two hosts spell it. Invisible in this
|
|
2634
|
+
// dump otherwise: a blurred image has the same frame, the same texture and
|
|
2635
|
+
// the same fit as a sharp one.
|
|
2636
|
+
if (imageBlur_ > 0.0f) {
|
|
2637
|
+
appendFormat(out, " blur=%g", static_cast<double>(imageBlur_));
|
|
2638
|
+
}
|
|
2639
|
+
// Printed for the same reason the fit is, and in the same format the other
|
|
2640
|
+
// two hosts use, so the cross-host diff can compare them.
|
|
2641
|
+
if (hasImageTint_) {
|
|
2642
|
+
appendFormat(out,
|
|
2643
|
+
" tint=#%02x%02x%02x%02x",
|
|
2644
|
+
static_cast<unsigned>(imageTint_[0] * 255.0f + 0.5f),
|
|
2645
|
+
static_cast<unsigned>(imageTint_[1] * 255.0f + 0.5f),
|
|
2646
|
+
static_cast<unsigned>(imageTint_[2] * 255.0f + 0.5f),
|
|
2647
|
+
static_cast<unsigned>(imageTint_[3] * 255.0f + 0.5f));
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
if (textLayout_ != nullptr) {
|
|
2652
|
+
const std::string &text = textLayout_->text();
|
|
2653
|
+
if (!text.empty()) {
|
|
2654
|
+
out += " text=\"";
|
|
2655
|
+
appendEscaped(out, text);
|
|
2656
|
+
out += "\"";
|
|
2657
|
+
}
|
|
2658
|
+
// The edge the text sits against, resolved, and the direction the app asked
|
|
2659
|
+
// for: the two lines the other two hosts print, in the same words, from the
|
|
2660
|
+
// same functions in core. Neither can be seen any other way in a dump -- a
|
|
2661
|
+
// right-aligned paragraph has the same box and the same string as a
|
|
2662
|
+
// left-aligned one, and a right-to-left paragraph of Latin text has both.
|
|
2663
|
+
//
|
|
2664
|
+
// This used to print the resolved direction as a boolean, because that is
|
|
2665
|
+
// what this host keeps, and so it was the one host whose dump could not be
|
|
2666
|
+
// compared with another's. The name the app used is what crosses now.
|
|
2667
|
+
if (textAlignName_ != nullptr) {
|
|
2668
|
+
out += " text-align=";
|
|
2669
|
+
out += textAlignName_;
|
|
2670
|
+
}
|
|
2671
|
+
if (textVerticalAlignName_ != nullptr) {
|
|
2672
|
+
out += " text-valign=";
|
|
2673
|
+
out += textVerticalAlignName_;
|
|
2674
|
+
}
|
|
2675
|
+
// A <Text>'s own padding and border. Invisible in every other line here --
|
|
2676
|
+
// the frame is the box including the padding, and the string is the same
|
|
2677
|
+
// string -- and it decides where the text is drawn and how wide it wraps.
|
|
2678
|
+
if (textInsetLeft_ != 0.0f || textInsetTop_ != 0.0f || textInsetRight_ != 0.0f ||
|
|
2679
|
+
textInsetBottom_ != 0.0f) {
|
|
2680
|
+
appendFormat(out, " text-inset=%g,%g,%g,%g",
|
|
2681
|
+
static_cast<double>(textInsetLeft_),
|
|
2682
|
+
static_cast<double>(textInsetTop_),
|
|
2683
|
+
static_cast<double>(textInsetRight_),
|
|
2684
|
+
static_cast<double>(textInsetBottom_));
|
|
2685
|
+
}
|
|
2686
|
+
// `<Text selectable>`, and what is selected in it. Neither can be seen any
|
|
2687
|
+
// other way in a dump: the prop changes no box and the highlight is a wash
|
|
2688
|
+
// under the glyphs. The range is in this host's own unit -- UTF-16 code
|
|
2689
|
+
// units, where GTK counts bytes -- so an end-to-end scenario compares
|
|
2690
|
+
// "something is selected" rather than the numbers.
|
|
2691
|
+
if (textSelectable_) {
|
|
2692
|
+
out += " selectable";
|
|
2693
|
+
}
|
|
2694
|
+
if (textSelectionLength() > 0) {
|
|
2695
|
+
appendFormat(out, " selection=%d,%d", textSelectionStart(), textSelectionLength());
|
|
2696
|
+
}
|
|
2697
|
+
if (writingDirectionName_ != nullptr) {
|
|
2698
|
+
out += " writing-dir=";
|
|
2699
|
+
out += writingDirectionName_;
|
|
2700
|
+
}
|
|
2701
|
+
// The paragraph's text shadow, spelled as the other two hosts spell it: no
|
|
2702
|
+
// other line can show it, a shadowed paragraph having the same text, the
|
|
2703
|
+
// same colour and the same box. The standard deviation React Native parsed,
|
|
2704
|
+
// which is also what the shadow effect was given.
|
|
2705
|
+
if (textLayout_->hasShadow()) {
|
|
2706
|
+
const float *const colour = textLayout_->shadowColour();
|
|
2707
|
+
appendFormat(out,
|
|
2708
|
+
" text-shadow=(%g,%g,%g,#%02x%02x%02x%02x)",
|
|
2709
|
+
static_cast<double>(textLayout_->shadowDx()),
|
|
2710
|
+
static_cast<double>(textLayout_->shadowDy()),
|
|
2711
|
+
static_cast<double>(textLayout_->shadowStandardDeviation()),
|
|
2712
|
+
static_cast<unsigned>(colour[0] * 255.0f + 0.5f),
|
|
2713
|
+
static_cast<unsigned>(colour[1] * 255.0f + 0.5f),
|
|
2714
|
+
static_cast<unsigned>(colour[2] * 255.0f + 0.5f),
|
|
2715
|
+
static_cast<unsigned>(colour[3] * 255.0f + 0.5f));
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
// A text field's content lives in its EDIT peer, not in a layout, so it would
|
|
2720
|
+
// otherwise be invisible to every test that reads this tree -- and the dump
|
|
2721
|
+
// would differ from the other two hosts' for a field that was working.
|
|
2722
|
+
if (editablePeer_ != nullptr) {
|
|
2723
|
+
const int length = GetWindowTextLength(editablePeer_);
|
|
2724
|
+
std::wstring wide(static_cast<size_t>(length) + 1, L'\0');
|
|
2725
|
+
GetWindowText(editablePeer_, wide.data(), length + 1);
|
|
2726
|
+
wide.resize(static_cast<size_t>(length));
|
|
2727
|
+
|
|
2728
|
+
out += " editable=\"";
|
|
2729
|
+
appendEscaped(out, narrow(wide));
|
|
2730
|
+
out += "\"";
|
|
2731
|
+
|
|
2732
|
+
// GetFocus is per-thread rather than global, which is the right question
|
|
2733
|
+
// here: this runs on the thread that owns the window, and what is being
|
|
2734
|
+
// asked is whether the field has the keyboard within it.
|
|
2735
|
+
if (GetFocus() == editablePeer_) {
|
|
2736
|
+
out += " focused";
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// What a field asked for about spelling. Neither word appears for a field
|
|
2741
|
+
// that said nothing, which is the third state rather than a default; what
|
|
2742
|
+
// each host then did with it is in its own suite and on the support page.
|
|
2743
|
+
// This one can honour neither: a classic EDIT has no spell checker.
|
|
2744
|
+
if (!spellCheck_.empty()) {
|
|
2745
|
+
appendFormat(out, " spellcheck=%s", spellCheck_.c_str());
|
|
2746
|
+
}
|
|
2747
|
+
if (!autoCorrect_.empty()) {
|
|
2748
|
+
appendFormat(out, " autocorrect=%s", autoCorrect_.c_str());
|
|
2749
|
+
}
|
|
2750
|
+
// `autoCapitalize` and `keyboardType`, printed as the app wrote them so the
|
|
2751
|
+
// three dumps compare. This host turns the second into an input scope and the
|
|
2752
|
+
// first into a style bit for `characters` only; see Win32TextInput.cpp.
|
|
2753
|
+
if (!autoCapitalize_.empty()) {
|
|
2754
|
+
appendFormat(out, " autocapitalize=%s", autoCapitalize_.c_str());
|
|
2755
|
+
}
|
|
2756
|
+
if (!keyboardType_.empty()) {
|
|
2757
|
+
appendFormat(out, " keyboard=%s", keyboardType_.c_str());
|
|
2758
|
+
}
|
|
2759
|
+
// `caretHidden` and `contextMenuHidden`, printed only when asked for. Each is
|
|
2760
|
+
// the absence of something -- a blink, a menu -- so there is nothing else for
|
|
2761
|
+
// a test or the cross-host diff to look at.
|
|
2762
|
+
if (caretHidden_) {
|
|
2763
|
+
appendFormat(out, " caret=hidden");
|
|
2764
|
+
}
|
|
2765
|
+
if (contextMenuHidden_) {
|
|
2766
|
+
appendFormat(out, " context-menu=hidden");
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// How many DevTools highlights this view is drawing. In the dump because they
|
|
2770
|
+
// are otherwise invisible to everything but a screenshot, and because a
|
|
2771
|
+
// command that arrived and drew nothing is exactly the failure worth
|
|
2772
|
+
// catching.
|
|
2773
|
+
if (!highlights_.empty()) {
|
|
2774
|
+
appendFormat(out, " highlights=%d", static_cast<int>(highlights_.size()));
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// What kind of control this view is, and what state it is in. Written by
|
|
2778
|
+
// core/DesktopControls.h rather than formatted here, for the same reason the
|
|
2779
|
+
// role name below is React Native's vocabulary and not UIA's: three hosts
|
|
2780
|
+
// describing the same switch in three ways is a diff on every line.
|
|
2781
|
+
if (!controlDescription_.empty()) {
|
|
2782
|
+
appendFormat(out, " control=%s", controlDescription_.c_str());
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// The border's style, when it is not solid. Printed for the same reason the
|
|
2786
|
+
// widths are: a dashed border and a solid one are the same four widths and
|
|
2787
|
+
// the same four colours, and this is the only thing that can say the prop
|
|
2788
|
+
// arrived. Spelled as the other two hosts spell it.
|
|
2789
|
+
if (hasBorders_ && borderStyle_ != LineStyle::Solid) {
|
|
2790
|
+
appendFormat(out,
|
|
2791
|
+
" border-style=%s",
|
|
2792
|
+
borderStyle_ == LineStyle::Dotted ? "dotted" : "dashed");
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// The cursor the app asked for, which is invisible in a frame: the pointer is
|
|
2796
|
+
// not part of the picture. Printed for the reason `pointerEvents` is, and in
|
|
2797
|
+
// CSS's words rather than Win32's, so the three hosts' lines agree.
|
|
2798
|
+
if (!cursor_.empty()) {
|
|
2799
|
+
appendFormat(out, " cursor=%s", cursor_.c_str());
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// The blend mode the app asked for, which a frame cannot show unless there is
|
|
2803
|
+
// something beneath it. The keyword rather than Direct2D's mode, so the line
|
|
2804
|
+
// is comparable with the other two hosts', and printed even for a keyword
|
|
2805
|
+
// this host cannot blend: the dump says what was asked for.
|
|
2806
|
+
if (!blendMode_.empty()) {
|
|
2807
|
+
appendFormat(out, " blend=%s", blendMode_.c_str());
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// The outline, which is invisible in every other line: it is not a border,
|
|
2811
|
+
// and a view with one has the same frame and the same colours without it.
|
|
2812
|
+
// Spelled exactly as the other two hosts spell it, style included only when
|
|
2813
|
+
// it is not solid, because this line is diffed across the three.
|
|
2814
|
+
if (outlineWidth_ > 0.0f) {
|
|
2815
|
+
appendFormat(out,
|
|
2816
|
+
" outline=(%g,%g,#%02x%02x%02x%02x",
|
|
2817
|
+
static_cast<double>(outlineWidth_),
|
|
2818
|
+
static_cast<double>(outlineOffset_),
|
|
2819
|
+
static_cast<unsigned>(outlineColour_[0] * 255.0f + 0.5f),
|
|
2820
|
+
static_cast<unsigned>(outlineColour_[1] * 255.0f + 0.5f),
|
|
2821
|
+
static_cast<unsigned>(outlineColour_[2] * 255.0f + 0.5f),
|
|
2822
|
+
static_cast<unsigned>(outlineColour_[3] * 255.0f + 0.5f));
|
|
2823
|
+
if (outlineStyle_ != LineStyle::Solid) {
|
|
2824
|
+
appendFormat(out, ",%s", outlineStyle_ == LineStyle::Dotted ? "dotted" : "dashed");
|
|
2825
|
+
}
|
|
2826
|
+
out += ")";
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// `filter`, as the pieces it came to plus what its matrix makes of one probe
|
|
2830
|
+
// colour, which is the format the other two hosts print and the end-to-end
|
|
2831
|
+
// run reads. Sixteen numbers would drown the line; one colour is eight
|
|
2832
|
+
// characters and still fails when a matrix is wrong. The probe is
|
|
2833
|
+
// (1, 0.5, 0.25) so that no two channels can be swapped without the answer
|
|
2834
|
+
// changing.
|
|
2835
|
+
if (!filters_.empty()) {
|
|
2836
|
+
out += " filter=(";
|
|
2837
|
+
bool first = true;
|
|
2838
|
+
if (filters_.hasMatrix) {
|
|
2839
|
+
const float probe[4] = {1.0f, 0.5f, 0.25f, 1.0f};
|
|
2840
|
+
float result[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
2841
|
+
for (int row = 0; row < 4; row++) {
|
|
2842
|
+
result[row] = filters_.offset[row];
|
|
2843
|
+
for (int column = 0; column < 4; column++) {
|
|
2844
|
+
result[row] += filters_.matrix[row * 4 + column] * probe[column];
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
const auto byte = [](float value) {
|
|
2848
|
+
const float clamped = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
|
|
2849
|
+
return static_cast<unsigned>(clamped * 255.0f + 0.5f);
|
|
2850
|
+
};
|
|
2851
|
+
appendFormat(out,
|
|
2852
|
+
"probe=#%02x%02x%02x%02x",
|
|
2853
|
+
byte(result[0]),
|
|
2854
|
+
byte(result[1]),
|
|
2855
|
+
byte(result[2]),
|
|
2856
|
+
byte(result[3]));
|
|
2857
|
+
first = false;
|
|
2858
|
+
}
|
|
2859
|
+
if (filters_.blurRadius > 0.0f) {
|
|
2860
|
+
appendFormat(out,
|
|
2861
|
+
"%sblur=%g",
|
|
2862
|
+
first ? "" : ",",
|
|
2863
|
+
static_cast<double>(filters_.blurRadius));
|
|
2864
|
+
first = false;
|
|
2865
|
+
}
|
|
2866
|
+
if (filters_.opacity < 1.0f) {
|
|
2867
|
+
appendFormat(out,
|
|
2868
|
+
"%sopacity=%g",
|
|
2869
|
+
first ? "" : ",",
|
|
2870
|
+
static_cast<double>(filters_.opacity));
|
|
2871
|
+
first = false;
|
|
2872
|
+
}
|
|
2873
|
+
// Each drop shadow, with the standard deviation React Native parsed: that
|
|
2874
|
+
// is the number all three hosts are handed, so it is the one a cross-host
|
|
2875
|
+
// diff should compare.
|
|
2876
|
+
for (const FilterShadow &shadow : filters_.shadows) {
|
|
2877
|
+
appendFormat(out,
|
|
2878
|
+
"%sshadow=(%g,%g,%g,#%02x%02x%02x%02x)",
|
|
2879
|
+
first ? "" : ",",
|
|
2880
|
+
static_cast<double>(shadow.dx),
|
|
2881
|
+
static_cast<double>(shadow.dy),
|
|
2882
|
+
static_cast<double>(shadow.standardDeviation),
|
|
2883
|
+
static_cast<unsigned>(shadow.colour[0] * 255.0f + 0.5f),
|
|
2884
|
+
static_cast<unsigned>(shadow.colour[1] * 255.0f + 0.5f),
|
|
2885
|
+
static_cast<unsigned>(shadow.colour[2] * 255.0f + 0.5f),
|
|
2886
|
+
static_cast<unsigned>(shadow.colour[3] * 255.0f + 0.5f));
|
|
2887
|
+
first = false;
|
|
2888
|
+
}
|
|
2889
|
+
out += ")";
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// `hitSlop`, which is invisible in every other line of this dump: a view with
|
|
2893
|
+
// a bigger target is drawn exactly like one without. Before `role=` because
|
|
2894
|
+
// that is where the other two hosts print it, and this dump is diffed line by
|
|
2895
|
+
// line.
|
|
2896
|
+
if (hasHitSlop_) {
|
|
2897
|
+
appendFormat(out,
|
|
2898
|
+
" hit-slop=(%g,%g,%g,%g)",
|
|
2899
|
+
static_cast<double>(hitSlop_[0]),
|
|
2900
|
+
static_cast<double>(hitSlop_[1]),
|
|
2901
|
+
static_cast<double>(hitSlop_[2]),
|
|
2902
|
+
static_cast<double>(hitSlop_[3]));
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
// The resolved LABELLED_BY relation, by tag. The ids the app wrote are in its
|
|
2906
|
+
// own source; what is worth reporting is that they were resolved, and the
|
|
2907
|
+
// tags are Fabric's, so the three hosts print the same ones.
|
|
2908
|
+
if (!labelledBy_.empty()) {
|
|
2909
|
+
out += " labelled-by=";
|
|
2910
|
+
bool first = true;
|
|
2911
|
+
for (const RnWin32View *label : labelledBy_) {
|
|
2912
|
+
if (label == nullptr) {
|
|
2913
|
+
continue;
|
|
2914
|
+
}
|
|
2915
|
+
appendFormat(out, "%s%d", first ? "" : ",", label->tag());
|
|
2916
|
+
first = false;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// React Native's role name, not UIA's. This dump is compared line by line
|
|
2921
|
+
// across three platforms, and each reporting its own toolkit's vocabulary
|
|
2922
|
+
// would make every accessible view look like a difference. That the *UIA*
|
|
2923
|
+
// control type was really applied is asserted in
|
|
2924
|
+
// tests/test_win32_accessibility.cpp, which is where a platform question
|
|
2925
|
+
// belongs.
|
|
2926
|
+
if (!accessible_.role.empty()) {
|
|
2927
|
+
appendFormat(out, " role=%s", accessible_.role.c_str());
|
|
2928
|
+
}
|
|
2929
|
+
// `testID`, as React Native spelled it: the same division as the role above,
|
|
2930
|
+
// and that UIA really published it as the automation id is asserted in
|
|
2931
|
+
// tests/test_win32_accessibility.cpp.
|
|
2932
|
+
if (!accessible_.testId.empty()) {
|
|
2933
|
+
appendFormat(out, " testid=%s", accessible_.testId.c_str());
|
|
2934
|
+
}
|
|
2935
|
+
// Whether Tab stops here, after `role=` because that is where the other two
|
|
2936
|
+
// hosts print it and this dump is diffed line by line. Whether it is focused
|
|
2937
|
+
// *now* is deliberately not printed: that depends on what the window manager
|
|
2938
|
+
// did when the window opened, which is not a property of the platform and
|
|
2939
|
+
// would make this dump differ between two machines running the same app.
|
|
2940
|
+
if (focusable_) {
|
|
2941
|
+
out += " focusable";
|
|
2942
|
+
}
|
|
2943
|
+
// `accessibilityViewIsModal`, after `focusable` because that is where the
|
|
2944
|
+
// other two hosts print it and this dump is diffed line by line.
|
|
2945
|
+
if (accessible_.modal) {
|
|
2946
|
+
out += " modal";
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
out += "\n";
|
|
2950
|
+
|
|
2951
|
+
for (const RnWin32View *child : children_) {
|
|
2952
|
+
child->describeInto(out, depth + 1);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
} // namespace basalt::win32
|