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