basalt-win32 0.0.0-rc5
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 +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -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 +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -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 +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -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 +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -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 +515 -0
- package/native/win32/Win32ScrollView.h +208 -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 +811 -0
- package/native/win32/Win32TextInput.h +232 -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 +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -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 +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,1281 @@
|
|
|
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 "Win32Strings.h"
|
|
11
|
+
|
|
12
|
+
// Before d2d1.h, which wants the base Windows types and does not pull them in
|
|
13
|
+
// itself. NOMINMAX and WIN32_LEAN_AND_MEAN come from the package's CMakeLists;
|
|
14
|
+
// without the first, windows.h defines `min` and `max` as macros and breaks
|
|
15
|
+
// <algorithm> below at a distance.
|
|
16
|
+
#include <windows.h>
|
|
17
|
+
|
|
18
|
+
#include <d2d1.h>
|
|
19
|
+
#include <d2d1helper.h>
|
|
20
|
+
#include <wrl/client.h>
|
|
21
|
+
|
|
22
|
+
#include <algorithm>
|
|
23
|
+
#include <chrono>
|
|
24
|
+
#include <utility>
|
|
25
|
+
#include <cmath>
|
|
26
|
+
#include <cstdio>
|
|
27
|
+
|
|
28
|
+
using Microsoft::WRL::ComPtr;
|
|
29
|
+
|
|
30
|
+
namespace basalt::win32 {
|
|
31
|
+
|
|
32
|
+
using basalt::kFocusRingAlpha;
|
|
33
|
+
using basalt::kFocusRingBlue;
|
|
34
|
+
using basalt::kFocusRingGreen;
|
|
35
|
+
using basalt::kFocusRingRed;
|
|
36
|
+
using basalt::kFocusRingWidth;
|
|
37
|
+
|
|
38
|
+
namespace {
|
|
39
|
+
|
|
40
|
+
// A scoped opacity layer, for `opacity` on a view.
|
|
41
|
+
//
|
|
42
|
+
// A layer rather than multiplying every brush's alpha: React Native's opacity
|
|
43
|
+
// composites the subtree as a unit, so two overlapping half-transparent
|
|
44
|
+
// children do not show through each other.
|
|
45
|
+
class ScopedOpacity {
|
|
46
|
+
public:
|
|
47
|
+
ScopedOpacity(ID2D1RenderTarget *target, float opacity) : target_(target) {
|
|
48
|
+
if (opacity >= 1.0f) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (FAILED(target_->CreateLayer(nullptr, layer_.GetAddressOf()))) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
auto parameters = D2D1::LayerParameters();
|
|
55
|
+
parameters.contentBounds = D2D1::InfiniteRect();
|
|
56
|
+
parameters.opacity = opacity;
|
|
57
|
+
target_->PushLayer(parameters, layer_.Get());
|
|
58
|
+
pushed_ = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
~ScopedOpacity() {
|
|
62
|
+
if (pushed_) {
|
|
63
|
+
target_->PopLayer();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ScopedOpacity(const ScopedOpacity &) = delete;
|
|
68
|
+
ScopedOpacity &operator=(const ScopedOpacity &) = delete;
|
|
69
|
+
|
|
70
|
+
private:
|
|
71
|
+
ID2D1RenderTarget *target_;
|
|
72
|
+
ComPtr<ID2D1Layer> layer_;
|
|
73
|
+
bool pushed_ = false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Escaped the way g_strescape's output is on the GTK side, so a string with a
|
|
77
|
+
// quote or a newline in it stays one line and stays comparable.
|
|
78
|
+
void appendEscaped(std::string &out, const std::string &text) {
|
|
79
|
+
for (const char c : text) {
|
|
80
|
+
switch (c) {
|
|
81
|
+
case '\\':
|
|
82
|
+
out += "\\\\";
|
|
83
|
+
break;
|
|
84
|
+
case '"':
|
|
85
|
+
out += "\\\"";
|
|
86
|
+
break;
|
|
87
|
+
case '\n':
|
|
88
|
+
out += "\\n";
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
out += c;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
template <typename... Args>
|
|
98
|
+
void appendFormat(std::string &out, const char *format, Args... args) {
|
|
99
|
+
char buffer[256];
|
|
100
|
+
const int written = std::snprintf(buffer, sizeof(buffer), format, args...);
|
|
101
|
+
if (written > 0) {
|
|
102
|
+
// snprintf returns what it *would* have written, so a truncated field is
|
|
103
|
+
// clamped here rather than read past the buffer.
|
|
104
|
+
const int limit = static_cast<int>(sizeof(buffer)) - 1;
|
|
105
|
+
out.append(buffer, static_cast<size_t>(written < limit ? written : limit));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
unsigned toByte(float component) {
|
|
110
|
+
return static_cast<unsigned>(component * 255.0f + 0.5f);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Two 2D affine matrices, in Direct2D's Matrix3x2F order and its row-vector
|
|
114
|
+
// convention: `compose(a, b)` is "apply a, then b", which is what
|
|
115
|
+
// `Matrix3x2F::SetProduct(a, b)` computes. Written out in floats rather than
|
|
116
|
+
// built with D2D1::Matrix3x2F so that hit testing -- which wants none of
|
|
117
|
+
// Direct2D -- can use the same composition painting does.
|
|
118
|
+
void compose(const float a[6], const float b[6], float out[6]) {
|
|
119
|
+
const float r[6] = {
|
|
120
|
+
a[0] * b[0] + a[1] * b[2],
|
|
121
|
+
a[0] * b[1] + a[1] * b[3],
|
|
122
|
+
a[2] * b[0] + a[3] * b[2],
|
|
123
|
+
a[2] * b[1] + a[3] * b[3],
|
|
124
|
+
a[4] * b[0] + a[5] * b[2] + b[4],
|
|
125
|
+
a[4] * b[1] + a[5] * b[3] + b[5],
|
|
126
|
+
};
|
|
127
|
+
for (int i = 0; i < 6; i++) {
|
|
128
|
+
out[i] = r[i];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Maps a point through the inverse of `m`. False when `m` is singular, which is
|
|
133
|
+
// a view scaled to nothing: it paints no pixels, so nothing can be over it.
|
|
134
|
+
bool invertPoint(const float m[6], float x, float y, float &outX, float &outY) {
|
|
135
|
+
const float determinant = m[0] * m[3] - m[1] * m[2];
|
|
136
|
+
if (std::fabs(determinant) < 1e-6f) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const float shiftedX = x - m[4];
|
|
140
|
+
const float shiftedY = y - m[5];
|
|
141
|
+
outX = (shiftedX * m[3] - shiftedY * m[2]) / determinant;
|
|
142
|
+
outY = (shiftedY * m[0] - shiftedX * m[1]) / determinant;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
} // namespace
|
|
147
|
+
|
|
148
|
+
RnWin32View::RnWin32View(int32_t tag) : tag_(tag) {}
|
|
149
|
+
|
|
150
|
+
RnWin32View::~RnWin32View() {
|
|
151
|
+
// A view owns neither its children nor its parent; the registry owns every
|
|
152
|
+
// view. What a destructor does owe is that nothing is left pointing at it.
|
|
153
|
+
if (parent_ != nullptr) {
|
|
154
|
+
parent_->removeChild(this);
|
|
155
|
+
}
|
|
156
|
+
for (RnWin32View *child : children_) {
|
|
157
|
+
child->parent_ = nullptr;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Geometry --------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
void RnWin32View::setFrame(float x, float y, float width, float height) {
|
|
164
|
+
frame_ = RnRect{x, y, width, height};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void RnWin32View::setScrollOffset(float x, float y) {
|
|
168
|
+
scrollX_ = x;
|
|
169
|
+
scrollY_ = y;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
void RnWin32View::setScrollIndicators(float verticalOffset,
|
|
173
|
+
float verticalLength,
|
|
174
|
+
float horizontalOffset,
|
|
175
|
+
float horizontalLength) {
|
|
176
|
+
indicatorVerticalOffset_ = verticalOffset;
|
|
177
|
+
indicatorVerticalLength_ = verticalLength;
|
|
178
|
+
indicatorHorizontalOffset_ = horizontalOffset;
|
|
179
|
+
indicatorHorizontalLength_ = horizontalLength;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Appearance ------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
void RnWin32View::setBackgroundColor(float red,
|
|
185
|
+
float green,
|
|
186
|
+
float blue,
|
|
187
|
+
float alpha,
|
|
188
|
+
bool hasColor) {
|
|
189
|
+
hasBackgroundColor_ = hasColor;
|
|
190
|
+
backgroundColor_[0] = red;
|
|
191
|
+
backgroundColor_[1] = green;
|
|
192
|
+
backgroundColor_[2] = blue;
|
|
193
|
+
backgroundColor_[3] = alpha;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void RnWin32View::setOpacity(float opacity) {
|
|
197
|
+
opacity_ = opacity;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
void RnWin32View::setTransform(const float *matrix16) {
|
|
201
|
+
if (matrix16 == nullptr) {
|
|
202
|
+
hasTransform_ = false;
|
|
203
|
+
transform_[0] = 1.0f;
|
|
204
|
+
transform_[1] = 0.0f;
|
|
205
|
+
transform_[2] = 0.0f;
|
|
206
|
+
transform_[3] = 1.0f;
|
|
207
|
+
transform_[4] = 0.0f;
|
|
208
|
+
transform_[5] = 0.0f;
|
|
209
|
+
updateBackFace();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// CSS matrix3d order, which is column-major: m11 m12 m13 m14 m21 ... So the
|
|
214
|
+
// 2D affine part is elements 0, 1, 4, 5, 12 and 13, and those are the same
|
|
215
|
+
// six the GTK side pulls out of its graphene matrix and the macOS side out of
|
|
216
|
+
// its CATransform3D.
|
|
217
|
+
hasTransform_ = true;
|
|
218
|
+
transform_[0] = matrix16[0];
|
|
219
|
+
transform_[1] = matrix16[1];
|
|
220
|
+
transform_[2] = matrix16[4];
|
|
221
|
+
transform_[3] = matrix16[5];
|
|
222
|
+
transform_[4] = matrix16[12];
|
|
223
|
+
transform_[5] = matrix16[13];
|
|
224
|
+
updateBackFace();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
void RnWin32View::setHidesBackFace(bool hides) {
|
|
228
|
+
if (hidesBackFace_ == hides) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
hidesBackFace_ = hides;
|
|
232
|
+
updateBackFace();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Hidden the same way the app's own `display: none` hides a view, because the
|
|
236
|
+
// effect is the same -- not painted, not hit tested, children included.
|
|
237
|
+
//
|
|
238
|
+
// The two reasons are kept apart and combined here. `hidden_` is one flag and
|
|
239
|
+
// `display: none` writes it too, so whichever of the two spoke last used to
|
|
240
|
+
// answer for both -- and layout metrics are applied after props, so a card
|
|
241
|
+
// turned away from the viewer was reliably un-hidden a moment later.
|
|
242
|
+
void RnWin32View::applyVisibility() {
|
|
243
|
+
hidden_ = hiddenByApp_ || hiddenByBackFace_;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
void RnWin32View::updateBackFace() {
|
|
247
|
+
// Recomputed rather than returned early on, so that turning the prop off
|
|
248
|
+
// shows a view it had hidden instead of leaving it hidden forever.
|
|
249
|
+
hiddenByBackFace_ =
|
|
250
|
+
hidesBackFace_ &&
|
|
251
|
+
basalt::facesAway(transform_[0], transform_[1], transform_[2], transform_[3]);
|
|
252
|
+
applyVisibility();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
void RnWin32View::setClipsChildren(bool clips) {
|
|
256
|
+
clipsChildren_ = clips;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
void RnWin32View::setCornerRadius(float radius) {
|
|
260
|
+
const float radii[8] = {radius, radius, radius, radius, radius, radius, radius, radius};
|
|
261
|
+
setCornerRadii(radii);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
void RnWin32View::setCornerRadii(const float radii[8]) {
|
|
265
|
+
hasCornerRadii_ = false;
|
|
266
|
+
for (int i = 0; i < 8; i++) {
|
|
267
|
+
cornerRadii_[i] = radii != nullptr ? radii[i] : 0.0f;
|
|
268
|
+
if (cornerRadii_[i] > 0.0f) {
|
|
269
|
+
hasCornerRadii_ = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
void RnWin32View::setBorders(const float widths[4], const float colours[16]) {
|
|
275
|
+
// "Has a border" is decided here, once, the way rn_view_set_borders decides
|
|
276
|
+
// it on GTK: some edge both wide and visible. Paint and the tree dump both
|
|
277
|
+
// read this flag, so they cannot disagree about whether a border exists.
|
|
278
|
+
hasBorders_ = false;
|
|
279
|
+
for (int i = 0; i < 4; i++) {
|
|
280
|
+
borderWidths_[i] = widths != nullptr ? widths[i] : 0.0f;
|
|
281
|
+
for (int c = 0; c < 4; c++) {
|
|
282
|
+
borderColours_[i * 4 + c] = colours != nullptr ? colours[i * 4 + c] : 0.0f;
|
|
283
|
+
}
|
|
284
|
+
if (borderWidths_[i] > 0.0f && borderColours_[i * 4 + 3] > 0.0f) {
|
|
285
|
+
hasBorders_ = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
void RnWin32View::setZIndex(int zIndex) {
|
|
291
|
+
zIndex_ = zIndex;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
void RnWin32View::setHidden(bool hidden) {
|
|
295
|
+
hiddenByApp_ = hidden;
|
|
296
|
+
applyVisibility();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Content ----------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
void RnWin32View::setTextLayout(std::shared_ptr<RnWin32TextLayout> layout) {
|
|
302
|
+
textLayout_ = std::move(layout);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
void RnWin32View::setImageTint(bool hasTint, const float components[4]) {
|
|
306
|
+
// No repaint request here, for the same reason `setImage` makes none: this
|
|
307
|
+
// host invalidates once per mounted transaction rather than per view. See
|
|
308
|
+
// requestRepaint in main_win32.cpp.
|
|
309
|
+
hasImageTint_ = hasTint;
|
|
310
|
+
if (hasTint && components != nullptr) {
|
|
311
|
+
for (int i = 0; i < 4; i++) {
|
|
312
|
+
imageTint_[i] = components[i];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
void RnWin32View::setImage(std::shared_ptr<RnWin32Image> image, RnImageFit fit) {
|
|
318
|
+
image_ = std::move(image);
|
|
319
|
+
imageFit_ = fit;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
void RnWin32View::setPainter(std::shared_ptr<RnWin32Painter> painter) {
|
|
323
|
+
painter_ = std::move(painter);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- Accessibility ----------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
void RnWin32View::setAccessibleInfo(const RnAccessibleInfo &info) {
|
|
329
|
+
accessible_ = info;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
IRawElementProviderSimple *RnWin32View::createAccessibleProvider() const {
|
|
333
|
+
return basalt::win32::createAccessibleProvider(accessible_);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// --- Geometry, resolved -----------------------------------------------------
|
|
337
|
+
|
|
338
|
+
void RnWin32View::localToParent(float out[6]) const {
|
|
339
|
+
const float translation[6] = {1.0f, 0.0f, 0.0f, 1.0f, frame_.x, frame_.y};
|
|
340
|
+
if (!hasTransform_) {
|
|
341
|
+
for (int i = 0; i < 6; i++) {
|
|
342
|
+
out[i] = translation[i];
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Anchored at the view's centre by moving the centre to the origin and back
|
|
348
|
+
// around the transform, which is what React Native means by an untouched
|
|
349
|
+
// `transformOrigin` and what every other platform here does. The frame's
|
|
350
|
+
// translation comes last because this composes "apply a, then b".
|
|
351
|
+
const float centreX = frame_.width / 2.0f;
|
|
352
|
+
const float centreY = frame_.height / 2.0f;
|
|
353
|
+
const float toOrigin[6] = {1.0f, 0.0f, 0.0f, 1.0f, -centreX, -centreY};
|
|
354
|
+
const float fromOrigin[6] = {1.0f, 0.0f, 0.0f, 1.0f, centreX, centreY};
|
|
355
|
+
|
|
356
|
+
float composed[6];
|
|
357
|
+
compose(toOrigin, transform_, composed);
|
|
358
|
+
compose(composed, fromOrigin, composed);
|
|
359
|
+
compose(composed, translation, out);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
bool RnWin32View::pageToLocal(const RnWin32View *root,
|
|
363
|
+
float pageX,
|
|
364
|
+
float pageY,
|
|
365
|
+
float &outX,
|
|
366
|
+
float &outY) const {
|
|
367
|
+
float toPage[6] = {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f};
|
|
368
|
+
|
|
369
|
+
for (const RnWin32View *node = this; node != nullptr && node != root;) {
|
|
370
|
+
float local[6];
|
|
371
|
+
node->localToParent(local);
|
|
372
|
+
compose(toPage, local, toPage);
|
|
373
|
+
|
|
374
|
+
const RnWin32View *parent = node->parent();
|
|
375
|
+
if (parent == nullptr) {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
// A scrolled ancestor moves its children by its offset, which is not part
|
|
379
|
+
// of any child's own placement -- the same offset `paintChildren`
|
|
380
|
+
// subtracts and `hitTest` adds back.
|
|
381
|
+
const float scroll[6] = {1.0f, 0.0f, 0.0f, 1.0f, -parent->scrollX(), -parent->scrollY()};
|
|
382
|
+
compose(toPage, scroll, toPage);
|
|
383
|
+
node = parent;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return invertPoint(toPage, pageX, pageY, outX, outY);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
std::vector<RnWin32View *> RnWin32View::childrenInPaintOrder() const {
|
|
390
|
+
std::vector<RnWin32View *> ordered = children_;
|
|
391
|
+
const bool needsSorting =
|
|
392
|
+
std::any_of(ordered.begin(), ordered.end(), [](const RnWin32View *child) {
|
|
393
|
+
return child->zIndex() != 0;
|
|
394
|
+
});
|
|
395
|
+
if (needsSorting) {
|
|
396
|
+
// Stable, so equal zIndex keeps document order -- which is what CSS and
|
|
397
|
+
// React Native both promise.
|
|
398
|
+
std::stable_sort(
|
|
399
|
+
ordered.begin(), ordered.end(), [](const RnWin32View *a, const RnWin32View *b) {
|
|
400
|
+
return a->zIndex() < b->zIndex();
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return ordered;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- Tree ------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
void RnWin32View::insertChild(RnWin32View *child, int index) {
|
|
409
|
+
if (child == nullptr || child == this) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (child->parent_ != nullptr) {
|
|
413
|
+
child->parent_->removeChild(child);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Fabric's index counts positions in the parent's *final* child list, so a
|
|
417
|
+
// later mutation lands between two existing children. Clamped rather than
|
|
418
|
+
// trusted: a transaction racing a surface teardown can name a position that
|
|
419
|
+
// no longer exists, and that has to be survivable.
|
|
420
|
+
const int count = static_cast<int>(children_.size());
|
|
421
|
+
const int at = std::clamp(index, 0, count);
|
|
422
|
+
children_.insert(children_.begin() + at, child);
|
|
423
|
+
child->parent_ = this;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
void RnWin32View::removeChild(RnWin32View *child) {
|
|
427
|
+
const auto it = std::find(children_.begin(), children_.end(), child);
|
|
428
|
+
if (it == children_.end()) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
(*it)->parent_ = nullptr;
|
|
432
|
+
children_.erase(it);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// --- Painting --------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
void RnWin32View::paint(ID2D1RenderTarget *target) const {
|
|
438
|
+
if (target == nullptr || hidden_) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
D2D1::Matrix3x2F parentTransform;
|
|
443
|
+
target->GetTransform(&parentTransform);
|
|
444
|
+
|
|
445
|
+
// From localToParent rather than composed here, so that hit testing -- which
|
|
446
|
+
// inverts the same six numbers -- cannot end up with a different idea of
|
|
447
|
+
// where this view is.
|
|
448
|
+
float localValues[6];
|
|
449
|
+
localToParent(localValues);
|
|
450
|
+
const D2D1::Matrix3x2F local(localValues[0],
|
|
451
|
+
localValues[1],
|
|
452
|
+
localValues[2],
|
|
453
|
+
localValues[3],
|
|
454
|
+
localValues[4],
|
|
455
|
+
localValues[5]);
|
|
456
|
+
target->SetTransform(local * parentTransform);
|
|
457
|
+
|
|
458
|
+
const D2D1_RECT_F bounds = D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height);
|
|
459
|
+
|
|
460
|
+
{
|
|
461
|
+
const ScopedOpacity fade(target, opacity_);
|
|
462
|
+
|
|
463
|
+
// The background is always clipped to the rounded box, even when children
|
|
464
|
+
// are not: `overflow: visible` lets a child escape the corner, but the
|
|
465
|
+
// view's own fill still has to respect its border radius.
|
|
466
|
+
if (hasBackgroundColor_) {
|
|
467
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
468
|
+
// An explicit sRGB colour. Letting the target pick a device space shifts
|
|
469
|
+
// every colour slightly on a wide-gamut display -- not wrong exactly, but
|
|
470
|
+
// different from the same app on Linux, which is the one thing this
|
|
471
|
+
// project is trying not to be.
|
|
472
|
+
const D2D1_COLOR_F colour = D2D1::ColorF(
|
|
473
|
+
backgroundColor_[0], backgroundColor_[1], backgroundColor_[2], backgroundColor_[3]);
|
|
474
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
475
|
+
if (hasCornerRadii_) {
|
|
476
|
+
// The real shape: each corner its own, elliptical where React Native
|
|
477
|
+
// says so. See roundedBoxGeometry in Win32Clip.h.
|
|
478
|
+
ComPtr<ID2D1Factory> factory;
|
|
479
|
+
target->GetFactory(factory.GetAddressOf());
|
|
480
|
+
const ComPtr<ID2D1Geometry> box = roundedBoxGeometry(factory.Get(), bounds, cornerRadii_);
|
|
481
|
+
if (box) {
|
|
482
|
+
target->FillGeometry(box.Get(), brush.Get());
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
target->FillRectangle(bounds, brush.Get());
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Then the image, then the text, then the children -- the order
|
|
491
|
+
// `rn_view_snapshot` uses on GTK. Nothing in React Native puts two of these
|
|
492
|
+
// on one view, but the order still has to be decided somewhere, and it is
|
|
493
|
+
// cheaper to match than to argue about later.
|
|
494
|
+
if (image_ != nullptr) {
|
|
495
|
+
// The four floats as stored; RnWin32Image builds the Direct2D colour,
|
|
496
|
+
// where d2d1.h is in scope. See its header for why it takes floats.
|
|
497
|
+
image_->draw(target,
|
|
498
|
+
frame_.width,
|
|
499
|
+
frame_.height,
|
|
500
|
+
imageFit_,
|
|
501
|
+
hasImageTint_ ? imageTint_ : nullptr);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Then anything this layer cannot draw itself -- the Skia surface behind a
|
|
505
|
+
// `<Canvas>`, and nothing else so far. Here rather than in its own pass so
|
|
506
|
+
// that it is inside the clip, the transform and the opacity this view has
|
|
507
|
+
// already applied, which is the whole point of drawing it in the walk.
|
|
508
|
+
if (painter_ != nullptr) {
|
|
509
|
+
painter_->draw(target, frame_.width, frame_.height);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Text sits above the background and below any children, which is the
|
|
513
|
+
// order `<Text>` with nested views expects. The paragraph draws itself at
|
|
514
|
+
// the view's own origin, in the box Yoga gave the view -- the same box it
|
|
515
|
+
// was measured against, because both go through RnWin32TextLayout.
|
|
516
|
+
if (textLayout_ != nullptr) {
|
|
517
|
+
textLayout_->draw(target, frame_.width, frame_.height);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// A control sits where the text would: above the background, below the
|
|
521
|
+
// children. Nothing in React Native puts children inside a <Switch>, but
|
|
522
|
+
// the order still has to be decided somewhere.
|
|
523
|
+
if (control_ != Control::None) {
|
|
524
|
+
paintControl(target);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
paintChildren(target);
|
|
528
|
+
|
|
529
|
+
// paintChildren leaves a ScrollView's offset applied, and nothing below
|
|
530
|
+
// here scrolls with the content -- so put the transform back to this view's
|
|
531
|
+
// own before drawing any of it.
|
|
532
|
+
target->SetTransform(local * parentTransform);
|
|
533
|
+
|
|
534
|
+
// The scrollbars: an overlay, so above the content and above any children,
|
|
535
|
+
// which is what "overlay indicator" means -- and below the DevTools overlay
|
|
536
|
+
// and the focus ring, neither of which belongs to the app.
|
|
537
|
+
if (indicatorVerticalLength_ > 0.0f || indicatorHorizontalLength_ > 0.0f) {
|
|
538
|
+
paintScrollIndicators(target);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// The border: over the content, as GTK and AppKit draw it, and inside the
|
|
542
|
+
// view's own rounded box rather than around it -- React Native's border is
|
|
543
|
+
// part of the box, which is why Yoga has already inset the content by it.
|
|
544
|
+
// Below the DevTools overlay and the focus ring, which are not the app's.
|
|
545
|
+
if (hasBorders_) {
|
|
546
|
+
paintBorders(target);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// React DevTools' overlay, over everything including the children. Above
|
|
550
|
+
// the app on purpose: it is not part of it, and an inspected element half
|
|
551
|
+
// hidden behind a card would be pointing at the wrong thing.
|
|
552
|
+
if (!highlights_.empty()) {
|
|
553
|
+
paintHighlights(target);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// The focus ring, over everything including the children, because it is the
|
|
557
|
+
// answer to "where am I" and must not be hidden by what it is drawn on.
|
|
558
|
+
//
|
|
559
|
+
// Inside the view's own bounds rather than around them, which is where both
|
|
560
|
+
// other hosts draw it -- AppKit's is clipped to the view and GTK's matches
|
|
561
|
+
// that on purpose. Stroked down the middle of the line, so the rectangle is
|
|
562
|
+
// inset by half of it to keep the whole ring inside.
|
|
563
|
+
if (showsFocusRing_) {
|
|
564
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
565
|
+
const D2D1_COLOR_F colour = D2D1::ColorF(
|
|
566
|
+
kFocusRingRed, kFocusRingGreen, kFocusRingBlue, kFocusRingAlpha);
|
|
567
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
|
|
568
|
+
const float inset = kFocusRingWidth / 2.0f;
|
|
569
|
+
const D2D1_RECT_F ring = D2D1::RectF(inset,
|
|
570
|
+
inset,
|
|
571
|
+
(std::max)(inset, frame_.width - inset),
|
|
572
|
+
(std::max)(inset, frame_.height - inset));
|
|
573
|
+
if (hasCornerRadii_) {
|
|
574
|
+
// The view's own shape, each radius pulled in by the inset so the
|
|
575
|
+
// ring follows the corner rather than cutting across it.
|
|
576
|
+
float insetRadii[8];
|
|
577
|
+
for (int i = 0; i < 8; i++) {
|
|
578
|
+
insetRadii[i] = (std::max)(0.0f, cornerRadii_[i] - inset);
|
|
579
|
+
}
|
|
580
|
+
ComPtr<ID2D1Factory> factory;
|
|
581
|
+
target->GetFactory(factory.GetAddressOf());
|
|
582
|
+
const ComPtr<ID2D1Geometry> outline =
|
|
583
|
+
roundedBoxGeometry(factory.Get(), ring, insetRadii);
|
|
584
|
+
if (outline) {
|
|
585
|
+
target->DrawGeometry(outline.Get(), brush.Get(), kFocusRingWidth);
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
target->DrawRectangle(ring, brush.Get(), kFocusRingWidth);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
target->SetTransform(parentTransform);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// The border, as the ring between the view's rounded box and that box inset by
|
|
598
|
+
// each edge's width.
|
|
599
|
+
//
|
|
600
|
+
// The inner corners follow CSS: each outer radius less the width of the edge
|
|
601
|
+
// it meets, and never below zero -- which is why a thick border on a small
|
|
602
|
+
// radius has a square inside, on every platform.
|
|
603
|
+
//
|
|
604
|
+
// One colour all round is one fill. Different colours per edge are how CSS
|
|
605
|
+
// draws them: the ring is split along the diagonals from each outer corner to
|
|
606
|
+
// the matching inner one, and each side is filled in its own colour. Those
|
|
607
|
+
// sides are filled aliased inside a layer whose mask is the anti-aliased ring:
|
|
608
|
+
// adjacent anti-aliased fills leave a faint seam down each diagonal where their
|
|
609
|
+
// coverages meet, and the mask already gives the ring's own edges their
|
|
610
|
+
// smoothing.
|
|
611
|
+
void RnWin32View::paintBorders(ID2D1RenderTarget *target) const {
|
|
612
|
+
ComPtr<ID2D1Factory> factory;
|
|
613
|
+
target->GetFactory(factory.GetAddressOf());
|
|
614
|
+
if (!factory) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const float width = frame_.width;
|
|
619
|
+
const float height = frame_.height;
|
|
620
|
+
const float top = borderWidths_[0];
|
|
621
|
+
const float right = borderWidths_[1];
|
|
622
|
+
const float bottom = borderWidths_[2];
|
|
623
|
+
const float left = borderWidths_[3];
|
|
624
|
+
|
|
625
|
+
const auto floor0 = [](float value) { return (std::max)(0.0f, value); };
|
|
626
|
+
const D2D1_RECT_F outerRect = D2D1::RectF(0.0f, 0.0f, width, height);
|
|
627
|
+
const D2D1_RECT_F innerRect = D2D1::RectF(
|
|
628
|
+
left, top, (std::max)(left, width - right), (std::max)(top, height - bottom));
|
|
629
|
+
const float innerRadii[8] = {
|
|
630
|
+
floor0(cornerRadii_[0] - left),
|
|
631
|
+
floor0(cornerRadii_[1] - top),
|
|
632
|
+
floor0(cornerRadii_[2] - right),
|
|
633
|
+
floor0(cornerRadii_[3] - top),
|
|
634
|
+
floor0(cornerRadii_[4] - right),
|
|
635
|
+
floor0(cornerRadii_[5] - bottom),
|
|
636
|
+
floor0(cornerRadii_[6] - left),
|
|
637
|
+
floor0(cornerRadii_[7] - bottom),
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const ComPtr<ID2D1Geometry> outer = roundedBoxGeometry(factory.Get(), outerRect, cornerRadii_);
|
|
641
|
+
const ComPtr<ID2D1Geometry> inner = roundedBoxGeometry(factory.Get(), innerRect, innerRadii);
|
|
642
|
+
if (!outer || !inner) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
ComPtr<ID2D1PathGeometry> ring;
|
|
647
|
+
ComPtr<ID2D1GeometrySink> sink;
|
|
648
|
+
if (FAILED(factory->CreatePathGeometry(ring.GetAddressOf())) ||
|
|
649
|
+
FAILED(ring->Open(sink.GetAddressOf())) ||
|
|
650
|
+
FAILED(outer->CombineWithGeometry(
|
|
651
|
+
inner.Get(), D2D1_COMBINE_MODE_EXCLUDE, nullptr, sink.Get())) ||
|
|
652
|
+
FAILED(sink->Close())) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const auto colourOf = [this](int edge) {
|
|
657
|
+
const float *c = &borderColours_[edge * 4];
|
|
658
|
+
return D2D1::ColorF(c[0], c[1], c[2], c[3]);
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
bool oneColour = true;
|
|
662
|
+
for (int edge = 1; edge < 4; edge++) {
|
|
663
|
+
for (int c = 0; c < 4; c++) {
|
|
664
|
+
if (borderColours_[edge * 4 + c] != borderColours_[c]) {
|
|
665
|
+
oneColour = false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (oneColour) {
|
|
671
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
672
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(colourOf(0), brush.GetAddressOf()))) {
|
|
673
|
+
target->FillGeometry(ring.Get(), brush.Get());
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
ComPtr<ID2D1Layer> layer;
|
|
679
|
+
if (FAILED(target->CreateLayer(nullptr, layer.GetAddressOf()))) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
auto parameters = D2D1::LayerParameters();
|
|
683
|
+
parameters.contentBounds = D2D1::InfiniteRect();
|
|
684
|
+
parameters.geometricMask = ring.Get();
|
|
685
|
+
target->PushLayer(parameters, layer.Get());
|
|
686
|
+
|
|
687
|
+
const D2D1_ANTIALIAS_MODE previous = target->GetAntialiasMode();
|
|
688
|
+
target->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
|
|
689
|
+
|
|
690
|
+
// Each side, from its two outer corners to the matching inner ones. Top,
|
|
691
|
+
// right, bottom, left, as the widths and colours are kept.
|
|
692
|
+
const D2D1_POINT_2F sides[4][4] = {
|
|
693
|
+
{{0.0f, 0.0f}, {width, 0.0f}, {width - right, top}, {left, top}},
|
|
694
|
+
{{width, 0.0f}, {width, height}, {width - right, height - bottom}, {width - right, top}},
|
|
695
|
+
{{width, height}, {0.0f, height}, {left, height - bottom}, {width - right, height - bottom}},
|
|
696
|
+
{{0.0f, height}, {0.0f, 0.0f}, {left, top}, {left, height - bottom}},
|
|
697
|
+
};
|
|
698
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
699
|
+
if (borderWidths_[edge] <= 0.0f || borderColours_[edge * 4 + 3] <= 0.0f) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
ComPtr<ID2D1PathGeometry> side;
|
|
703
|
+
ComPtr<ID2D1GeometrySink> sideSink;
|
|
704
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
705
|
+
if (FAILED(factory->CreatePathGeometry(side.GetAddressOf())) ||
|
|
706
|
+
FAILED(side->Open(sideSink.GetAddressOf())) ||
|
|
707
|
+
FAILED(target->CreateSolidColorBrush(colourOf(edge), brush.GetAddressOf()))) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
sideSink->BeginFigure(sides[edge][0], D2D1_FIGURE_BEGIN_FILLED);
|
|
711
|
+
sideSink->AddLines(&sides[edge][1], 3);
|
|
712
|
+
sideSink->EndFigure(D2D1_FIGURE_END_CLOSED);
|
|
713
|
+
if (SUCCEEDED(sideSink->Close())) {
|
|
714
|
+
target->FillGeometry(side.Get(), brush.Get());
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
target->SetAntialiasMode(previous);
|
|
719
|
+
target->PopLayer();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Reads the current transform rather than being handed one, which is what keeps
|
|
723
|
+
// D2D1_MATRIX_3X2_F -- and windows.h behind it -- out of the header.
|
|
724
|
+
|
|
725
|
+
// ---------------------------------------------------------------------------
|
|
726
|
+
// Controls
|
|
727
|
+
// ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
namespace {
|
|
730
|
+
|
|
731
|
+
// Where a spinner is in its turn, 0..1, from a monotonic clock.
|
|
732
|
+
//
|
|
733
|
+
// A clock rather than per-view state: every spinner on screen turns together,
|
|
734
|
+
// which is what a real toolkit spinner does, and it means a view needs no
|
|
735
|
+
// animation bookkeeping of its own. One turn a second, which is roughly what
|
|
736
|
+
// GtkSpinner and NSProgressIndicator do.
|
|
737
|
+
//
|
|
738
|
+
// Here rather than in core/DesktopControls.h because this target deliberately
|
|
739
|
+
// does not link React Native, and that header does.
|
|
740
|
+
float spinnerTurn() {
|
|
741
|
+
using namespace std::chrono;
|
|
742
|
+
const double seconds = duration<double>(steady_clock::now().time_since_epoch()).count();
|
|
743
|
+
return static_cast<float>(seconds - std::floor(seconds));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
D2D1_COLOR_F colourOr(const float rgba[4], bool has, D2D1_COLOR_F fallback) {
|
|
747
|
+
return has ? D2D1::ColorF(rgba[0], rgba[1], rgba[2], rgba[3]) : fallback;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// The greys the rest of this host draws controls in. Windows' own accent colour
|
|
751
|
+
// would be more native and is a per-user setting read through UISettings, which
|
|
752
|
+
// is a WinRT dependency this package does not have; React Native's own default
|
|
753
|
+
// switch is not accent-coloured either.
|
|
754
|
+
constexpr float kTrackOffGrey = 0.78f;
|
|
755
|
+
constexpr float kTrackOnBlue[3] = {0.20f, 0.60f, 0.35f};
|
|
756
|
+
constexpr float kSpinnerGrey = 0.45f;
|
|
757
|
+
|
|
758
|
+
} // namespace
|
|
759
|
+
|
|
760
|
+
void RnWin32View::setControl(Control kind, const ControlStyle &style, std::string description) {
|
|
761
|
+
control_ = kind;
|
|
762
|
+
controlStyle_ = style;
|
|
763
|
+
controlDescription_ = std::move(description);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
bool RnWin32View::hasAnimatingSpinner() const {
|
|
767
|
+
if (control_ == Control::Spinner && controlStyle_.on) {
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
if (hidden_) {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
for (const RnWin32View *child : children_) {
|
|
774
|
+
if (child != nullptr && child->hasAnimatingSpinner()) {
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
void RnWin32View::setHighlights(std::vector<Highlight> highlights) {
|
|
782
|
+
highlights_ = std::move(highlights);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
void RnWin32View::paintHighlights(ID2D1RenderTarget *target) const {
|
|
786
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
787
|
+
if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0, 1.0f), brush.GetAddressOf()))) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
for (const Highlight &highlight : highlights_) {
|
|
791
|
+
const D2D1_RECT_F area = D2D1::RectF(highlight.x,
|
|
792
|
+
highlight.y,
|
|
793
|
+
highlight.x + highlight.width,
|
|
794
|
+
highlight.y + highlight.height);
|
|
795
|
+
if (highlight.filled) {
|
|
796
|
+
brush->SetColor(
|
|
797
|
+
D2D1::ColorF(highlight.color[0], highlight.color[1], highlight.color[2]));
|
|
798
|
+
brush->SetOpacity(highlight.color[3]);
|
|
799
|
+
target->FillRectangle(area, brush.Get());
|
|
800
|
+
}
|
|
801
|
+
// Opaque on the outline even when the fill is not, so the edge of an
|
|
802
|
+
// inspected element is a line rather than a suggestion. Stroked down the
|
|
803
|
+
// middle, so the rectangle is inset by half the width to keep it inside.
|
|
804
|
+
brush->SetColor(D2D1::ColorF(highlight.color[0], highlight.color[1], highlight.color[2]));
|
|
805
|
+
brush->SetOpacity(1.0f);
|
|
806
|
+
const float inset = basalt::kHighlightBorderWidth / 2.0f;
|
|
807
|
+
target->DrawRectangle(
|
|
808
|
+
D2D1::RectF(area.left + inset, area.top + inset, area.right - inset, area.bottom - inset),
|
|
809
|
+
brush.Get(),
|
|
810
|
+
basalt::kHighlightBorderWidth);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
void RnWin32View::paintControl(ID2D1RenderTarget *target) const {
|
|
815
|
+
if (control_ == Control::Spinner) {
|
|
816
|
+
// A stopped indicator draws nothing at all, which is what
|
|
817
|
+
// `hidesWhenStopped` means and what a <RefreshControl> that is not
|
|
818
|
+
// refreshing does.
|
|
819
|
+
if (!controlStyle_.on && controlStyle_.hidesWhenStopped) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Eight dots around a circle, the leading one opaque and the rest fading
|
|
824
|
+
// behind it. The same shape GtkSpinner and NSProgressIndicator draw, which
|
|
825
|
+
// matters because these three hosts are screenshotted side by side.
|
|
826
|
+
const float diameter = std::min(
|
|
827
|
+
{controlStyle_.large ? basalt::kSpinnerLarge : basalt::kSpinnerSmall,
|
|
828
|
+
frame_.width,
|
|
829
|
+
frame_.height});
|
|
830
|
+
if (diameter <= 0.0f) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const float centreX = frame_.width / 2.0f;
|
|
834
|
+
const float centreY = frame_.height / 2.0f;
|
|
835
|
+
const float radius = diameter / 2.0f;
|
|
836
|
+
const float dotRadius = std::max(radius / 6.0f, 1.0f);
|
|
837
|
+
|
|
838
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
839
|
+
const D2D1_COLOR_F base = colourOr(controlStyle_.thumb,
|
|
840
|
+
controlStyle_.hasThumb,
|
|
841
|
+
D2D1::ColorF(kSpinnerGrey, kSpinnerGrey, kSpinnerGrey, 1.0f));
|
|
842
|
+
if (FAILED(target->CreateSolidColorBrush(base, brush.GetAddressOf()))) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
constexpr int kDots = 8;
|
|
847
|
+
// A stopped indicator that is still shown is drawn still, at phase zero,
|
|
848
|
+
// rather than at whatever the clock happens to say -- so a screenshot of a
|
|
849
|
+
// stopped spinner is the same picture every time.
|
|
850
|
+
const float turn = controlStyle_.on ? spinnerTurn() : 0.0f;
|
|
851
|
+
const int leading = static_cast<int>(turn * kDots) % kDots;
|
|
852
|
+
for (int i = 0; i < kDots; i++) {
|
|
853
|
+
const float angle = 6.2831853f * static_cast<float>(i) / static_cast<float>(kDots);
|
|
854
|
+
const int behind = (leading - i + kDots) % kDots;
|
|
855
|
+
brush->SetOpacity(1.0f - static_cast<float>(behind) / static_cast<float>(kDots));
|
|
856
|
+
const D2D1_ELLIPSE dot = D2D1::Ellipse(
|
|
857
|
+
D2D1::Point2F(centreX + std::cos(angle) * (radius - dotRadius),
|
|
858
|
+
centreY + std::sin(angle) * (radius - dotRadius)),
|
|
859
|
+
dotRadius,
|
|
860
|
+
dotRadius);
|
|
861
|
+
target->FillEllipse(dot, brush.Get());
|
|
862
|
+
}
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// A switch: a rounded track with a circular thumb at one end. Sized from
|
|
867
|
+
// core/ControlMetrics.h -- the same numbers the shadow node measures to --
|
|
868
|
+
// and centred, so a <Switch> given a bigger box keeps its shape rather than
|
|
869
|
+
// stretching.
|
|
870
|
+
const float width = std::min(basalt::kSwitchWidth, frame_.width);
|
|
871
|
+
const float height = std::min(basalt::kSwitchHeight, frame_.height);
|
|
872
|
+
if (width <= 0.0f || height <= 0.0f) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const float left = (frame_.width - width) / 2.0f;
|
|
876
|
+
const float top = (frame_.height - height) / 2.0f;
|
|
877
|
+
|
|
878
|
+
const D2D1_COLOR_F trackColour = controlStyle_.on
|
|
879
|
+
? colourOr(controlStyle_.trackOn,
|
|
880
|
+
controlStyle_.hasTrackOn,
|
|
881
|
+
D2D1::ColorF(kTrackOnBlue[0], kTrackOnBlue[1], kTrackOnBlue[2], 1.0f))
|
|
882
|
+
: colourOr(controlStyle_.trackOff,
|
|
883
|
+
controlStyle_.hasTrackOff,
|
|
884
|
+
D2D1::ColorF(kTrackOffGrey, kTrackOffGrey, kTrackOffGrey, 1.0f));
|
|
885
|
+
|
|
886
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
887
|
+
if (FAILED(target->CreateSolidColorBrush(trackColour, brush.GetAddressOf()))) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
// Half opacity for a disabled switch, which is what every platform does and
|
|
891
|
+
// is the only thing `disabled` can mean to something that draws itself.
|
|
892
|
+
brush->SetOpacity(controlStyle_.disabled ? 0.4f : 1.0f);
|
|
893
|
+
const float radius = height / 2.0f;
|
|
894
|
+
target->FillRoundedRectangle(
|
|
895
|
+
D2D1::RoundedRect(D2D1::RectF(left, top, left + width, top + height), radius, radius),
|
|
896
|
+
brush.Get());
|
|
897
|
+
|
|
898
|
+
const float inset = 2.0f;
|
|
899
|
+
const float thumbRadius = radius - inset;
|
|
900
|
+
const float thumbX = controlStyle_.on ? left + width - radius : left + radius;
|
|
901
|
+
ComPtr<ID2D1SolidColorBrush> thumbBrush;
|
|
902
|
+
const D2D1_COLOR_F thumbColour =
|
|
903
|
+
colourOr(controlStyle_.thumb, controlStyle_.hasThumb, D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f));
|
|
904
|
+
if (FAILED(target->CreateSolidColorBrush(thumbColour, thumbBrush.GetAddressOf()))) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
thumbBrush->SetOpacity(controlStyle_.disabled ? 0.4f : 1.0f);
|
|
908
|
+
target->FillEllipse(D2D1::Ellipse(D2D1::Point2F(thumbX, top + radius), thumbRadius, thumbRadius),
|
|
909
|
+
thumbBrush.Get());
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Always drawn, rather than faded in while scrolling and out after: there is no
|
|
913
|
+
// timer here and no animation, which is why `flashScrollIndicators` stays a
|
|
914
|
+
// no-op -- there is nothing to flash something already on screen.
|
|
915
|
+
void RnWin32View::paintScrollIndicators(ID2D1RenderTarget *target) const {
|
|
916
|
+
const float thickness = static_cast<float>(basalt::kScrollIndicatorThickness);
|
|
917
|
+
const float inset = static_cast<float>(basalt::kScrollIndicatorInset);
|
|
918
|
+
|
|
919
|
+
ComPtr<ID2D1SolidColorBrush> brush;
|
|
920
|
+
// Neutral and translucent, so it reads over light and dark content alike --
|
|
921
|
+
// the same colour the GTK and AppKit hosts use.
|
|
922
|
+
if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.35f),
|
|
923
|
+
brush.GetAddressOf()))) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const float radius = thickness / 2.0f;
|
|
928
|
+
if (indicatorVerticalLength_ > 0.0f) {
|
|
929
|
+
const float left = frame_.width - thickness - inset;
|
|
930
|
+
target->FillRoundedRectangle(
|
|
931
|
+
D2D1::RoundedRect(D2D1::RectF(left,
|
|
932
|
+
indicatorVerticalOffset_,
|
|
933
|
+
left + thickness,
|
|
934
|
+
indicatorVerticalOffset_ + indicatorVerticalLength_),
|
|
935
|
+
radius,
|
|
936
|
+
radius),
|
|
937
|
+
brush.Get());
|
|
938
|
+
}
|
|
939
|
+
if (indicatorHorizontalLength_ > 0.0f) {
|
|
940
|
+
const float top = frame_.height - thickness - inset;
|
|
941
|
+
target->FillRoundedRectangle(
|
|
942
|
+
D2D1::RoundedRect(D2D1::RectF(indicatorHorizontalOffset_,
|
|
943
|
+
top,
|
|
944
|
+
indicatorHorizontalOffset_ + indicatorHorizontalLength_,
|
|
945
|
+
top + thickness),
|
|
946
|
+
radius,
|
|
947
|
+
radius),
|
|
948
|
+
brush.Get());
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
void RnWin32View::paintChildren(ID2D1RenderTarget *target) const {
|
|
953
|
+
if (children_.empty()) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
D2D1::Matrix3x2F worldTransform;
|
|
958
|
+
target->GetTransform(&worldTransform);
|
|
959
|
+
|
|
960
|
+
// `overflow: hidden`, and only that: the background above is clipped whether
|
|
961
|
+
// or not this is set.
|
|
962
|
+
const D2D1_RECT_F bounds = D2D1::RectF(0.0f, 0.0f, frame_.width, frame_.height);
|
|
963
|
+
ScopedGeometryClip clip(clipsChildren_ ? target : nullptr, bounds, cornerRadii_);
|
|
964
|
+
|
|
965
|
+
// A ScrollView's offset moves its children and nothing else, so it belongs
|
|
966
|
+
// between this view's transform and theirs.
|
|
967
|
+
if (scrollX_ != 0.0f || scrollY_ != 0.0f) {
|
|
968
|
+
target->SetTransform(D2D1::Matrix3x2F::Translation(-scrollX_, -scrollY_) * worldTransform);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Forwards, so the last child painted is on top. Hit testing walks the same
|
|
972
|
+
// list backwards.
|
|
973
|
+
for (const RnWin32View *child : childrenInPaintOrder()) {
|
|
974
|
+
child->paint(target);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// --- Hit testing ------------------------------------------------------------
|
|
979
|
+
|
|
980
|
+
namespace {
|
|
981
|
+
|
|
982
|
+
// The spelling React Native uses for the prop, which is also CSS's, so the
|
|
983
|
+
// three hosts' dumps say the same words.
|
|
984
|
+
const char *pointerEventsName(RnWin32View::PointerEvents mode) {
|
|
985
|
+
switch (mode) {
|
|
986
|
+
case RnWin32View::PointerEvents::None:
|
|
987
|
+
return "none";
|
|
988
|
+
case RnWin32View::PointerEvents::BoxNone:
|
|
989
|
+
return "box-none";
|
|
990
|
+
case RnWin32View::PointerEvents::BoxOnly:
|
|
991
|
+
return "box-only";
|
|
992
|
+
case RnWin32View::PointerEvents::Auto:
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
return "auto";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
} // namespace
|
|
999
|
+
|
|
1000
|
+
RnWin32View *hitTest(RnWin32View *root, float x, float y) {
|
|
1001
|
+
// `pointerEvents: none` takes the view and everything inside it out of hit
|
|
1002
|
+
// testing entirely, so the caller's loop carries on to whatever is behind.
|
|
1003
|
+
if (root == nullptr || root->hidden() ||
|
|
1004
|
+
root->pointerEvents() == RnWin32View::PointerEvents::None) {
|
|
1005
|
+
return nullptr;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const RnRect &frame = root->frame();
|
|
1009
|
+
if (x < 0.0f || y < 0.0f || x >= frame.width || y >= frame.height) {
|
|
1010
|
+
return nullptr;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Children are placed in this view's content space, which a scroll offset
|
|
1014
|
+
// shifts. Adding it back here is what makes hit testing follow a scroll with
|
|
1015
|
+
// nothing in this function knowing what a ScrollView is -- the same offset
|
|
1016
|
+
// `paintChildren` subtracts, from the same two fields.
|
|
1017
|
+
const float contentX = x + root->scrollX();
|
|
1018
|
+
const float contentY = y + root->scrollY();
|
|
1019
|
+
|
|
1020
|
+
// Backwards: the last child painted is the topmost, and the topmost is what a
|
|
1021
|
+
// press should land on.
|
|
1022
|
+
//
|
|
1023
|
+
// `box-only` is the one mode that skips this: the box is the target and
|
|
1024
|
+
// nothing inside it is, which is what makes an overlay swallow a press meant
|
|
1025
|
+
// for a button drawn on top of it.
|
|
1026
|
+
if (root->pointerEvents() != RnWin32View::PointerEvents::BoxOnly) {
|
|
1027
|
+
const std::vector<RnWin32View *> ordered = root->childrenInPaintOrder();
|
|
1028
|
+
for (auto it = ordered.rbegin(); it != ordered.rend(); ++it) {
|
|
1029
|
+
RnWin32View *child = *it;
|
|
1030
|
+
float local[6];
|
|
1031
|
+
child->localToParent(local);
|
|
1032
|
+
|
|
1033
|
+
float childX = 0.0f;
|
|
1034
|
+
float childY = 0.0f;
|
|
1035
|
+
if (!invertPoint(local, contentX, contentY, childX, childY)) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (RnWin32View *hit = hitTest(child, childX, childY)) {
|
|
1039
|
+
return hit;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// `box-none` is transparent to a press that misses everything inside it.
|
|
1045
|
+
// Returning null rather than the parent is the whole of it: the caller is
|
|
1046
|
+
// partway through its own list of children, so the press carries on to the
|
|
1047
|
+
// sibling *behind* this view -- which is what the absolutely-positioned
|
|
1048
|
+
// overlay this mode exists for is asking for.
|
|
1049
|
+
if (root->pointerEvents() == RnWin32View::PointerEvents::BoxNone) {
|
|
1050
|
+
return nullptr;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// A point inside this view but over none of its children is this view. React
|
|
1054
|
+
// Native's responder system needs a target for every press inside the
|
|
1055
|
+
// surface, and the root is the honest answer for one that missed everything.
|
|
1056
|
+
return root;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// --- Reporting -------------------------------------------------------------
|
|
1060
|
+
|
|
1061
|
+
std::string RnWin32View::describeTree() const {
|
|
1062
|
+
std::string out;
|
|
1063
|
+
describeInto(out, 0);
|
|
1064
|
+
return out;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
void RnWin32View::describeInto(std::string &out, int depth) const {
|
|
1068
|
+
for (int i = 0; i < depth; i++) {
|
|
1069
|
+
out += " ";
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
appendFormat(out,
|
|
1073
|
+
"view tag=%d frame=(%g,%g %gx%g)",
|
|
1074
|
+
tag_,
|
|
1075
|
+
static_cast<double>(frame_.x),
|
|
1076
|
+
static_cast<double>(frame_.y),
|
|
1077
|
+
static_cast<double>(frame_.width),
|
|
1078
|
+
static_cast<double>(frame_.height));
|
|
1079
|
+
|
|
1080
|
+
if (hasBackgroundColor_) {
|
|
1081
|
+
appendFormat(out,
|
|
1082
|
+
" bg=#%02x%02x%02x%02x",
|
|
1083
|
+
toByte(backgroundColor_[0]),
|
|
1084
|
+
toByte(backgroundColor_[1]),
|
|
1085
|
+
toByte(backgroundColor_[2]),
|
|
1086
|
+
toByte(backgroundColor_[3]));
|
|
1087
|
+
}
|
|
1088
|
+
// Field order matches the GTK and AppKit sides exactly -- bg, opacity, clip,
|
|
1089
|
+
// transform, scroll, then the content fields -- because
|
|
1090
|
+
// scripts/compare_hosts.sh diffs the dumps line by line and a reordering
|
|
1091
|
+
// would read as every line differing.
|
|
1092
|
+
if (opacity_ < 1.0f) {
|
|
1093
|
+
appendFormat(out, " opacity=%g", static_cast<double>(opacity_));
|
|
1094
|
+
}
|
|
1095
|
+
if (clipsChildren_) {
|
|
1096
|
+
out += " clip";
|
|
1097
|
+
}
|
|
1098
|
+
// Whether anything is drawn at all. Without this a view hidden by
|
|
1099
|
+
// `display: none` or by a back face turned away reads exactly like a visible
|
|
1100
|
+
// one, and the only prop in this dump that removes a view entirely was the
|
|
1101
|
+
// only one it could not show.
|
|
1102
|
+
if (hidden_) {
|
|
1103
|
+
out += " hidden";
|
|
1104
|
+
}
|
|
1105
|
+
// Per-corner radii and per-edge borders, in the fields, the order and the
|
|
1106
|
+
// formatting GTK prints them -- RnView.cpp's describe -- so that
|
|
1107
|
+
// scripts/compare_hosts.sh compares what each host actually draws.
|
|
1108
|
+
//
|
|
1109
|
+
// Both were missing here until this host drew them: it printed one circular
|
|
1110
|
+
// radius eight times and no border at all, and printing nothing was what let
|
|
1111
|
+
// the comparison say so rather than hide it. Radii are eight numbers because
|
|
1112
|
+
// React Native's are elliptical: top-left, top-right, bottom-right,
|
|
1113
|
+
// bottom-left, horizontal then vertical.
|
|
1114
|
+
if (hasCornerRadii_) {
|
|
1115
|
+
appendFormat(out,
|
|
1116
|
+
" radii=(%g,%g,%g,%g,%g,%g,%g,%g)",
|
|
1117
|
+
static_cast<double>(cornerRadii_[0]),
|
|
1118
|
+
static_cast<double>(cornerRadii_[1]),
|
|
1119
|
+
static_cast<double>(cornerRadii_[2]),
|
|
1120
|
+
static_cast<double>(cornerRadii_[3]),
|
|
1121
|
+
static_cast<double>(cornerRadii_[4]),
|
|
1122
|
+
static_cast<double>(cornerRadii_[5]),
|
|
1123
|
+
static_cast<double>(cornerRadii_[6]),
|
|
1124
|
+
static_cast<double>(cornerRadii_[7]));
|
|
1125
|
+
}
|
|
1126
|
+
// Top, right, bottom, left, for both.
|
|
1127
|
+
if (hasBorders_) {
|
|
1128
|
+
appendFormat(out,
|
|
1129
|
+
" borderw=(%g,%g,%g,%g)",
|
|
1130
|
+
static_cast<double>(borderWidths_[0]),
|
|
1131
|
+
static_cast<double>(borderWidths_[1]),
|
|
1132
|
+
static_cast<double>(borderWidths_[2]),
|
|
1133
|
+
static_cast<double>(borderWidths_[3]));
|
|
1134
|
+
out += " borderc=(";
|
|
1135
|
+
for (int edge = 0; edge < 4; edge++) {
|
|
1136
|
+
const float *c = &borderColours_[edge * 4];
|
|
1137
|
+
appendFormat(out,
|
|
1138
|
+
"%s#%02x%02x%02x%02x",
|
|
1139
|
+
edge == 0 ? "" : ",",
|
|
1140
|
+
toByte(c[0]),
|
|
1141
|
+
toByte(c[1]),
|
|
1142
|
+
toByte(c[2]),
|
|
1143
|
+
toByte(c[3]));
|
|
1144
|
+
}
|
|
1145
|
+
out += ")";
|
|
1146
|
+
}
|
|
1147
|
+
if (hasTransform_) {
|
|
1148
|
+
// The 2D affine part, in the order CSS writes a matrix(): a, b, c, d, tx,
|
|
1149
|
+
// ty. The other two platforms print the same six from their own matrix
|
|
1150
|
+
// types, so a transform is comparable across all three -- without which a
|
|
1151
|
+
// view rotated on one desktop and not on another looks identical here.
|
|
1152
|
+
appendFormat(out,
|
|
1153
|
+
" transform=(%g,%g,%g,%g,%g,%g)",
|
|
1154
|
+
static_cast<double>(transform_[0]),
|
|
1155
|
+
static_cast<double>(transform_[1]),
|
|
1156
|
+
static_cast<double>(transform_[2]),
|
|
1157
|
+
static_cast<double>(transform_[3]),
|
|
1158
|
+
static_cast<double>(transform_[4]),
|
|
1159
|
+
static_cast<double>(transform_[5]));
|
|
1160
|
+
}
|
|
1161
|
+
if (scrollX_ != 0.0f || scrollY_ != 0.0f) {
|
|
1162
|
+
appendFormat(out,
|
|
1163
|
+
" scroll=(%g,%g)",
|
|
1164
|
+
static_cast<double>(scrollX_),
|
|
1165
|
+
static_cast<double>(scrollY_));
|
|
1166
|
+
}
|
|
1167
|
+
// The overlay scrollbars, which are otherwise pure paint and so invisible to
|
|
1168
|
+
// every test this project has. Printed only when there is one, so a view that
|
|
1169
|
+
// does not scroll stays as short as it was.
|
|
1170
|
+
if (indicatorVerticalLength_ > 0.0f) {
|
|
1171
|
+
appendFormat(out,
|
|
1172
|
+
" scrollbar-v=(%g,%g)",
|
|
1173
|
+
static_cast<double>(indicatorVerticalOffset_),
|
|
1174
|
+
static_cast<double>(indicatorVerticalLength_));
|
|
1175
|
+
}
|
|
1176
|
+
if (indicatorHorizontalLength_ > 0.0f) {
|
|
1177
|
+
appendFormat(out,
|
|
1178
|
+
" scrollbar-h=(%g,%g)",
|
|
1179
|
+
static_cast<double>(indicatorHorizontalOffset_),
|
|
1180
|
+
static_cast<double>(indicatorHorizontalLength_));
|
|
1181
|
+
}
|
|
1182
|
+
// Printed only when it is not the default, like every other field here.
|
|
1183
|
+
// Worth printing at all because it is invisible: a view with
|
|
1184
|
+
// `pointerEvents: none` is drawn exactly like one without, and the only way
|
|
1185
|
+
// the cross-host diff can say the prop arrived on all three is if each one
|
|
1186
|
+
// reports it.
|
|
1187
|
+
if (pointerEvents_ != PointerEvents::Auto) {
|
|
1188
|
+
appendFormat(out, " pe=%s", pointerEventsName(pointerEvents_));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (image_ != nullptr) {
|
|
1192
|
+
// The same `texture=WxH fit=<name>` the other two hosts emit. The fit is
|
|
1193
|
+
// here because it is the only thing about a drawn image that a frame cannot
|
|
1194
|
+
// show: two views the same size holding the same picture are identical in
|
|
1195
|
+
// every other field of this dump and different on screen.
|
|
1196
|
+
appendFormat(out, " texture=%ux%u", image_->width(), image_->height());
|
|
1197
|
+
appendFormat(out, " fit=%s", imageFitName(imageFit_));
|
|
1198
|
+
// Printed for the same reason the fit is, and in the same format the other
|
|
1199
|
+
// two hosts use, so the cross-host diff can compare them.
|
|
1200
|
+
if (hasImageTint_) {
|
|
1201
|
+
appendFormat(out,
|
|
1202
|
+
" tint=#%02x%02x%02x%02x",
|
|
1203
|
+
static_cast<unsigned>(imageTint_[0] * 255.0f + 0.5f),
|
|
1204
|
+
static_cast<unsigned>(imageTint_[1] * 255.0f + 0.5f),
|
|
1205
|
+
static_cast<unsigned>(imageTint_[2] * 255.0f + 0.5f),
|
|
1206
|
+
static_cast<unsigned>(imageTint_[3] * 255.0f + 0.5f));
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (textLayout_ != nullptr) {
|
|
1211
|
+
const std::string &text = textLayout_->text();
|
|
1212
|
+
if (!text.empty()) {
|
|
1213
|
+
out += " text=\"";
|
|
1214
|
+
appendEscaped(out, text);
|
|
1215
|
+
out += "\"";
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// A text field's content lives in its EDIT peer, not in a layout, so it would
|
|
1220
|
+
// otherwise be invisible to every test that reads this tree -- and the dump
|
|
1221
|
+
// would differ from the other two hosts' for a field that was working.
|
|
1222
|
+
if (editablePeer_ != nullptr) {
|
|
1223
|
+
const int length = GetWindowTextLength(editablePeer_);
|
|
1224
|
+
std::wstring wide(static_cast<size_t>(length) + 1, L'\0');
|
|
1225
|
+
GetWindowText(editablePeer_, wide.data(), length + 1);
|
|
1226
|
+
wide.resize(static_cast<size_t>(length));
|
|
1227
|
+
|
|
1228
|
+
out += " editable=\"";
|
|
1229
|
+
appendEscaped(out, narrow(wide));
|
|
1230
|
+
out += "\"";
|
|
1231
|
+
|
|
1232
|
+
// GetFocus is per-thread rather than global, which is the right question
|
|
1233
|
+
// here: this runs on the thread that owns the window, and what is being
|
|
1234
|
+
// asked is whether the field has the keyboard within it.
|
|
1235
|
+
if (GetFocus() == editablePeer_) {
|
|
1236
|
+
out += " focused";
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// How many DevTools highlights this view is drawing. In the dump because they
|
|
1241
|
+
// are otherwise invisible to everything but a screenshot, and because a
|
|
1242
|
+
// command that arrived and drew nothing is exactly the failure worth
|
|
1243
|
+
// catching.
|
|
1244
|
+
if (!highlights_.empty()) {
|
|
1245
|
+
appendFormat(out, " highlights=%d", static_cast<int>(highlights_.size()));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// What kind of control this view is, and what state it is in. Written by
|
|
1249
|
+
// core/DesktopControls.h rather than formatted here, for the same reason the
|
|
1250
|
+
// role name below is React Native's vocabulary and not UIA's: three hosts
|
|
1251
|
+
// describing the same switch in three ways is a diff on every line.
|
|
1252
|
+
if (!controlDescription_.empty()) {
|
|
1253
|
+
appendFormat(out, " control=%s", controlDescription_.c_str());
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// React Native's role name, not UIA's. This dump is compared line by line
|
|
1257
|
+
// across three platforms, and each reporting its own toolkit's vocabulary
|
|
1258
|
+
// would make every accessible view look like a difference. That the *UIA*
|
|
1259
|
+
// control type was really applied is asserted in
|
|
1260
|
+
// tests/test_win32_accessibility.cpp, which is where a platform question
|
|
1261
|
+
// belongs.
|
|
1262
|
+
if (!accessible_.role.empty()) {
|
|
1263
|
+
appendFormat(out, " role=%s", accessible_.role.c_str());
|
|
1264
|
+
}
|
|
1265
|
+
// Whether Tab stops here, after `role=` because that is where the other two
|
|
1266
|
+
// hosts print it and this dump is diffed line by line. Whether it is focused
|
|
1267
|
+
// *now* is deliberately not printed: that depends on what the window manager
|
|
1268
|
+
// did when the window opened, which is not a property of the platform and
|
|
1269
|
+
// would make this dump differ between two machines running the same app.
|
|
1270
|
+
if (focusable_) {
|
|
1271
|
+
out += " focusable";
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
out += "\n";
|
|
1275
|
+
|
|
1276
|
+
for (const RnWin32View *child : children_) {
|
|
1277
|
+
child->describeInto(out, depth + 1);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
} // namespace basalt::win32
|