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,727 @@
|
|
|
1
|
+
#include "Win32MountingManager.h"
|
|
2
|
+
|
|
3
|
+
#include "DirectWriteLayout.h"
|
|
4
|
+
#include "ImageBytes.h"
|
|
5
|
+
#include "PlatformServices.h"
|
|
6
|
+
#include "UIManagerAccess.h"
|
|
7
|
+
|
|
8
|
+
#ifdef BASALT_HAS_SKIA
|
|
9
|
+
#include "Win32SkiaPeer.h"
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
#include <react/renderer/components/image/ImageProps.h>
|
|
13
|
+
#include <react/renderer/components/text/ParagraphProps.h>
|
|
14
|
+
#include <react/renderer/components/text/ParagraphState.h>
|
|
15
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
16
|
+
#include <react/renderer/core/ConcreteState.h>
|
|
17
|
+
#include <react/renderer/graphics/Color.h>
|
|
18
|
+
#include <react/renderer/uimanager/UIManager.h>
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
#include <memory>
|
|
22
|
+
#include <string_view>
|
|
23
|
+
#include <utility>
|
|
24
|
+
|
|
25
|
+
namespace basalt {
|
|
26
|
+
|
|
27
|
+
using facebook::react::ImageProps;
|
|
28
|
+
using facebook::react::ImageResizeMode;
|
|
29
|
+
using facebook::react::MountingTransaction;
|
|
30
|
+
using facebook::react::ParagraphState;
|
|
31
|
+
using facebook::react::ShadowView;
|
|
32
|
+
using facebook::react::SurfaceId;
|
|
33
|
+
using facebook::react::Tag;
|
|
34
|
+
using facebook::react::ViewProps;
|
|
35
|
+
using win32::RnImageFit;
|
|
36
|
+
using win32::RnWin32View;
|
|
37
|
+
|
|
38
|
+
Win32MountingManager::Win32MountingManager()
|
|
39
|
+
: scrollViews_([this](Tag tag) { return eventEmitterForTag(tag); }),
|
|
40
|
+
textInputs_([this](Tag tag) { return eventEmitterForTag(tag); }) {
|
|
41
|
+
// The pull past the top of a list, counted in core/PullToRefresh.h and fired
|
|
42
|
+
// at whichever <RefreshControl> that scroll view has. Identical on all three
|
|
43
|
+
// desktops, which is the point of the two of them being portable.
|
|
44
|
+
scrollViews_.setOverscrollTopHandler([this](Tag tag, double amount) {
|
|
45
|
+
if (amount <= 0.0) {
|
|
46
|
+
pullToRefresh().release(tag);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (pullToRefresh().pull(tag, amount)) {
|
|
50
|
+
fireRefresh(tag);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Win32MountingManager::~Win32MountingManager() noexcept {
|
|
56
|
+
// MountingWalk cannot do this from its own destructor: by the time a base
|
|
57
|
+
// destructor runs, the platform half that knows how to free a view is already
|
|
58
|
+
// gone. Its header says so, and every platform has to make this call.
|
|
59
|
+
releaseAllViews();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// IMountingManager
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
void Win32MountingManager::executeMount(SurfaceId surfaceId, MountingTransaction &&transaction) {
|
|
67
|
+
// This runs on the JS thread: Scheduler::uiManagerDidFinishTransaction queues
|
|
68
|
+
// the mount via RuntimeScheduler::scheduleRenderingUpdate, which drains in the
|
|
69
|
+
// event loop's "update the rendering" step with the jsi::Runtime live. A view
|
|
70
|
+
// here is not thread-affine the way a GtkWidget or an NSView is -- it is a
|
|
71
|
+
// plain C++ object -- but the *painting* is, and so is every HWND a later
|
|
72
|
+
// <TextInput> will own. So nothing here may touch a view either.
|
|
73
|
+
//
|
|
74
|
+
// Always post, never run inline even when already on the UI thread. The queue
|
|
75
|
+
// is FIFO, and running one transaction inline while another is already
|
|
76
|
+
// queued would reorder them -- so this is about ordering rather than about
|
|
77
|
+
// thread-safety, and it is why the condition is absent rather than merely
|
|
78
|
+
// unnecessary.
|
|
79
|
+
//
|
|
80
|
+
// That is this project's choice rather than everyone's, and worth being exact
|
|
81
|
+
// about: iOS takes the other side. RCTMountingManager::scheduleTransaction
|
|
82
|
+
// runs `initiateTransaction` inline when `RCTIsMainQueue()`, and only
|
|
83
|
+
// dispatches otherwise. It gets away with it because under Fabric the JS
|
|
84
|
+
// thread is not the main thread, so the fast path is reached only when
|
|
85
|
+
// everything is already on one thread and there is nothing to reorder against.
|
|
86
|
+
// GTK and AppKit both queue unconditionally here, and this matches them.
|
|
87
|
+
//
|
|
88
|
+
// None of this changed with the New Architecture. What JSI removed is the
|
|
89
|
+
// *bridge* -- the JSON serialisation and the asynchronous message queue
|
|
90
|
+
// between JavaScript and native, which is why a TurboModule call is now a
|
|
91
|
+
// direct C++ call and why `measure` can answer synchronously. Mounting was
|
|
92
|
+
// never on that path: Fabric computes layout off the UI thread, commits to
|
|
93
|
+
// the shadow tree, and the mutations still have to be applied where the
|
|
94
|
+
// platform permits. Thread affinity is USER32's rule, not the bridge's.
|
|
95
|
+
//
|
|
96
|
+
// MountingTransaction is move-only, so it travels in a shared_ptr the lambda
|
|
97
|
+
// can capture -- std::function requires its target to be copyable.
|
|
98
|
+
auto pending = std::make_shared<MountingTransaction>(std::move(transaction));
|
|
99
|
+
postToUiThread([this, surfaceId, pending] {
|
|
100
|
+
applyTransaction(surfaceId, std::move(*pending));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
void Win32MountingManager::applyTransaction(SurfaceId surfaceId,
|
|
105
|
+
MountingTransaction &&transaction) {
|
|
106
|
+
// The walk itself is in core/MountingWalk.h and is shared with the other two
|
|
107
|
+
// desktops; what is Windows about mounting is below, in the operations it
|
|
108
|
+
// calls back into.
|
|
109
|
+
applyMutations(transaction.getMutations());
|
|
110
|
+
|
|
111
|
+
// Tell the UIManager the transaction is on screen. Anything registered as a
|
|
112
|
+
// mount hook -- Reanimated's is the one that matters -- is waiting for this,
|
|
113
|
+
// and without it an animated style is computed every frame, committed to the
|
|
114
|
+
// shadow tree, and never resumed, so nothing moves. ReactCxxPlatform calls it
|
|
115
|
+
// nowhere.
|
|
116
|
+
if (auto uiManager = sharedUIManager()) {
|
|
117
|
+
uiManager->reportMount(surfaceId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// And ask the host to repaint, because on Windows nothing does that on its
|
|
121
|
+
// own. See setOnDidMount.
|
|
122
|
+
if (onDidMount_) {
|
|
123
|
+
onDidMount_();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void Win32MountingManager::setOnDidMount(std::function<void()> onDidMount) {
|
|
128
|
+
onDidMount_ = std::move(onDidMount);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
void Win32MountingManager::dispatchCommand(const ShadowView &shadowView,
|
|
132
|
+
const std::string &commandName,
|
|
133
|
+
const folly::dynamic &args) {
|
|
134
|
+
// Same trip, for the same reason, and posted onto the same queue so it stays
|
|
135
|
+
// behind the transaction that created the view it names. A scrollTo that
|
|
136
|
+
// arrived before its ScrollView was mounted would find no entry and be
|
|
137
|
+
// dropped.
|
|
138
|
+
const Tag tag = shadowView.tag;
|
|
139
|
+
postToUiThread([this, tag, commandName, args] { applyCommand(tag, commandName, args); });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void Win32MountingManager::applyCommand(Tag tag,
|
|
143
|
+
const std::string &commandName,
|
|
144
|
+
const folly::dynamic &args) {
|
|
145
|
+
if (scrollViews_.dispatchCommand(tag, commandName, args)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (textInputs_.dispatchCommand(tag, commandName, args)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// React DevTools' overlay, answered the same way on all three: see
|
|
152
|
+
// core/DebuggingOverlay.h.
|
|
153
|
+
if (applyOverlayCommand(tag, commandName, args)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Dropped silently rather than logged. Every command the other two desktops
|
|
157
|
+
// implement is now implemented here, so what reaches this line is a command
|
|
158
|
+
// for a component this platform does not claim -- which is expected rather
|
|
159
|
+
// than exceptional, and would otherwise be logged on every frame of a list.
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
facebook::react::ComponentRegistryFactory Win32MountingManager::getComponentRegistryFactory() {
|
|
163
|
+
return facebook::react::getDefaultComponentRegistryFactory();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
bool Win32MountingManager::hasComponent(const std::string &name) {
|
|
167
|
+
// Only what actually mounts. Claiming more would be worse than admitting the
|
|
168
|
+
// gap: the registry would build shadow nodes nothing can put on screen, and
|
|
169
|
+
// the app would render blank rectangles instead of failing somewhere legible.
|
|
170
|
+
//
|
|
171
|
+
// Paragraph is the mountable half of <Text>; Text and RawText exist only in
|
|
172
|
+
// the shadow tree, folded into the Paragraph's AttributedString.
|
|
173
|
+
//
|
|
174
|
+
// This list and ComponentRegistryWin32.cpp are two statements of one fact and
|
|
175
|
+
// must agree.
|
|
176
|
+
return name == "View" || name == "RootView" || name == "Paragraph" ||
|
|
177
|
+
name == "ScrollView" || name == "Image" || name == "TextInput" ||
|
|
178
|
+
name == "ActivityIndicatorView" || name == "Switch" || name == "ModalHostView" ||
|
|
179
|
+
name == "PullToRefreshView" || name == "UnimplementedNativeView" ||
|
|
180
|
+
name == "DebuggingOverlay";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
void Win32MountingManager::setUIManager(
|
|
184
|
+
std::weak_ptr<facebook::react::UIManager> uiManager) noexcept {
|
|
185
|
+
setSharedUIManager(std::move(uiManager));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// What MountingWalk asks of a platform
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
RnWin32View *Win32MountingManager::createView(const ShadowView &shadowView) {
|
|
193
|
+
return new RnWin32View(shadowView.tag);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
RnWin32View *Win32MountingManager::createRootView(Tag tag) {
|
|
197
|
+
return new RnWin32View(tag);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
void Win32MountingManager::destroyView(RnWin32View *view) {
|
|
201
|
+
// The one operation that is genuinely different here. On GTK the registry
|
|
202
|
+
// holds a g_object_ref_sink and this is an unref; under ARC it is nothing at
|
|
203
|
+
// all, because erasing the map entry releases the last strong reference. A
|
|
204
|
+
// plain C++ object has neither, so the registry's ownership is written out:
|
|
205
|
+
// this is where a view stops existing.
|
|
206
|
+
//
|
|
207
|
+
// Safe against a view that is still parented, because ~RnWin32View detaches
|
|
208
|
+
// itself -- though Fabric guarantees a Remove before every Delete, so that
|
|
209
|
+
// should never be the path taken.
|
|
210
|
+
delete view;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
void Win32MountingManager::insertChild(RnWin32View *parent, RnWin32View *child, int index) {
|
|
214
|
+
parent->insertChild(child, index);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
void Win32MountingManager::removeChild(RnWin32View *parent, RnWin32View *child) {
|
|
218
|
+
parent->removeChild(child);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
void Win32MountingManager::forgetTag(Tag tag) {
|
|
222
|
+
scrollViews_.remove(tag);
|
|
223
|
+
textInputs_.remove(tag);
|
|
224
|
+
imageUris_.erase(tag);
|
|
225
|
+
switchValues_.erase(tag);
|
|
226
|
+
#ifdef BASALT_HAS_SKIA
|
|
227
|
+
// Unregisters the canvas. This is the one moment a view is known to be
|
|
228
|
+
// finished with, and a canvas left registered is a surface the package will
|
|
229
|
+
// keep rendering into for a tag nothing will ever draw again.
|
|
230
|
+
forgetSkiaCanvas(tag);
|
|
231
|
+
#endif
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Applying a ShadowView to a view
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
void Win32MountingManager::updateView(RnWin32View *view, const ShadowView &shadowView) {
|
|
239
|
+
applyProps(view, shadowView);
|
|
240
|
+
applyText(view, shadowView);
|
|
241
|
+
applyImage(view, shadowView);
|
|
242
|
+
applyAccessibility(view, shadowView);
|
|
243
|
+
#ifdef BASALT_HAS_SKIA
|
|
244
|
+
// After applyProps, which is what puts nativeID on the view -- a canvas
|
|
245
|
+
// registers itself by that id and cannot attach before it is there. The size
|
|
246
|
+
// it reads comes from the shadow view rather than from the frame
|
|
247
|
+
// applyLayoutMetrics is about to set, so being before that one is only
|
|
248
|
+
// incidental.
|
|
249
|
+
//
|
|
250
|
+
// onDidMount_ is handed over as the way to say "the screen is stale": a
|
|
251
|
+
// canvas renders between transactions, when nothing else will invalidate the
|
|
252
|
+
// window. See Win32SkiaPeer.h.
|
|
253
|
+
applySkiaCanvas(view, shadowView, onDidMount_);
|
|
254
|
+
#endif
|
|
255
|
+
applyLayoutMetrics(view, shadowView);
|
|
256
|
+
// Last: the scroll manager clamps its offset against the frame that was just
|
|
257
|
+
// applied, and forces the clip that applyProps may have read as `visible`.
|
|
258
|
+
applyScrollView(view, shadowView);
|
|
259
|
+
applyTextInput(view, shadowView);
|
|
260
|
+
// After layout: a control is drawn centred in the frame it was just given,
|
|
261
|
+
// and a <Modal> commits that frame's size back into its own state.
|
|
262
|
+
applyControls(view, shadowView);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
void Win32MountingManager::applyProps(RnWin32View *view, const ShadowView &shadowView) {
|
|
266
|
+
const auto props = std::dynamic_pointer_cast<const ViewProps>(shadowView.props);
|
|
267
|
+
if (props == nullptr) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (props->backgroundColor) {
|
|
272
|
+
const auto components = facebook::react::colorComponentsFromColor(props->backgroundColor);
|
|
273
|
+
view->setBackgroundColor(
|
|
274
|
+
components.red, components.green, components.blue, components.alpha, true);
|
|
275
|
+
} else {
|
|
276
|
+
// Not transparent black: a view with no background does not paint at all.
|
|
277
|
+
view->setBackgroundColor(0, 0, 0, 0, false);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
view->setOpacity(props->opacity);
|
|
281
|
+
|
|
282
|
+
// overflow: 'hidden'. React Native's default is 'visible'.
|
|
283
|
+
view->setClipsChildren(props->getClipsContentToBounds());
|
|
284
|
+
|
|
285
|
+
// Radii depend on the frame -- percentage radii, and the clamping that stops
|
|
286
|
+
// opposite corners overlapping -- so they are resolved against the layout
|
|
287
|
+
// metrics rather than read raw.
|
|
288
|
+
const auto borders = props->resolveBorderMetrics(shadowView.layoutMetrics);
|
|
289
|
+
// All four corners, each with its own horizontal and vertical radius, in the
|
|
290
|
+
// order GtkMountingManager passes them and describeTree prints them. This
|
|
291
|
+
// used to hand over the top-left horizontal radius alone, which was right for
|
|
292
|
+
// a single `borderRadius` and visibly wrong for anything else.
|
|
293
|
+
const float radii[8] = {
|
|
294
|
+
static_cast<float>(borders.borderRadii.topLeft.horizontal),
|
|
295
|
+
static_cast<float>(borders.borderRadii.topLeft.vertical),
|
|
296
|
+
static_cast<float>(borders.borderRadii.topRight.horizontal),
|
|
297
|
+
static_cast<float>(borders.borderRadii.topRight.vertical),
|
|
298
|
+
static_cast<float>(borders.borderRadii.bottomRight.horizontal),
|
|
299
|
+
static_cast<float>(borders.borderRadii.bottomRight.vertical),
|
|
300
|
+
static_cast<float>(borders.borderRadii.bottomLeft.horizontal),
|
|
301
|
+
static_cast<float>(borders.borderRadii.bottomLeft.vertical),
|
|
302
|
+
};
|
|
303
|
+
view->setCornerRadii(radii);
|
|
304
|
+
|
|
305
|
+
// Top, right, bottom, left, as GTK takes them -- the order CSS names the
|
|
306
|
+
// edges in, and not the order RectangleEdges stores them. An edge without a
|
|
307
|
+
// colour is transparent, so it draws nothing whatever its width.
|
|
308
|
+
const float widths[4] = {
|
|
309
|
+
static_cast<float>(borders.borderWidths.top),
|
|
310
|
+
static_cast<float>(borders.borderWidths.right),
|
|
311
|
+
static_cast<float>(borders.borderWidths.bottom),
|
|
312
|
+
static_cast<float>(borders.borderWidths.left),
|
|
313
|
+
};
|
|
314
|
+
float colours[16] = {};
|
|
315
|
+
const facebook::react::SharedColor *edges[4] = {
|
|
316
|
+
&borders.borderColors.top,
|
|
317
|
+
&borders.borderColors.right,
|
|
318
|
+
&borders.borderColors.bottom,
|
|
319
|
+
&borders.borderColors.left,
|
|
320
|
+
};
|
|
321
|
+
for (int i = 0; i < 4; i++) {
|
|
322
|
+
if (*edges[i]) {
|
|
323
|
+
const auto components = facebook::react::colorComponentsFromColor(*edges[i]);
|
|
324
|
+
colours[i * 4 + 0] = components.red;
|
|
325
|
+
colours[i * 4 + 1] = components.green;
|
|
326
|
+
colours[i * 4 + 2] = components.blue;
|
|
327
|
+
colours[i * 4 + 3] = components.alpha;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
view->setBorders(widths, colours);
|
|
331
|
+
|
|
332
|
+
view->setZIndex(static_cast<int>(props->zIndex.value_or(0)));
|
|
333
|
+
|
|
334
|
+
// resolveTransform folds in transformOrigin, but only when one was set: the
|
|
335
|
+
// default anchor is the view's centre, which is what RnWin32View::paint
|
|
336
|
+
// already assumes.
|
|
337
|
+
const auto transform = props->resolveTransform(shadowView.layoutMetrics);
|
|
338
|
+
if (transform == facebook::react::Transform::Identity()) {
|
|
339
|
+
view->setTransform(nullptr);
|
|
340
|
+
} else {
|
|
341
|
+
view->setTransform(transform.matrix.data());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// `backfaceVisibility: 'hidden'`, which stops the back of a card being drawn
|
|
345
|
+
// mirrored halfway through a flip. Whether a transform has turned away is
|
|
346
|
+
// core/Backface.h's to decide, so all three hide the same face.
|
|
347
|
+
//
|
|
348
|
+
// Applied after the transform, because the view has to know the matrix
|
|
349
|
+
// before it can say which way it is facing.
|
|
350
|
+
view->setHidesBackFace(props->backfaceVisibility ==
|
|
351
|
+
facebook::react::BackfaceVisibility::Hidden);
|
|
352
|
+
|
|
353
|
+
// Only a hidden title bar reads this, to find the drag regions an app marked
|
|
354
|
+
// with <TitleBar.DragRegion>.
|
|
355
|
+
view->setNativeId(props->nativeId);
|
|
356
|
+
|
|
357
|
+
// Hit testing only. `hitTest` reads it; nothing about painting does.
|
|
358
|
+
switch (props->pointerEvents) {
|
|
359
|
+
case facebook::react::PointerEventsMode::None:
|
|
360
|
+
view->setPointerEvents(win32::RnWin32View::PointerEvents::None);
|
|
361
|
+
break;
|
|
362
|
+
case facebook::react::PointerEventsMode::BoxNone:
|
|
363
|
+
view->setPointerEvents(win32::RnWin32View::PointerEvents::BoxNone);
|
|
364
|
+
break;
|
|
365
|
+
case facebook::react::PointerEventsMode::BoxOnly:
|
|
366
|
+
view->setPointerEvents(win32::RnWin32View::PointerEvents::BoxOnly);
|
|
367
|
+
break;
|
|
368
|
+
case facebook::react::PointerEventsMode::Auto:
|
|
369
|
+
view->setPointerEvents(win32::RnWin32View::PointerEvents::Auto);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// A <Paragraph> carries its text in state, not props: ParagraphShadowNode
|
|
375
|
+
// resolves the whole <Text> subtree into one AttributedString and commits it as
|
|
376
|
+
// ParagraphState, which is why nothing here walks child shadow nodes.
|
|
377
|
+
void Win32MountingManager::applyText(RnWin32View *view, const ShadowView &shadowView) {
|
|
378
|
+
if (shadowView.componentName == nullptr ||
|
|
379
|
+
std::string_view(shadowView.componentName) != "Paragraph") {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const auto state =
|
|
384
|
+
std::dynamic_pointer_cast<const facebook::react::ConcreteState<ParagraphState>>(
|
|
385
|
+
shadowView.state);
|
|
386
|
+
if (state == nullptr) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const auto &data = state->getData();
|
|
391
|
+
// Built through the same function the measurement seam uses, which is what
|
|
392
|
+
// makes the painted lines break where the measured ones did.
|
|
393
|
+
view->setTextLayout(
|
|
394
|
+
win32::buildTextLayout(data.attributedString, data.paragraphAttributes));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
namespace {
|
|
398
|
+
|
|
399
|
+
RnImageFit toImageFit(ImageResizeMode mode) {
|
|
400
|
+
switch (mode) {
|
|
401
|
+
case ImageResizeMode::Contain:
|
|
402
|
+
return RnImageFit::Contain;
|
|
403
|
+
case ImageResizeMode::Stretch:
|
|
404
|
+
return RnImageFit::Stretch;
|
|
405
|
+
case ImageResizeMode::Center:
|
|
406
|
+
case ImageResizeMode::None:
|
|
407
|
+
return RnImageFit::Center;
|
|
408
|
+
case ImageResizeMode::Repeat:
|
|
409
|
+
return RnImageFit::Repeat;
|
|
410
|
+
case ImageResizeMode::Cover:
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
return RnImageFit::Cover;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} // namespace
|
|
417
|
+
|
|
418
|
+
void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shadowView) {
|
|
419
|
+
if (shadowView.componentName == nullptr ||
|
|
420
|
+
std::string_view(shadowView.componentName) != "Image") {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const auto props = std::dynamic_pointer_cast<const ImageProps>(shadowView.props);
|
|
425
|
+
if (props == nullptr) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const RnImageFit fit = toImageFit(props->resizeMode);
|
|
430
|
+
const std::string uri =
|
|
431
|
+
props->sources.empty() ? std::string{} : props->sources.front().uri;
|
|
432
|
+
|
|
433
|
+
// `tintColor`, applied before the load rather than in its callback: the tint
|
|
434
|
+
// is a prop and the image is a loader's answer, and either can arrive first.
|
|
435
|
+
if (props->tintColor) {
|
|
436
|
+
const auto components = facebook::react::colorComponentsFromColor(*props->tintColor);
|
|
437
|
+
const float rgba[4] = {components.red, components.green, components.blue, components.alpha};
|
|
438
|
+
view->setImageTint(true, rgba);
|
|
439
|
+
} else {
|
|
440
|
+
view->setImageTint(false, nullptr);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// A mutation that changed only layout must not restart the load, or an
|
|
444
|
+
// <Image> flickers whenever its parent resizes. The fit is applied every time
|
|
445
|
+
// regardless, because changing it is cheap and does not touch the pixels.
|
|
446
|
+
const auto previous = imageUris_.find(shadowView.tag);
|
|
447
|
+
if (previous != imageUris_.end() && previous->second == uri) {
|
|
448
|
+
view->setImage(view->image(), fit);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
imageUris_[shadowView.tag] = uri;
|
|
452
|
+
|
|
453
|
+
if (uri.empty()) {
|
|
454
|
+
view->setImage(nullptr, fit);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Clear whatever was showing: the source changed, and leaving the old picture
|
|
459
|
+
// up while the new one loads is worse than a blank box, because it looks like
|
|
460
|
+
// the change did not take.
|
|
461
|
+
view->setImage(nullptr, fit);
|
|
462
|
+
|
|
463
|
+
// The fetch is in the shared half, which knows file, data: and http URIs --
|
|
464
|
+
// a URI means the same thing on every desktop -- and the loader puts both it
|
|
465
|
+
// and the decode on a worker thread.
|
|
466
|
+
//
|
|
467
|
+
// The completion captures the *tag*, not the view. A view can be deleted
|
|
468
|
+
// while its bytes are in flight, and capturing the pointer would be a
|
|
469
|
+
// use-after-free on a slow network; looking it up again is the arrangement
|
|
470
|
+
// GTK settled on for the same reason. `this` is safe to capture because the
|
|
471
|
+
// loader is a member and cannot outlive the manager -- and the loader itself
|
|
472
|
+
// guards the case where the manager goes first.
|
|
473
|
+
const Tag tag = shadowView.tag;
|
|
474
|
+
imageLoader_->load(uri, [this, tag, uri, fit](std::shared_ptr<win32::RnWin32Image> image,
|
|
475
|
+
const std::string &error) {
|
|
476
|
+
(void)error;
|
|
477
|
+
RnWin32View *target = viewForTag(tag);
|
|
478
|
+
if (target == nullptr) {
|
|
479
|
+
// Deleted while loading. Not an error, and not worth logging: scrolling a
|
|
480
|
+
// list past an image faster than it arrives does exactly this.
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// And the source may have changed *again* while this one was loading, in
|
|
484
|
+
// which case a later load owns the view and this result is stale.
|
|
485
|
+
const auto current = imageUris_.find(tag);
|
|
486
|
+
if (current == imageUris_.end() || current->second != uri) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
target->setImage(std::move(image), fit);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
namespace {
|
|
494
|
+
|
|
495
|
+
win32::RnAccessibleFlag toFlag(bool value) {
|
|
496
|
+
return value ? win32::RnAccessibleFlag::True : win32::RnAccessibleFlag::False;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// The role React Native effectively means for this view: what the app asked
|
|
500
|
+
// for, or what the component implies.
|
|
501
|
+
//
|
|
502
|
+
// A <Text> is a label and an <Image> is an image whether or not the app said
|
|
503
|
+
// so, which is what makes an ordinary screen navigable without every developer
|
|
504
|
+
// having annotated it. Both other platforms infer the same two.
|
|
505
|
+
std::string effectiveRole(const ShadowView &shadowView) {
|
|
506
|
+
if (const auto props =
|
|
507
|
+
std::dynamic_pointer_cast<const facebook::react::AccessibilityProps>(shadowView.props)) {
|
|
508
|
+
if (!props->accessibilityRole.empty()) {
|
|
509
|
+
return props->accessibilityRole;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (shadowView.componentName != nullptr) {
|
|
513
|
+
const std::string_view name(shadowView.componentName);
|
|
514
|
+
if (name == "Paragraph") {
|
|
515
|
+
return "text";
|
|
516
|
+
}
|
|
517
|
+
if (name == "Image") {
|
|
518
|
+
return "image";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return {};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
} // namespace
|
|
525
|
+
|
|
526
|
+
void Win32MountingManager::applyAccessibility(RnWin32View *view, const ShadowView &shadowView) {
|
|
527
|
+
const auto props =
|
|
528
|
+
std::dynamic_pointer_cast<const facebook::react::AccessibilityProps>(shadowView.props);
|
|
529
|
+
if (props == nullptr) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
win32::RnAccessibleInfo info;
|
|
534
|
+
info.role = effectiveRole(shadowView);
|
|
535
|
+
|
|
536
|
+
// A label given in props wins. Falling back to a Paragraph's own text means a
|
|
537
|
+
// plain <Text> announces itself without the app having repeated the string in
|
|
538
|
+
// an accessibilityLabel, which is what the other two hosts do.
|
|
539
|
+
info.label = props->accessibilityLabel;
|
|
540
|
+
if (info.label.empty() && view->textLayout() != nullptr) {
|
|
541
|
+
info.label = view->textLayout()->text();
|
|
542
|
+
}
|
|
543
|
+
info.hint = props->accessibilityHint;
|
|
544
|
+
|
|
545
|
+
const auto &state = props->accessibilityState;
|
|
546
|
+
if (state.has_value()) {
|
|
547
|
+
info.state.disabled = toFlag(state->disabled);
|
|
548
|
+
info.state.selected = toFlag(state->selected);
|
|
549
|
+
info.state.busy = toFlag(state->busy);
|
|
550
|
+
if (state->checked != facebook::react::AccessibilityState::Unchecked ||
|
|
551
|
+
!info.role.empty()) {
|
|
552
|
+
// Tri-state on purpose: `checked` unset is not the same as false, and a
|
|
553
|
+
// view that never mentions being checked must not report ToggleState_Off.
|
|
554
|
+
// React Native's own enum has no "unset", so Unchecked on a view with no
|
|
555
|
+
// role is treated as "did not say".
|
|
556
|
+
info.state.checked = state->checked == facebook::react::AccessibilityState::Checked
|
|
557
|
+
? win32::RnAccessibleFlag::True
|
|
558
|
+
: win32::RnAccessibleFlag::False;
|
|
559
|
+
}
|
|
560
|
+
if (state->expanded.has_value()) {
|
|
561
|
+
info.state.expanded = toFlag(*state->expanded);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// accessible={false} and accessibilityElementsHidden both mean "not for a
|
|
566
|
+
// screen reader".
|
|
567
|
+
info.hidden = !props->accessible ||
|
|
568
|
+
props->importantForAccessibility == facebook::react::ImportantForAccessibility::NoHideDescendants;
|
|
569
|
+
|
|
570
|
+
view->setAccessibleInfo(info);
|
|
571
|
+
|
|
572
|
+
// Keyboard focus. `accessible` is the signal because React Native's
|
|
573
|
+
// `focusable` prop never reaches this platform -- see Win32Focus.h -- and
|
|
574
|
+
// because it is what <Pressable> sets on everything it renders. A hidden view
|
|
575
|
+
// is not focusable whatever it says: Tab stopping on something nobody can see
|
|
576
|
+
// is worse than Tab skipping it.
|
|
577
|
+
// A control is focusable whether or not the app said `accessible`.
|
|
578
|
+
//
|
|
579
|
+
// React Native's `focusable` prop never reaches this platform -- ReactCommon
|
|
580
|
+
// parses it only into Android's and tvOS's props -- so `accessible` is the
|
|
581
|
+
// signal, and <Pressable> sets it. <Switch> does not: on a phone the native
|
|
582
|
+
// control is focusable by being a control, and React Native never had to say
|
|
583
|
+
// so. On a desktop a switch that Tab skips is simply broken, so being a
|
|
584
|
+
// control is the signal here too.
|
|
585
|
+
// Disabled is excluded, which is what every desktop does: Tab skips a control
|
|
586
|
+
// that cannot be operated rather than stopping on one that does nothing.
|
|
587
|
+
const basalt::ControlState control = basalt::controlStateOf(shadowView);
|
|
588
|
+
const bool isControl = control.kind == basalt::ControlKind::Switch && !control.disabled;
|
|
589
|
+
view->setFocusable((props->accessible || isControl) && !props->accessibilityElementsHidden);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
void Win32MountingManager::applyLayoutMetrics(RnWin32View *view, const ShadowView &shadowView) {
|
|
593
|
+
const auto &frame = shadowView.layoutMetrics.frame;
|
|
594
|
+
view->setFrame(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height);
|
|
595
|
+
|
|
596
|
+
// display: 'none' keeps the node in the shadow tree but takes it out of
|
|
597
|
+
// layout and painting -- and, here, out of hit testing too.
|
|
598
|
+
view->setHidden(shadowView.layoutMetrics.displayType == facebook::react::DisplayType::None);
|
|
599
|
+
|
|
600
|
+
// TODO(layout): pointScaleFactor, once a high-DPI backing store is involved.
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
void Win32MountingManager::applyScrollView(RnWin32View *view, const ShadowView &shadowView) {
|
|
604
|
+
if (std::string_view(shadowView.componentName) != "ScrollView") {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
scrollViews_.update(view, shadowView);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// The wheel
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
bool Win32MountingManager::scrollAt(
|
|
615
|
+
RnWin32View *root, double x, double y, double deltaX, double deltaY) {
|
|
616
|
+
return scrollViews_.scrollAt(root, x, y, deltaX, deltaY);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// <TextInput>
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// Controls
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
void Win32MountingManager::applyControlPeer(RnWin32View *view, const basalt::ControlState &state) {
|
|
629
|
+
RnWin32View::ControlStyle style;
|
|
630
|
+
style.on = state.on;
|
|
631
|
+
style.disabled = state.disabled;
|
|
632
|
+
style.large = state.large;
|
|
633
|
+
style.hidesWhenStopped = state.hidesWhenStopped;
|
|
634
|
+
style.hasThumb = state.hasForeground;
|
|
635
|
+
style.hasTrackOn = state.hasTrackOn;
|
|
636
|
+
style.hasTrackOff = state.hasTrackOff;
|
|
637
|
+
std::copy(std::begin(state.foreground), std::end(state.foreground), std::begin(style.thumb));
|
|
638
|
+
std::copy(std::begin(state.trackOn), std::end(state.trackOn), std::begin(style.trackOn));
|
|
639
|
+
std::copy(std::begin(state.trackOff), std::end(state.trackOff), std::begin(style.trackOff));
|
|
640
|
+
|
|
641
|
+
if (state.kind == basalt::ControlKind::Switch) {
|
|
642
|
+
switchValues_[static_cast<Tag>(view->tag())] = state.on;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
view->setControl(state.kind == basalt::ControlKind::Switch ? RnWin32View::Control::Switch
|
|
646
|
+
: RnWin32View::Control::Spinner,
|
|
647
|
+
style,
|
|
648
|
+
basalt::describeControl(state));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
void Win32MountingManager::setHighlights(RnWin32View *view,
|
|
652
|
+
const std::vector<basalt::Highlight> &highlights) {
|
|
653
|
+
std::vector<RnWin32View::Highlight> converted;
|
|
654
|
+
converted.reserve(highlights.size());
|
|
655
|
+
for (const basalt::Highlight &highlight : highlights) {
|
|
656
|
+
RnWin32View::Highlight entry;
|
|
657
|
+
entry.x = highlight.x;
|
|
658
|
+
entry.y = highlight.y;
|
|
659
|
+
entry.width = highlight.width;
|
|
660
|
+
entry.height = highlight.height;
|
|
661
|
+
std::copy(std::begin(highlight.color), std::end(highlight.color), std::begin(entry.color));
|
|
662
|
+
entry.filled = highlight.filled;
|
|
663
|
+
converted.push_back(entry);
|
|
664
|
+
}
|
|
665
|
+
view->setHighlights(std::move(converted));
|
|
666
|
+
// Nothing else asks for a repaint: an overlay command arrives outside a mount
|
|
667
|
+
// transaction, so the window would otherwise show the highlight at whatever
|
|
668
|
+
// moment something else happened to invalidate it.
|
|
669
|
+
if (onDidMount_) {
|
|
670
|
+
onDidMount_();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
void Win32MountingManager::pressedView(Tag tag) {
|
|
675
|
+
RnWin32View *view = viewForTag(tag);
|
|
676
|
+
if (view == nullptr || view->control() != RnWin32View::Control::Switch) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (view->controlStyle().disabled) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const auto known = switchValues_.find(tag);
|
|
683
|
+
if (known == switchValues_.end()) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Told, and then left alone. React Native's <Switch> is a controlled
|
|
688
|
+
// component: the app's `value` prop is the only thing that moves it, and a
|
|
689
|
+
// switch that flipped itself would show a state its props do not agree with
|
|
690
|
+
// -- which is exactly what an app that ignores onValueChange is supposed to
|
|
691
|
+
// look like. The other two hosts have to put their real widget back for the
|
|
692
|
+
// same reason; here there is nothing to put back, because nothing moved.
|
|
693
|
+
basalt::emitSwitchChange(eventEmitterForTag(tag), tag, !known->second);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
void Win32MountingManager::applyTextInput(RnWin32View *view, const ShadowView &shadowView) {
|
|
697
|
+
if (std::string_view(shadowView.componentName) != "TextInput") {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
textInputs_.update(view, shadowView);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
void Win32MountingManager::setHostWindow(HWND window) {
|
|
704
|
+
textInputs_.setHostWindow(window);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
void Win32MountingManager::syncTextInputBounds(RnWin32View *root) {
|
|
708
|
+
textInputs_.syncBounds(root);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
bool Win32MountingManager::handleControlCommand(WPARAM wparam, LPARAM lparam) {
|
|
712
|
+
return textInputs_.handleControlCommand(wparam, lparam);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
HBRUSH Win32MountingManager::controlColor(HDC deviceContext, HWND control) {
|
|
716
|
+
return textInputs_.controlColor(deviceContext, control);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
bool Win32MountingManager::typeIntoFocusedTextInput(const std::string &text) {
|
|
720
|
+
return textInputs_.typeIntoFocused(text);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
bool Win32MountingManager::focusTextInputAt(RnWin32View *root, double x, double y) {
|
|
724
|
+
return textInputs_.focusAt(root, x, y);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
} // namespace basalt
|