basalt-appkit 0.0.0-rc5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/CMakeLists.txt +16 -2
- package/native/appkit/AppKitAppWindow.h +24 -0
- package/native/appkit/AppKitImageLoader.h +48 -1
- package/native/appkit/AppKitImageLoader.mm +228 -10
- package/native/appkit/AppKitMenuBar.mm +58 -0
- package/native/appkit/AppKitMountingManager.h +34 -0
- package/native/appkit/AppKitMountingManager.mm +582 -12
- package/native/appkit/AppKitScrollView.h +48 -0
- package/native/appkit/AppKitScrollView.mm +210 -0
- package/native/appkit/AppKitSkiaPeer.h +15 -0
- package/native/appkit/AppKitTextInput.h +38 -1
- package/native/appkit/AppKitTextInput.mm +235 -8
- package/native/appkit/AppKitTextPeer.h +60 -0
- package/native/appkit/AppKitTextPeer.mm +307 -0
- package/native/appkit/AppKitTouchDispatcher.h +56 -0
- package/native/appkit/AppKitTouchDispatcher.mm +140 -0
- package/native/appkit/AppKitWindowControl.mm +38 -3
- package/native/appkit/ComponentRegistryAppKit.mm +28 -3
- package/native/appkit/CoreTextLayout.h +64 -2
- package/native/appkit/CoreTextLayout.mm +431 -71
- package/native/appkit/CoreTextLayoutManager.mm +57 -6
- package/native/appkit/FontRegistryCoreText.mm +6 -5
- package/native/appkit/PlatformServicesAppKit.mm +101 -20
- package/native/appkit/RnAppKitView.h +492 -0
- package/native/appkit/RnAppKitView.mm +2266 -23
- package/native/appkit/RnTextLayout.h +106 -0
- package/native/appkit/RnTextLayout.mm +321 -2
- package/native/appkit/main_appkit.mm +174 -3
- package/native/tests/test_appkit_accessibility.mm +289 -0
- package/native/tests/test_appkit_borders.mm +975 -0
- package/native/tests/test_appkit_fonts.mm +203 -0
- package/native/tests/test_appkit_image.mm +392 -17
- package/native/tests/test_appkit_input.mm +209 -0
- package/native/tests/test_appkit_mounting.mm +360 -0
- package/native/tests/test_appkit_scrollbar.mm +125 -0
- package/native/tests/test_appkit_scrollview.mm +371 -0
- package/native/tests/test_appkit_selection.mm +349 -0
- package/native/tests/test_appkit_services.mm +51 -1
- package/native/tests/test_appkit_text.mm +1628 -0
- package/native/tests/test_appkit_textinput.mm +819 -0
- package/native/tests/test_appkit_titlebar.mm +43 -0
- package/native/tests/test_appkit_view.mm +609 -0
- package/package.json +4 -3
|
@@ -6,16 +6,32 @@
|
|
|
6
6
|
#import <CoreImage/CoreImage.h>
|
|
7
7
|
|
|
8
8
|
#include "ComponentRegistry.h"
|
|
9
|
+
#include "BackgroundLayers.h"
|
|
10
|
+
#include "TextShadows.h"
|
|
11
|
+
#include "TextAlignments.h"
|
|
12
|
+
#include "WritingDirections.h"
|
|
13
|
+
#include "BlendModes.h"
|
|
14
|
+
#include "LegacyShadow.h"
|
|
15
|
+
#include "CursorNames.h"
|
|
16
|
+
#include "Filters.h"
|
|
17
|
+
#include "Gradients.h"
|
|
9
18
|
#include "ExpoImageComponent.h"
|
|
10
|
-
#
|
|
19
|
+
#include "ImageProgress.h"
|
|
20
|
+
// Either Skia: the 2.x peer in this package or the 3.x one in basalt-skia,
|
|
21
|
+
// which share this header's two functions and are never both linked. See
|
|
22
|
+
// AppKitSkiaPeer.h.
|
|
23
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
11
24
|
#include "AppKitSkiaPeer.h"
|
|
12
25
|
#endif
|
|
13
26
|
#include "UIManagerAccess.h"
|
|
14
27
|
|
|
28
|
+
#include <cstdint>
|
|
29
|
+
|
|
15
30
|
#include <react/renderer/components/view/AccessibilityProps.h>
|
|
16
31
|
#include <react/renderer/components/image/ImageEventEmitter.h>
|
|
17
32
|
#include <react/renderer/components/image/ImageProps.h>
|
|
18
33
|
#include <react/renderer/components/scrollview/ScrollViewProps.h>
|
|
34
|
+
#include <react/renderer/components/text/ParagraphProps.h>
|
|
19
35
|
#include <react/renderer/components/text/ParagraphState.h>
|
|
20
36
|
#include <react/renderer/components/view/ViewProps.h>
|
|
21
37
|
#include <react/renderer/core/ConcreteState.h>
|
|
@@ -107,10 +123,22 @@ struct PendingMount {
|
|
|
107
123
|
AppKitMountingManager *manager;
|
|
108
124
|
SurfaceId surfaceId;
|
|
109
125
|
MountingTransaction transaction;
|
|
126
|
+
// Whether `manager` is still there, and which instance this came from. See
|
|
127
|
+
// MountingWalk::mountGuard.
|
|
128
|
+
std::weak_ptr<bool> alive;
|
|
129
|
+
std::uint64_t epoch;
|
|
110
130
|
};
|
|
111
131
|
|
|
112
132
|
void applyPendingMount(void *data) {
|
|
113
133
|
std::unique_ptr<PendingMount> pending{static_cast<PendingMount *>(data)};
|
|
134
|
+
// The guard first: dereferencing `manager` to read its epoch is itself the
|
|
135
|
+
// use-after-free when the host has already torn it down.
|
|
136
|
+
if (pending->alive.expired()) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (pending->manager->mountEpoch() != pending->epoch) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
114
142
|
pending->manager->applyTransaction(pending->surfaceId, std::move(pending->transaction));
|
|
115
143
|
}
|
|
116
144
|
|
|
@@ -127,15 +155,30 @@ void AppKitMountingManager::executeMount(SurfaceId surfaceId, MountingTransactio
|
|
|
127
155
|
// The main queue is FIFO, which is what keeps mutation ordering intact, and
|
|
128
156
|
// running one transaction inline while another is queued would break it. iOS
|
|
129
157
|
// and Android marshal here too.
|
|
130
|
-
auto *pending =
|
|
158
|
+
auto *pending =
|
|
159
|
+
new PendingMount{this, surfaceId, std::move(transaction), mountGuard(), mountEpoch()};
|
|
131
160
|
dispatch_async_f(dispatch_get_main_queue(), pending, applyPendingMount);
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
void AppKitMountingManager::applyTransaction(SurfaceId surfaceId, MountingTransaction &&transaction) {
|
|
164
|
+
// `maintainVisibleContentPosition` is measured before the mutations and acted
|
|
165
|
+
// on after them, which is the only place either half can go: the prop is
|
|
166
|
+
// about how far a child moved *during* a transaction. iOS hangs the same pair
|
|
167
|
+
// off `mountingTransactionWillMount` and `...DidMount`.
|
|
168
|
+
scrollViews_.prepareMaintainVisiblePosition();
|
|
169
|
+
|
|
135
170
|
// The walk itself is in core/MountingWalk.h and is shared with GTK; what is
|
|
136
171
|
// AppKit about mounting is below, in the operations it calls back into.
|
|
137
172
|
applyMutations(transaction.getMutations());
|
|
138
173
|
|
|
174
|
+
// And the other half, before anything repaints: the offset moves so that the
|
|
175
|
+
// child the list was looking at is still under the eye.
|
|
176
|
+
scrollViews_.adjustForMaintainVisiblePosition();
|
|
177
|
+
|
|
178
|
+
// Now that the tree is on screen, anything `autoFocus` asked for can be given
|
|
179
|
+
// focus. Not before: see AppKitTextInput.h.
|
|
180
|
+
textInputs_.flushAutoFocus();
|
|
181
|
+
|
|
139
182
|
// Tell the UIManager the transaction is on screen. Anything registered as a
|
|
140
183
|
// mount hook -- Reanimated's is the one that matters here -- is waiting for
|
|
141
184
|
// this, and without it an animated style is computed every frame, committed
|
|
@@ -145,8 +188,81 @@ void AppKitMountingManager::applyTransaction(SurfaceId surfaceId, MountingTransa
|
|
|
145
188
|
// manager. ReactCxxPlatform does it nowhere, which is a gap in the shared
|
|
146
189
|
// platform rather than in either host: nothing in it had a mount hook until
|
|
147
190
|
// a third-party library brought one.
|
|
148
|
-
|
|
149
|
-
|
|
191
|
+
// Refuses a surface the UIManager no longer has, which is what a reload's
|
|
192
|
+
// teardown leaves behind. See core/UIManagerAccess.h.
|
|
193
|
+
reportMountedSurface(surfaceId);
|
|
194
|
+
|
|
195
|
+
// After the mutations, because a view can be labelled by one that mounts after
|
|
196
|
+
// it: Fabric mounts in tree order, and the label of a field often follows it.
|
|
197
|
+
applyLabelRelations();
|
|
198
|
+
|
|
199
|
+
// And after those, because an announcement is the last word on a transaction:
|
|
200
|
+
// the text a live region says now is what every mutation in this batch has left
|
|
201
|
+
// it saying.
|
|
202
|
+
announceLiveRegions();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void AppKitMountingManager::announceLiveRegions() {
|
|
206
|
+
if (liveRegions_.empty()) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const facebook::react::Tag tag : liveRegions_.tags()) {
|
|
210
|
+
RnAppKitView *view = viewForTag(tag);
|
|
211
|
+
if (view == nil) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// A label stands in for the text when the app set one: an icon-only status
|
|
215
|
+
// has no paragraph of its own, and the label is what would be read. The GTK
|
|
216
|
+
// host makes the same choice in the same order.
|
|
217
|
+
const auto label = liveRegionLabels_.find(tag);
|
|
218
|
+
std::string text = label != liveRegionLabels_.end() ? label->second : std::string();
|
|
219
|
+
if (text.empty()) {
|
|
220
|
+
text = view.rnCollectedText.UTF8String != nullptr ? view.rnCollectedText.UTF8String : "";
|
|
221
|
+
}
|
|
222
|
+
const auto politeness = liveRegions_.noticed(tag, text);
|
|
223
|
+
if (!politeness.has_value()) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const BOOL assertive = *politeness == basalt::LiveRegionPoliteness::Assertive ? YES : NO;
|
|
227
|
+
[view rnAnnounce:[NSString stringWithUTF8String:text.c_str()] assertive:assertive];
|
|
228
|
+
// Logged as well as announced: nothing in an automated run is running
|
|
229
|
+
// VoiceOver, so this line is how an end-to-end test sees that it happened.
|
|
230
|
+
LOG(INFO) << "announced" << (assertive ? " (assertive): " : ": ") << text;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
void AppKitMountingManager::applyLabelRelations() {
|
|
235
|
+
if (labels_.empty()) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
for (const auto &change : labels_.changes()) {
|
|
239
|
+
RnAppKitView *view = viewForTag(change.tag);
|
|
240
|
+
if (view == nil) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
NSMutableArray<RnAppKitView *> *resolved = [NSMutableArray array];
|
|
244
|
+
for (const facebook::react::Tag tag : change.labels) {
|
|
245
|
+
if (RnAppKitView *label = viewForTag(tag); label != nil) {
|
|
246
|
+
[resolved addObject:label];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
[view setRnLabelledBy:resolved];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The reading order, from the same registry and on the same terms: only what
|
|
253
|
+
// changed, and an empty list is a view that stopped asking.
|
|
254
|
+
for (const auto &change : labels_.orderChanges()) {
|
|
255
|
+
RnAppKitView *view = viewForTag(change.tag);
|
|
256
|
+
if (view == nil) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
NSMutableArray<RnAppKitView *> *children = [NSMutableArray array];
|
|
260
|
+
for (const facebook::react::Tag tag : change.labels) {
|
|
261
|
+
if (RnAppKitView *child = viewForTag(tag); child != nil) {
|
|
262
|
+
[children addObject:child];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
[view setRnAccessibilityOrder:children];
|
|
150
266
|
}
|
|
151
267
|
}
|
|
152
268
|
|
|
@@ -204,6 +320,17 @@ void AppKitMountingManager::setUIManager(std::weak_ptr<facebook::react::UIManage
|
|
|
204
320
|
setSharedUIManager(std::move(uiManager));
|
|
205
321
|
}
|
|
206
322
|
|
|
323
|
+
void AppKitMountingManager::setSchedulerTaskExecutor(
|
|
324
|
+
facebook::react::SchedulerTaskExecutor &&schedulerTaskExecutor) noexcept {
|
|
325
|
+
// A null executor means `destroyReactInstance`: the Scheduler and the
|
|
326
|
+
// SurfaceManager are about to go, and anything this already queued to the main
|
|
327
|
+
// queue would be applied against them. Non-null means a new instance, which
|
|
328
|
+
// needs nothing from here.
|
|
329
|
+
if (!schedulerTaskExecutor) {
|
|
330
|
+
invalidatePendingMounts();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
207
334
|
ComponentRegistryFactory AppKitMountingManager::getComponentRegistryFactory() {
|
|
208
335
|
return facebook::react::getDefaultComponentRegistryFactory();
|
|
209
336
|
}
|
|
@@ -261,11 +388,17 @@ void AppKitMountingManager::removeChild(RnAppKitView *parent, RnAppKitView *chil
|
|
|
261
388
|
}
|
|
262
389
|
|
|
263
390
|
void AppKitMountingManager::forgetTag(Tag tag) {
|
|
391
|
+
liveRegions_.forget(tag);
|
|
392
|
+
liveRegionLabels_.erase(tag);
|
|
393
|
+
// Both sides of a label relation: a view that goes away stops naming anything
|
|
394
|
+
// and stops being nameable, and a relation left pointing at it would have
|
|
395
|
+
// VoiceOver read a view that is no longer on screen.
|
|
396
|
+
labels_.forget(tag);
|
|
264
397
|
scrollViews_.remove(tag);
|
|
265
398
|
textInputs_.remove(tag);
|
|
266
399
|
imageUris_.erase(tag);
|
|
267
400
|
switchValues_.erase(tag);
|
|
268
|
-
#
|
|
401
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
269
402
|
// Unregisters the canvas and takes its layer off the view. This is the one
|
|
270
403
|
// moment a view is known to be finished with, which matters more here than
|
|
271
404
|
// elsewhere: a canvas left registered keeps a Metal layer and a GPU surface
|
|
@@ -276,6 +409,31 @@ void AppKitMountingManager::forgetTag(Tag tag) {
|
|
|
276
409
|
|
|
277
410
|
namespace {
|
|
278
411
|
|
|
412
|
+
// Hands a view the frames of an animated image, or clears any it had.
|
|
413
|
+
//
|
|
414
|
+
// Asked of the loader rather than carried in the load callback, because being
|
|
415
|
+
// animated is a property of the file and the callback's job is the pixels. The
|
|
416
|
+
// arrays are the loader's and are the same arrays each time, which is what lets
|
|
417
|
+
// the view tell a re-mount from a new animation.
|
|
418
|
+
void applyImageAnimation(AppKitImageLoader *loader, RnAppKitView *view, const std::string &uri) {
|
|
419
|
+
if (view == nil) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const AppKitImageLoader::Animation *animation =
|
|
423
|
+
uri.empty() ? nullptr : loader->animation(uri);
|
|
424
|
+
if (animation == nullptr) {
|
|
425
|
+
[view setRnImageFrames:nil delaysMs:nil loopCount:0];
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
[view setRnImageFrames:animation->frames
|
|
429
|
+
delaysMs:animation->delaysMs
|
|
430
|
+
loopCount:animation->loopCount];
|
|
431
|
+
// And drive it, which the view deliberately does not do for itself; see
|
|
432
|
+
// rnStartImageAnimation. Idempotent, so the re-apply on every mutation costs
|
|
433
|
+
// nothing.
|
|
434
|
+
[view rnStartImageAnimation];
|
|
435
|
+
}
|
|
436
|
+
|
|
279
437
|
RnAppKitImageFit toImageFit(ImageResizeMode mode) {
|
|
280
438
|
switch (mode) {
|
|
281
439
|
case ImageResizeMode::Contain:
|
|
@@ -319,6 +477,10 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
319
477
|
// unqualified ImageSource resolves to that one.
|
|
320
478
|
facebook::react::ImageSource source{};
|
|
321
479
|
facebook::react::SharedColor tint{};
|
|
480
|
+
// blurRadius alongside the tint, captured the same way: each branch below
|
|
481
|
+
// knows its own props type, and both props are applied together after them.
|
|
482
|
+
// expo-image has no blur prop, so that branch leaves this at zero.
|
|
483
|
+
CGFloat blurRadius = 0;
|
|
322
484
|
|
|
323
485
|
if (isExpoImage) {
|
|
324
486
|
const auto props = std::dynamic_pointer_cast<const facebook::react::ExpoImageProps>(shadowView.props);
|
|
@@ -344,13 +506,14 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
344
506
|
if (props->tintColor) {
|
|
345
507
|
tint = *props->tintColor;
|
|
346
508
|
}
|
|
509
|
+
blurRadius = (CGFloat)props->blurRadius;
|
|
347
510
|
}
|
|
348
511
|
|
|
349
512
|
const std::string uri = source.uri;
|
|
350
513
|
const Tag tag = shadowView.tag;
|
|
351
514
|
|
|
352
|
-
// Applied before the load rather than in its callback: the tint
|
|
353
|
-
// the image is a loader's answer, and either can arrive first.
|
|
515
|
+
// Applied before the load rather than in its callback: the tint and the blur
|
|
516
|
+
// are props where the image is a loader's answer, and either can arrive first.
|
|
354
517
|
if (RnAppKitView *tinted = viewForTag(tag); tinted != nil) {
|
|
355
518
|
if (tint) {
|
|
356
519
|
const ColorComponents components = colorComponentsFromColor(tint);
|
|
@@ -361,6 +524,7 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
361
524
|
} else {
|
|
362
525
|
[tinted setRnImageTint:nil];
|
|
363
526
|
}
|
|
527
|
+
[tinted setRnImageBlur:blurRadius];
|
|
364
528
|
}
|
|
365
529
|
|
|
366
530
|
// A mutation that changed only layout must not restart the load, or an
|
|
@@ -370,9 +534,10 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
370
534
|
const auto known = imageUris_.find(tag);
|
|
371
535
|
if (known != imageUris_.end() && known->second == uri) {
|
|
372
536
|
if (!uri.empty()) {
|
|
373
|
-
imageLoader_->load(uri, [this, tag, fit](CGImageRef image, const std::string &) {
|
|
537
|
+
imageLoader_->load(uri, [this, tag, fit, uri](CGImageRef image, const std::string &) {
|
|
374
538
|
if (RnAppKitView *target = viewForTag(tag); target != nil) {
|
|
375
539
|
[target setRnImage:image fit:fit];
|
|
540
|
+
applyImageAnimation(imageLoader_.get(), target, uri);
|
|
376
541
|
}
|
|
377
542
|
});
|
|
378
543
|
}
|
|
@@ -383,6 +548,7 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
383
548
|
|
|
384
549
|
if (uri.empty()) {
|
|
385
550
|
[view setRnImage:nullptr fit:fit];
|
|
551
|
+
applyImageAnimation(imageLoader_.get(), view, uri);
|
|
386
552
|
return;
|
|
387
553
|
}
|
|
388
554
|
|
|
@@ -411,12 +577,34 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
411
577
|
emitter->onLoadStart();
|
|
412
578
|
}
|
|
413
579
|
|
|
580
|
+
// `<Image onProgress>`, for an http source: the bytes as they arrive, from
|
|
581
|
+
// the fetching thread. The emitter is looked up *here*, on the main thread,
|
|
582
|
+
// and captured -- `eventEmitterForTag` reads a map only this thread may touch
|
|
583
|
+
// (see core/MountingWalk.h), and a progress tick arrives on another.
|
|
584
|
+
//
|
|
585
|
+
// Which ticks become events is core/ImageProgress.h's: all three hosts gate
|
|
586
|
+
// them the same way, and the arithmetic is testable where the lambda is not.
|
|
587
|
+
// expo-image has no progress event, so its branch reports nothing.
|
|
588
|
+
basalt::ImageProgress reportProgress;
|
|
589
|
+
if (!isExpoImage) {
|
|
590
|
+
if (auto emitter =
|
|
591
|
+
std::dynamic_pointer_cast<const ImageEventEmitter>(eventEmitterForTag(tag))) {
|
|
592
|
+
auto ticker = std::make_shared<basalt::ImageProgressTicker>();
|
|
593
|
+
reportProgress = [emitter, ticker](long long loaded, long long total) {
|
|
594
|
+
if (const auto progress = ticker->tick(loaded, total)) {
|
|
595
|
+
emitter->onProgress(*progress, loaded, total);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
414
601
|
imageLoader_->load(uri, [this, tag, fit, source, isExpoImage](CGImageRef image,
|
|
415
602
|
const std::string &error) {
|
|
416
603
|
// The view may have been deleted while the image was in flight, which is
|
|
417
604
|
// why this looks the tag up again rather than capturing the view.
|
|
418
605
|
if (RnAppKitView *target = viewForTag(tag); target != nil) {
|
|
419
606
|
[target setRnImage:image fit:fit];
|
|
607
|
+
applyImageAnimation(imageLoader_.get(), target, source.uri);
|
|
420
608
|
}
|
|
421
609
|
|
|
422
610
|
if (image == nullptr) {
|
|
@@ -446,12 +634,26 @@ void AppKitMountingManager::applyImage(RnAppKitView *view, const ShadowView &sha
|
|
|
446
634
|
return;
|
|
447
635
|
}
|
|
448
636
|
if (image != nullptr) {
|
|
449
|
-
|
|
637
|
+
// The decoded size, which the props do not carry for a remote image:
|
|
638
|
+
// `onLoad`'s payload is `source.size` times `source.scale`, so a source
|
|
639
|
+
// that arrived as `{uri}` and nothing else reported zero by zero on
|
|
640
|
+
// every host. iOS fills it in the same place and the same way, from the
|
|
641
|
+
// image it has just decoded -- `RCTImageComponentView`'s
|
|
642
|
+
// `didReceiveImage:` assigns `imageSource.size` before emitting.
|
|
643
|
+
//
|
|
644
|
+
// Divided by the scale because the payload multiplies by it: the event
|
|
645
|
+
// reports pixels, and a 2x asset is half as many points.
|
|
646
|
+
facebook::react::ImageSource loaded = source;
|
|
647
|
+
const double scale = loaded.scale > 0 ? loaded.scale : 1.0;
|
|
648
|
+
loaded.size = {static_cast<facebook::react::Float>(CGImageGetWidth(image) / scale),
|
|
649
|
+
static_cast<facebook::react::Float>(CGImageGetHeight(image) / scale)};
|
|
650
|
+
emitter->onLoad(loaded);
|
|
450
651
|
} else {
|
|
451
652
|
emitter->onError(facebook::react::ImageErrorInfo{.error = error});
|
|
452
653
|
}
|
|
453
654
|
emitter->onLoadEnd();
|
|
454
|
-
}
|
|
655
|
+
},
|
|
656
|
+
std::move(reportProgress));
|
|
455
657
|
}
|
|
456
658
|
|
|
457
659
|
void AppKitMountingManager::updateView(RnAppKitView *view, const ShadowView &shadowView) {
|
|
@@ -464,7 +666,7 @@ void AppKitMountingManager::updateView(RnAppKitView *view, const ShadowView &sha
|
|
|
464
666
|
// needed the same swap for the same reason.
|
|
465
667
|
applyTextInput(view, shadowView);
|
|
466
668
|
applyAccessibility(view, shadowView);
|
|
467
|
-
#
|
|
669
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
468
670
|
// After applyProps, which is what puts nativeID on the view -- a canvas
|
|
469
671
|
// registers itself by that id and cannot be attached before it is there. And
|
|
470
672
|
// before applyLayoutMetrics only incidentally: the size it reads comes from the
|
|
@@ -658,7 +860,63 @@ void AppKitMountingManager::applyText(RnAppKitView *view, const ShadowView &shad
|
|
|
658
860
|
// Built through the same function the measurement seam uses, which is what
|
|
659
861
|
// makes the painted lines break where the measured ones did. The width is not
|
|
660
862
|
// baked in -- the view draws at whatever size Yoga gave it.
|
|
661
|
-
|
|
863
|
+
// `adjustsFontSizeToFit` again, against the frame rather than the constraint:
|
|
864
|
+
// the text is painted into the box Yoga assigned, which is the box it has to
|
|
865
|
+
// fit. Measuring and painting run the same search through the same builder,
|
|
866
|
+
// which is what keeps the painted size the measured one.
|
|
867
|
+
// The *content* box, not the frame: a <Text> may carry padding and a border,
|
|
868
|
+
// and Yoga measured the paragraph against the size inside them. See
|
|
869
|
+
// RnAppKitView's rnTextInset.
|
|
870
|
+
const auto &insets = shadowView.layoutMetrics.contentInsets;
|
|
871
|
+
const basalt::FontFit fit =
|
|
872
|
+
basalt::textFitScale(data.attributedString,
|
|
873
|
+
data.paragraphAttributes,
|
|
874
|
+
static_cast<float>(shadowView.layoutMetrics.frame.size.width -
|
|
875
|
+
insets.left - insets.right),
|
|
876
|
+
static_cast<float>(shadowView.layoutMetrics.frame.size.height -
|
|
877
|
+
insets.top - insets.bottom));
|
|
878
|
+
RnTextLayout *layout =
|
|
879
|
+
basalt::buildTextLayout(data.attributedString, data.paragraphAttributes, fit);
|
|
880
|
+
|
|
881
|
+
// The paragraph's text shadow, resolved in core/TextShadows.h: the props are
|
|
882
|
+
// per fragment and one Core Text frame draws them all, so the first fragment
|
|
883
|
+
// that asks for a shadow decides it. The standard deviation crosses unchanged,
|
|
884
|
+
// `CGContextSetShadowWithColor`'s blur being one, where the GTK side doubles
|
|
885
|
+
// it for GSK.
|
|
886
|
+
// The paragraph's writing direction, for the dump: the paragraph style the
|
|
887
|
+
// layout carries is what actually decides it.
|
|
888
|
+
const char *const direction =
|
|
889
|
+
basalt::writingDirectionName(basalt::writingDirection(data.attributedString));
|
|
890
|
+
view.rnWritingDirection = direction != nullptr ? @(direction) : nil;
|
|
891
|
+
// And the edge its text sits against, resolved: `textAlign` is relative in
|
|
892
|
+
// two of its five spellings, so the prop is not the answer. See
|
|
893
|
+
// core/TextAlignments.h.
|
|
894
|
+
view.rnTextAlign = @(basalt::paragraphTextAlignmentName(data.attributedString));
|
|
895
|
+
const char *const verticalAlign =
|
|
896
|
+
basalt::textAlignVerticalName(data.paragraphAttributes.textAlignVertical);
|
|
897
|
+
view.rnTextVerticalAlign = verticalAlign != nullptr ? @(verticalAlign) : nil;
|
|
898
|
+
// The padding and border the paragraph sits inside, which the view draws at
|
|
899
|
+
// and hit-tests against. Yoga has already laid the box out with them; this is
|
|
900
|
+
// what stops the text being drawn underneath its own padding.
|
|
901
|
+
view.rnTextInset = NSEdgeInsetsMake(insets.top, insets.left, insets.bottom, insets.right);
|
|
902
|
+
|
|
903
|
+
// `<Text selectable>`, which `userSelect` also arrives in: React Native's own
|
|
904
|
+
// `Text.js` maps the style onto the prop. From the props rather than the
|
|
905
|
+
// state, unlike the text itself.
|
|
906
|
+
const auto paragraphProps =
|
|
907
|
+
std::dynamic_pointer_cast<const facebook::react::ParagraphProps>(shadowView.props);
|
|
908
|
+
[view setRnTextSelectable:paragraphProps != nullptr && paragraphProps->isSelectable ? YES : NO];
|
|
909
|
+
|
|
910
|
+
if (const auto shadow = basalt::textShadow(data.attributedString)) {
|
|
911
|
+
layout.shadowOffset = CGSizeMake(shadow->dx, shadow->dy);
|
|
912
|
+
layout.shadowStandardDeviation = shadow->standardDeviation;
|
|
913
|
+
layout.shadowColor = [NSColor colorWithSRGBRed:shadow->red
|
|
914
|
+
green:shadow->green
|
|
915
|
+
blue:shadow->blue
|
|
916
|
+
alpha:shadow->alpha];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
[view setRnTextLayout:layout];
|
|
662
920
|
}
|
|
663
921
|
|
|
664
922
|
void AppKitMountingManager::applyProps(RnAppKitView *view, const ShadowView &shadowView) {
|
|
@@ -752,6 +1010,24 @@ void AppKitMountingManager::applyProps(RnAppKitView *view, const ShadowView &sha
|
|
|
752
1010
|
// The app's nativeID, which marks a view for a behaviour React Native has
|
|
753
1011
|
// no prop for -- a drop target today. See core/DragAndDrop.h, and
|
|
754
1012
|
// core/TitleBarRegions.h for the older use of the same idea.
|
|
1013
|
+
// Which view this nativeID names, which is what `accessibilityLabelledBy`
|
|
1014
|
+
// needs: that prop names other views by their nativeID and nothing else here
|
|
1015
|
+
// can look one up. See core/LabelRegistry.h.
|
|
1016
|
+
labels_.setNativeId(shadowView.tag, props->nativeId);
|
|
1017
|
+
// `experimental_accessibilityOrder`, which names children by their nativeID
|
|
1018
|
+
// the way `accessibilityLabelledBy` names labels, and is resolved by the same
|
|
1019
|
+
// registry for the same reason: a view can be named before it mounts.
|
|
1020
|
+
labels_.setAccessibilityOrder(shadowView.tag, props->accessibilityOrder);
|
|
1021
|
+
|
|
1022
|
+
// `testID`, which is not `nativeID`: one is how an app names a view to
|
|
1023
|
+
// itself, the other is how something outside the process finds it.
|
|
1024
|
+
view.rnTestId = props->testId.empty()
|
|
1025
|
+
? nil
|
|
1026
|
+
: [NSString stringWithUTF8String:props->testId.c_str()];
|
|
1027
|
+
|
|
1028
|
+
// `accessibilityViewIsModal`, which keeps VoiceOver inside a <Modal>.
|
|
1029
|
+
view.rnAccessibleModal = props->accessibilityViewIsModal ? YES : NO;
|
|
1030
|
+
|
|
755
1031
|
view.rnNativeId = props->nativeId.empty()
|
|
756
1032
|
? nil
|
|
757
1033
|
: [NSString stringWithUTF8String:props->nativeId.c_str()];
|
|
@@ -774,6 +1050,269 @@ void AppKitMountingManager::applyProps(RnAppKitView *view, const ShadowView &sha
|
|
|
774
1050
|
view.rnPointerEvents = RnAppKitPointerEventsAuto;
|
|
775
1051
|
break;
|
|
776
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
// backgroundImage: linear and radial gradients. The angle or the ending shape,
|
|
1055
|
+
// the box size and CSS's colour-stop fixup are resolved here through
|
|
1056
|
+
// core/Gradients.h, shared with the GTK host, and the view is handed points
|
|
1057
|
+
// and radii and a list of stops.
|
|
1058
|
+
//
|
|
1059
|
+
// One list rather than two, because `background-image` is one list and the
|
|
1060
|
+
// first in it is the one on top: two lists would lose the order between a
|
|
1061
|
+
// radial gradient and a linear one.
|
|
1062
|
+
//
|
|
1063
|
+
// Against this mutation's own frame, and applyProps runs on every update, so a
|
|
1064
|
+
// resized view gets a resized gradient without the view layer knowing anything
|
|
1065
|
+
// about angles or corners.
|
|
1066
|
+
{
|
|
1067
|
+
const auto &metrics = shadowView.layoutMetrics;
|
|
1068
|
+
// The two areas CSS gives a background: the painting area is the border box,
|
|
1069
|
+
// which it is clipped to, and the positioning area is the padding box, which
|
|
1070
|
+
// sizes and positions it. getPaddingFrame's origin is already relative to
|
|
1071
|
+
// the view, being the border widths.
|
|
1072
|
+
const basalt::BackgroundArea painting{
|
|
1073
|
+
0.0F, 0.0F, (float)metrics.frame.size.width, (float)metrics.frame.size.height};
|
|
1074
|
+
const auto paddingFrame = metrics.getPaddingFrame();
|
|
1075
|
+
const basalt::BackgroundArea positioning{(float)paddingFrame.origin.x,
|
|
1076
|
+
(float)paddingFrame.origin.y,
|
|
1077
|
+
(float)paddingFrame.size.width,
|
|
1078
|
+
(float)paddingFrame.size.height};
|
|
1079
|
+
|
|
1080
|
+
std::vector<RnAppKitGradient> gradients;
|
|
1081
|
+
std::vector<std::vector<RnAppKitGradientStop>> stops;
|
|
1082
|
+
gradients.reserve(props->backgroundImage.size());
|
|
1083
|
+
stops.reserve(props->backgroundImage.size());
|
|
1084
|
+
for (size_t index = 0; index < props->backgroundImage.size(); index++) {
|
|
1085
|
+
const auto &image = props->backgroundImage[index];
|
|
1086
|
+
// Each list is indexed modulo its own length, which is CSS's rule for a
|
|
1087
|
+
// list shorter than the image list and is what iOS does.
|
|
1088
|
+
const basalt::BackgroundLayer layer = basalt::resolveBackgroundLayer(
|
|
1089
|
+
positioning,
|
|
1090
|
+
painting,
|
|
1091
|
+
basalt::backgroundSizeAt(props->backgroundSize, index),
|
|
1092
|
+
basalt::backgroundPositionAt(props->backgroundPosition, index),
|
|
1093
|
+
basalt::backgroundRepeatAt(props->backgroundRepeat, index));
|
|
1094
|
+
if (layer.empty()) {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
// The gradient is resolved against the image's size and then offset to
|
|
1098
|
+
// where the image goes: resolving against the view would ignore
|
|
1099
|
+
// `backgroundSize` even with the rectangle right.
|
|
1100
|
+
const auto width = layer.width;
|
|
1101
|
+
const auto height = layer.height;
|
|
1102
|
+
|
|
1103
|
+
RnAppKitGradient converted{};
|
|
1104
|
+
converted.area = CGRectMake(layer.x, layer.y, layer.width, layer.height);
|
|
1105
|
+
converted.tile = CGRectMake(layer.tileX, layer.tileY, layer.tileWidth, layer.tileHeight);
|
|
1106
|
+
converted.repeats = layer.repeats();
|
|
1107
|
+
const std::vector<facebook::react::ColorStop> *colorStops = nullptr;
|
|
1108
|
+
float rayLength = 0.0F;
|
|
1109
|
+
|
|
1110
|
+
if (std::holds_alternative<facebook::react::LinearGradient>(image)) {
|
|
1111
|
+
const auto &gradient = std::get<facebook::react::LinearGradient>(image);
|
|
1112
|
+
const basalt::GradientLine line = basalt::linearGradientLine(gradient, width, height);
|
|
1113
|
+
converted.kind = RnAppKitGradientKindLinear;
|
|
1114
|
+
converted.start = CGPointMake(line.startX + layer.x, line.startY + layer.y);
|
|
1115
|
+
converted.end = CGPointMake(line.endX + layer.x, line.endY + layer.y);
|
|
1116
|
+
colorStops = &gradient.colorStops;
|
|
1117
|
+
rayLength = line.length();
|
|
1118
|
+
} else {
|
|
1119
|
+
const auto &gradient = std::get<facebook::react::RadialGradient>(image);
|
|
1120
|
+
const basalt::GradientEllipse shape =
|
|
1121
|
+
basalt::radialGradientEllipse(gradient, width, height);
|
|
1122
|
+
converted.kind = RnAppKitGradientKindRadial;
|
|
1123
|
+
converted.center = CGPointMake(shape.centerX + layer.x, shape.centerY + layer.y);
|
|
1124
|
+
converted.radiusX = shape.radiusX;
|
|
1125
|
+
converted.radiusY = shape.radiusY;
|
|
1126
|
+
colorStops = &gradient.colorStops;
|
|
1127
|
+
rayLength = shape.rayLength();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const auto resolved = basalt::resolveGradientStops(*colorStops, rayLength);
|
|
1131
|
+
if (resolved.empty()) {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
std::vector<RnAppKitGradientStop> stopList;
|
|
1135
|
+
stopList.reserve(resolved.size());
|
|
1136
|
+
for (const auto &stop : resolved) {
|
|
1137
|
+
RnAppKitGradientStop one{};
|
|
1138
|
+
one.offset = stop.offset;
|
|
1139
|
+
one.color[0] = stop.red;
|
|
1140
|
+
one.color[1] = stop.green;
|
|
1141
|
+
one.color[2] = stop.blue;
|
|
1142
|
+
one.color[3] = stop.alpha;
|
|
1143
|
+
stopList.push_back(one);
|
|
1144
|
+
}
|
|
1145
|
+
// Reserved above, so pushing cannot reallocate and the pointer handed over
|
|
1146
|
+
// below stays good for this block. The view copies what it is given.
|
|
1147
|
+
stops.push_back(std::move(stopList));
|
|
1148
|
+
converted.stops = stops.back().data();
|
|
1149
|
+
converted.stopCount = (NSInteger)stops.back().size();
|
|
1150
|
+
gradients.push_back(converted);
|
|
1151
|
+
}
|
|
1152
|
+
[view setRnGradients:gradients.data() count:(NSInteger)gradients.size()];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// boxShadow. React Native's BoxShadow carries the six CSS fields and the view
|
|
1156
|
+
// layer takes the same six, so nothing is converted here: it decides what a
|
|
1157
|
+
// shadow is made of on this platform, and this decides nothing.
|
|
1158
|
+
//
|
|
1159
|
+
// The list also carries the older iOS shadow props, converted in
|
|
1160
|
+
// core/LegacyShadow.h: one mechanism from here down, so a view with
|
|
1161
|
+
// `shadowOpacity` and a view with `boxShadow` take the same path.
|
|
1162
|
+
{
|
|
1163
|
+
const std::vector<facebook::react::BoxShadow> all = basalt::allShadows(*props);
|
|
1164
|
+
std::vector<RnAppKitBoxShadow> shadows;
|
|
1165
|
+
shadows.reserve(all.size());
|
|
1166
|
+
for (const auto &shadow : all) {
|
|
1167
|
+
RnAppKitBoxShadow converted{};
|
|
1168
|
+
converted.dx = (CGFloat)shadow.offsetX;
|
|
1169
|
+
converted.dy = (CGFloat)shadow.offsetY;
|
|
1170
|
+
converted.blur = (CGFloat)shadow.blurRadius;
|
|
1171
|
+
converted.spread = (CGFloat)shadow.spreadDistance;
|
|
1172
|
+
converted.inset = shadow.inset;
|
|
1173
|
+
if (shadow.color) {
|
|
1174
|
+
const ColorComponents components = colorComponentsFromColor(shadow.color);
|
|
1175
|
+
converted.color[0] = components.red;
|
|
1176
|
+
converted.color[1] = components.green;
|
|
1177
|
+
converted.color[2] = components.blue;
|
|
1178
|
+
converted.color[3] = components.alpha;
|
|
1179
|
+
}
|
|
1180
|
+
shadows.push_back(converted);
|
|
1181
|
+
}
|
|
1182
|
+
[view setRnBoxShadows:shadows.data() count:(NSInteger)shadows.size()];
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// The outline: CSS's, drawn outside the box and taking no layout space, so
|
|
1186
|
+
// nothing about it touches the frame. One width, one colour, one offset and one
|
|
1187
|
+
// style for the whole ring, unlike the border's four of each.
|
|
1188
|
+
{
|
|
1189
|
+
CGFloat colour[4] = {0, 0, 0, 0};
|
|
1190
|
+
if (props->outlineColor) {
|
|
1191
|
+
const ColorComponents components = colorComponentsFromColor(props->outlineColor);
|
|
1192
|
+
colour[0] = components.red;
|
|
1193
|
+
colour[1] = components.green;
|
|
1194
|
+
colour[2] = components.blue;
|
|
1195
|
+
colour[3] = components.alpha;
|
|
1196
|
+
}
|
|
1197
|
+
RnAppKitBorderStyle style = RnAppKitBorderStyleSolid;
|
|
1198
|
+
switch (props->outlineStyle) {
|
|
1199
|
+
case facebook::react::OutlineStyle::Dotted:
|
|
1200
|
+
style = RnAppKitBorderStyleDotted;
|
|
1201
|
+
break;
|
|
1202
|
+
case facebook::react::OutlineStyle::Dashed:
|
|
1203
|
+
style = RnAppKitBorderStyleDashed;
|
|
1204
|
+
break;
|
|
1205
|
+
case facebook::react::OutlineStyle::Solid:
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
[view setRnOutlineWidth:(CGFloat)props->outlineWidth
|
|
1209
|
+
offset:(CGFloat)props->outlineOffset
|
|
1210
|
+
color:colour
|
|
1211
|
+
style:style];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// filter. The arithmetic is core/Filters.h's and is shared with the GTK host:
|
|
1215
|
+
// the list of CSS functions comes to one colour matrix, one blur and one
|
|
1216
|
+
// opacity, and the view layer turns the first two into Core Image filters.
|
|
1217
|
+
//
|
|
1218
|
+
// A dropShadow() in the list is reported rather than applied -- it is the one
|
|
1219
|
+
// function that does not commute with the others. Logged once per view that
|
|
1220
|
+
// asks, because silence here looks like a working filter.
|
|
1221
|
+
{
|
|
1222
|
+
const basalt::ResolvedFilters resolved = basalt::resolveFilters(props->filter);
|
|
1223
|
+
if (resolved.empty()) {
|
|
1224
|
+
[view setRnFilters:nullptr];
|
|
1225
|
+
} else {
|
|
1226
|
+
RnAppKitFilters filters{};
|
|
1227
|
+
filters.hasMatrix = resolved.hasMatrix;
|
|
1228
|
+
for (int i = 0; i < 16; i++) {
|
|
1229
|
+
filters.matrix[i] = resolved.matrix.m[i];
|
|
1230
|
+
}
|
|
1231
|
+
for (int i = 0; i < 4; i++) {
|
|
1232
|
+
filters.offset[i] = resolved.matrix.offset[i];
|
|
1233
|
+
}
|
|
1234
|
+
filters.blurRadius = resolved.blurRadius;
|
|
1235
|
+
filters.opacity = resolved.opacity;
|
|
1236
|
+
|
|
1237
|
+
// `dropShadow()`, which this host draws as the layer's own shadow. The
|
|
1238
|
+
// standard deviation crosses unchanged, `CALayer.shadowRadius` being one,
|
|
1239
|
+
// where the GTK side doubles it for GSK.
|
|
1240
|
+
std::vector<RnAppKitFilterShadow> shadows;
|
|
1241
|
+
shadows.reserve(resolved.dropShadows.size());
|
|
1242
|
+
for (const auto &shadow : resolved.dropShadows) {
|
|
1243
|
+
RnAppKitFilterShadow converted{};
|
|
1244
|
+
converted.dx = (CGFloat)shadow.dx;
|
|
1245
|
+
converted.dy = (CGFloat)shadow.dy;
|
|
1246
|
+
converted.standardDeviation = (CGFloat)shadow.standardDeviation;
|
|
1247
|
+
converted.color[0] = shadow.red;
|
|
1248
|
+
converted.color[1] = shadow.green;
|
|
1249
|
+
converted.color[2] = shadow.blue;
|
|
1250
|
+
converted.color[3] = shadow.alpha;
|
|
1251
|
+
shadows.push_back(converted);
|
|
1252
|
+
}
|
|
1253
|
+
filters.shadows = shadows.data();
|
|
1254
|
+
filters.shadowCount = (NSInteger)shadows.size();
|
|
1255
|
+
// The view copies what it keeps, so the vector may go out of scope.
|
|
1256
|
+
[view setRnFilters:&filters];
|
|
1257
|
+
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// hitSlop, which grows what a press can land on without moving a pixel.
|
|
1262
|
+
// RnAppKitHitTest is the only reader; see -rnHitArea.
|
|
1263
|
+
{
|
|
1264
|
+
const CGFloat insets[4] = {
|
|
1265
|
+
(CGFloat)props->hitSlop.top,
|
|
1266
|
+
(CGFloat)props->hitSlop.right,
|
|
1267
|
+
(CGFloat)props->hitSlop.bottom,
|
|
1268
|
+
(CGFloat)props->hitSlop.left,
|
|
1269
|
+
};
|
|
1270
|
+
[view setRnHitSlop:insets];
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// The `cursor` style property, as a CSS keyword. core/CursorNames.h is shared
|
|
1274
|
+
// with the GTK host, which hands the same keyword to GDK; what macOS has no
|
|
1275
|
+
// cursor for is decided in the view layer, beside the NSCursors.
|
|
1276
|
+
{
|
|
1277
|
+
const char *name = basalt::cursorName(props->cursor);
|
|
1278
|
+
[view setRnCursorName:name != nullptr ? [NSString stringWithUTF8String:name] : nil];
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// `mixBlendMode`, also as a CSS keyword and also shared: Core Image has a
|
|
1282
|
+
// blend-mode filter per CSS mode, so the view layer maps the keyword onto one
|
|
1283
|
+
// and Core Animation does the blending.
|
|
1284
|
+
{
|
|
1285
|
+
const char *name = basalt::blendModeName(props->mixBlendMode);
|
|
1286
|
+
[view setRnBlendModeName:name != nullptr ? [NSString stringWithUTF8String:name] : nil];
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// borderStyle. React Native carries one per side and a stroked path carries
|
|
1290
|
+
// one dash pattern, so the first side that asks for something other than solid
|
|
1291
|
+
// decides the whole outline, exactly as on GTK. A border with different styles
|
|
1292
|
+
// per side is rare enough to be worth that; see backlog/correctness.md.
|
|
1293
|
+
//
|
|
1294
|
+
// From the *resolved* metrics, like the widths and the colours: the raw
|
|
1295
|
+
// `props->borderStyles` is a cascade of optionals with a slot per spelling, and
|
|
1296
|
+
// `borderStyle: 'dashed'` sets the `all` slot rather than the four sides. The
|
|
1297
|
+
// GTK side read the sides and so drew every dashed border solid.
|
|
1298
|
+
{
|
|
1299
|
+
const auto &styles =
|
|
1300
|
+
props->resolveBorderMetrics(shadowView.layoutMetrics).borderStyles;
|
|
1301
|
+
RnAppKitBorderStyle style = RnAppKitBorderStyleSolid;
|
|
1302
|
+
const facebook::react::BorderStyle sides[4] = {
|
|
1303
|
+
styles.top, styles.right, styles.bottom, styles.left};
|
|
1304
|
+
for (const auto &side : sides) {
|
|
1305
|
+
if (side == facebook::react::BorderStyle::Dotted) {
|
|
1306
|
+
style = RnAppKitBorderStyleDotted;
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
if (side == facebook::react::BorderStyle::Dashed) {
|
|
1310
|
+
style = RnAppKitBorderStyleDashed;
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
[view setRnBorderStyle:style];
|
|
1315
|
+
}
|
|
777
1316
|
}
|
|
778
1317
|
|
|
779
1318
|
namespace {
|
|
@@ -821,6 +1360,21 @@ void AppKitMountingManager::applyAccessibility(RnAppKitView *view, const ShadowV
|
|
|
821
1360
|
const std::string role = effectiveRole(shadowView);
|
|
822
1361
|
[view setRnAccessibleRole:role.empty() ? nil : [NSString stringWithUTF8String:role.c_str()]];
|
|
823
1362
|
|
|
1363
|
+
// `accessibilityLiveRegion`: a status message to read out when it changes.
|
|
1364
|
+
// Recorded here and acted on after the transaction, the text being whatever the
|
|
1365
|
+
// region says once every mutation in the batch has landed.
|
|
1366
|
+
liveRegions_.setPoliteness(shadowView.tag, props->accessibilityLiveRegion);
|
|
1367
|
+
if (props->accessibilityLiveRegion == facebook::react::AccessibilityLiveRegion::None) {
|
|
1368
|
+
liveRegionLabels_.erase(shadowView.tag);
|
|
1369
|
+
} else {
|
|
1370
|
+
liveRegionLabels_[shadowView.tag] = props->accessibilityLabel;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// `accessibilityLabelledBy`: other views, named by their nativeID, whose text
|
|
1374
|
+
// names this one. Only recorded here -- the resolution needs every view in the
|
|
1375
|
+
// transaction to have been seen, so it happens once at the end of the mount.
|
|
1376
|
+
labels_.setLabelledBy(shadowView.tag, props->accessibilityLabelledBy.value);
|
|
1377
|
+
|
|
824
1378
|
// A label given in props wins. Falling back to a Paragraph's own text means a
|
|
825
1379
|
// plain <Text> announces itself without the app having to repeat the string
|
|
826
1380
|
// in an accessibilityLabel.
|
|
@@ -898,6 +1452,22 @@ void AppKitMountingManager::applyAccessibility(RnAppKitView *view, const ShadowV
|
|
|
898
1452
|
busy:RnAppKitAccessibleUnset];
|
|
899
1453
|
}
|
|
900
1454
|
|
|
1455
|
+
// After the state, deliberately: AppKit carries both `checked` and
|
|
1456
|
+
// accessibilityValue in one property, and an explicit value is the app's own
|
|
1457
|
+
// word so it wins over one derived from a checkbox. See the setter.
|
|
1458
|
+
//
|
|
1459
|
+
// Each part passed as nil when absent, so a view that gave only `now` is not
|
|
1460
|
+
// announced as sitting at the bottom of a range it never mentioned.
|
|
1461
|
+
{
|
|
1462
|
+
const auto &value = props->accessibilityValue;
|
|
1463
|
+
[view setRnAccessibleValueMin:value.min.has_value() ? @(*value.min) : nil
|
|
1464
|
+
max:value.max.has_value() ? @(*value.max) : nil
|
|
1465
|
+
now:value.now.has_value() ? @(*value.now) : nil
|
|
1466
|
+
text:value.text.has_value()
|
|
1467
|
+
? [NSString stringWithUTF8String:value.text->c_str()]
|
|
1468
|
+
: nil];
|
|
1469
|
+
}
|
|
1470
|
+
|
|
901
1471
|
// Keyboard focus. `accessible` is the signal because React Native's
|
|
902
1472
|
// `focusable` prop never reaches this platform -- see AppKitFocus.h -- and
|
|
903
1473
|
// because it is what <Pressable> sets on everything it renders. A hidden view
|