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.
Files changed (43) hide show
  1. package/native/CMakeLists.txt +16 -2
  2. package/native/appkit/AppKitAppWindow.h +24 -0
  3. package/native/appkit/AppKitImageLoader.h +48 -1
  4. package/native/appkit/AppKitImageLoader.mm +228 -10
  5. package/native/appkit/AppKitMenuBar.mm +58 -0
  6. package/native/appkit/AppKitMountingManager.h +34 -0
  7. package/native/appkit/AppKitMountingManager.mm +582 -12
  8. package/native/appkit/AppKitScrollView.h +48 -0
  9. package/native/appkit/AppKitScrollView.mm +210 -0
  10. package/native/appkit/AppKitSkiaPeer.h +15 -0
  11. package/native/appkit/AppKitTextInput.h +38 -1
  12. package/native/appkit/AppKitTextInput.mm +235 -8
  13. package/native/appkit/AppKitTextPeer.h +60 -0
  14. package/native/appkit/AppKitTextPeer.mm +307 -0
  15. package/native/appkit/AppKitTouchDispatcher.h +56 -0
  16. package/native/appkit/AppKitTouchDispatcher.mm +140 -0
  17. package/native/appkit/AppKitWindowControl.mm +38 -3
  18. package/native/appkit/ComponentRegistryAppKit.mm +28 -3
  19. package/native/appkit/CoreTextLayout.h +64 -2
  20. package/native/appkit/CoreTextLayout.mm +431 -71
  21. package/native/appkit/CoreTextLayoutManager.mm +57 -6
  22. package/native/appkit/FontRegistryCoreText.mm +6 -5
  23. package/native/appkit/PlatformServicesAppKit.mm +101 -20
  24. package/native/appkit/RnAppKitView.h +492 -0
  25. package/native/appkit/RnAppKitView.mm +2266 -23
  26. package/native/appkit/RnTextLayout.h +106 -0
  27. package/native/appkit/RnTextLayout.mm +321 -2
  28. package/native/appkit/main_appkit.mm +174 -3
  29. package/native/tests/test_appkit_accessibility.mm +289 -0
  30. package/native/tests/test_appkit_borders.mm +975 -0
  31. package/native/tests/test_appkit_fonts.mm +203 -0
  32. package/native/tests/test_appkit_image.mm +392 -17
  33. package/native/tests/test_appkit_input.mm +209 -0
  34. package/native/tests/test_appkit_mounting.mm +360 -0
  35. package/native/tests/test_appkit_scrollbar.mm +125 -0
  36. package/native/tests/test_appkit_scrollview.mm +371 -0
  37. package/native/tests/test_appkit_selection.mm +349 -0
  38. package/native/tests/test_appkit_services.mm +51 -1
  39. package/native/tests/test_appkit_text.mm +1628 -0
  40. package/native/tests/test_appkit_textinput.mm +819 -0
  41. package/native/tests/test_appkit_titlebar.mm +43 -0
  42. package/native/tests/test_appkit_view.mm +609 -0
  43. package/package.json +4 -3
@@ -26,6 +26,10 @@
26
26
 
27
27
  #ifdef __OBJC__
28
28
  #import <Cocoa/Cocoa.h>
29
+ // For CALayer, which `boxShadow` is made of: Cocoa declares the view and
30
+ // QuartzCore the layers under it.
31
+ #import <CoreImage/CoreImage.h>
32
+ #import <QuartzCore/QuartzCore.h>
29
33
 
30
34
  NS_ASSUME_NONNULL_BEGIN
31
35
 
@@ -196,6 +200,43 @@ typedef NS_ENUM(NSInteger, RnAppKitImageFit) {
196
200
  // fit.
197
201
  - (void)setRnImage:(nullable CGImageRef)image fit:(RnAppKitImageFit)fit;
198
202
 
203
+ // The frames of an animated image, their delays, and how many times round.
204
+ //
205
+ // `frames` holds CGImageRefs and `delaysMs` the file's own delays, one per
206
+ // frame, unclamped: core/ImageAnimation.h applies the clamp, because that rule
207
+ // is the same on every host. `loopCount` is zero for forever. Fewer than two
208
+ // frames, or nil, is a still image and clears any animation.
209
+ //
210
+ // The view shows frame zero immediately and schedules the rest. Passing the
211
+ // same `frames` array again is a no-op, which is how a layout-only mutation
212
+ // avoids restarting the animation; AppKitImageLoader keeps one array per URI
213
+ // for exactly that.
214
+ - (void)setRnImageFrames:(nullable NSArray *)frames
215
+ delaysMs:(nullable NSArray<NSNumber *> *)delaysMs
216
+ loopCount:(NSUInteger)loopCount;
217
+
218
+ // Starts the timer that shows the rest of the frames, if one is not running.
219
+ //
220
+ // Separate from `setRnImageFrames:` so that nothing animates by itself: the
221
+ // mounting manager starts it because the view is in an app, and the suite never
222
+ // does, which keeps a test about frames deterministic. The GTK view is split
223
+ // the same way, where it matters more -- rendering a widget there spins the
224
+ // main loop, and a self-driven animation would be several frames further on
225
+ // than the test left it.
226
+ - (void)rnStartImageAnimation;
227
+
228
+ // Moves the animation on by `milliseconds` and shows the frame that lands on,
229
+ // answering with how long until the next one, or zero when nothing more is due.
230
+ //
231
+ // The elapsed time is an argument rather than a clock read inside: the host's
232
+ // timer passes the delay it waited, and the suite passes whatever it likes,
233
+ // which is what makes "a hundred and fifty milliseconds later" something a test
234
+ // can say without sleeping.
235
+ - (double)rnAdvanceImageAnimationBy:(double)milliseconds;
236
+
237
+ // Which frame is showing, zero for a still image.
238
+ @property(nonatomic, readonly) NSUInteger rnImageFrameIndex;
239
+
199
240
  // `tintColor`: recolours the image, keeping its alpha, so one silhouette asset
200
241
  // can be drawn in any colour. Pass nil to clear.
201
242
  //
@@ -203,6 +244,15 @@ typedef NS_ENUM(NSInteger, RnAppKitImageFit) {
203
244
  // from a loader, and either can land first.
204
245
  - (void)setRnImageTint:(nullable NSColor *)tint;
205
246
 
247
+ // `blurRadius`: blurs the image and not the view, which is what the prop means
248
+ // -- a blurred photograph behind sharp text is the usual reason to ask for it.
249
+ // Zero or negative is no blur.
250
+ //
251
+ // The radius is a distance in this view's own coordinates rather than in the
252
+ // image's pixels, so the same number is the same picture here as on the GTK
253
+ // side. Also separate from the image for the reason the tint is.
254
+ - (void)setRnImageBlur:(CGFloat)radius;
255
+
206
256
  // The paragraph this view draws, or nil for a view that draws none.
207
257
  //
208
258
  // A view either paints a layer or draws text; nothing here does both, because
@@ -212,6 +262,43 @@ typedef NS_ENUM(NSInteger, RnAppKitImageFit) {
212
262
  // both the mounting manager and the measurement seam go through that, so the
213
263
  // size a view draws at is the size Yoga was told.
214
264
  - (void)setRnTextLayout:(nullable id)layout;
265
+
266
+ // --- Selecting that text ------------------------------------------------------
267
+ //
268
+ // `<Text selectable>`, which `userSelect` also arrives in. The same four
269
+ // questions the GTK side answers in C, in the same order and with the same
270
+ // units: whether this paragraph may be selected, where a point lands in its
271
+ // text, what range is selected, and what that range says. The state machine
272
+ // above them is core/TextSelection.h, shared by all three hosts.
273
+ //
274
+ // Indices are UTF-16 code units, which is what `NSAttributedString` counts;
275
+ // the GTK side counts UTF-8 bytes, and core/TextSelection.h says why the two
276
+ // never have to agree.
277
+ - (void)setRnTextSelectable:(BOOL)selectable;
278
+ - (BOOL)rnTextSelectable;
279
+
280
+ // -1 when this view draws no paragraph. `point` is in the view's own
281
+ // coordinates.
282
+ - (NSInteger)rnTextIndexAtPoint:(CGPoint)point;
283
+
284
+ // How far down its box this view's paragraph is drawn, which is zero for every
285
+ // view that draws no text and for every paragraph that says nothing about
286
+ // `textAlignVertical`.
287
+ //
288
+ // The inline views inside a paragraph are moved by it. React Native measures an
289
+ // attachment against the content box and places it from the top --
290
+ // `ParagraphShadowNode::layout` adds the content insets and nothing else -- so
291
+ // it knows nothing about a box taller than its text, which is what
292
+ // `textAlignVertical` is about. Without this a marker inside a bottom-aligned
293
+ // sentence stayed at the top while the words went to the bottom.
294
+ - (CGFloat)rnTextVerticalOffset;
295
+
296
+ - (void)setRnTextSelectionStart:(NSInteger)start length:(NSInteger)length;
297
+ - (NSInteger)rnTextSelectionStart;
298
+ - (NSInteger)rnTextSelectionLength;
299
+
300
+ // The selected text, or nil when nothing is selected.
301
+ - (nullable NSString *)rnSelectedText;
215
302
  - (void)setRnClipsChildren:(BOOL)clips;
216
303
 
217
304
  // `zIndex`. Reorders painting and hit testing, and never the child list --
@@ -285,6 +372,217 @@ typedef NS_ENUM(NSInteger, RnAppKitImageFit) {
285
372
  - (void)setRnBorderWidths:(nullable const CGFloat *)widths
286
373
  colors:(nullable const CGFloat *)colors;
287
374
 
375
+ // One colour stop of a gradient: an offset from 0 to 1 along the gradient line
376
+ // and the colour there, as RGBA components.
377
+ typedef struct {
378
+ CGFloat offset;
379
+ CGFloat color[4];
380
+ } RnAppKitGradientStop;
381
+
382
+ // Which kind of gradient a record is. CSS's `background-image` is one list that
383
+ // can hold both, and the first in it is the one on top, so they cannot be two
384
+ // lists here without losing the order between them.
385
+ typedef NS_ENUM(NSInteger, RnAppKitGradientKind) {
386
+ RnAppKitGradientKindLinear,
387
+ RnAppKitGradientKindRadial,
388
+ };
389
+
390
+ // One `backgroundImage` gradient, already resolved against this view's size: a
391
+ // linear one's two end points, or a radial one's centre and two radii, in this
392
+ // view's own coordinates.
393
+ //
394
+ // Resolved rather than described, because the resolution is CSS's and is shared
395
+ // with the GTK host in core/Gradients.h -- the angle or the ending shape, the box
396
+ // size and the stop fixup all go into these numbers and the offsets. This layer
397
+ // draws what it is given. The stops are borrowed for the duration of the call.
398
+ typedef struct {
399
+ RnAppKitGradientKind kind;
400
+ // Linear only: the ends of the gradient line.
401
+ CGPoint start;
402
+ CGPoint end;
403
+ // Radial only: the centre of the ending shape and its two radii. A circle is
404
+ // an ellipse whose radii are equal, which is what CSS makes of one.
405
+ CGPoint center;
406
+ CGFloat radiusX;
407
+ CGFloat radiusY;
408
+ // Where the image goes and how it tiles, from `backgroundSize`,
409
+ // `backgroundPosition` and `backgroundRepeat`, resolved in
410
+ // core/BackgroundLayers.h: `area` is the rectangle the gradient fills, in this
411
+ // view's coordinates, and `tile` is what it repeats in -- the same rectangle
412
+ // for `repeat`, wider for `space`, and the painting area's extent on an axis
413
+ // that does not repeat, so one step covers it.
414
+ //
415
+ // The gradient's own geometry above is resolved against `area`'s size and
416
+ // offset to its origin, so this layer draws rather than positions.
417
+ CGRect area;
418
+ CGRect tile;
419
+ bool repeats;
420
+ const RnAppKitGradientStop *stops;
421
+ NSInteger stopCount;
422
+ } RnAppKitGradient;
423
+
424
+ // `backgroundImage`. Replaces whatever was there; pass NULL or 0 for none.
425
+ //
426
+ // Drawn above the background colour, which is a layer property and therefore
427
+ // below anything `drawRect:` paints, and below the image and the text, which is
428
+ // where CSS puts a background image. First in the list is on top, so they are
429
+ // drawn back to front.
430
+ - (void)setRnGradients:(nullable const RnAppKitGradient *)gradients count:(NSInteger)count;
431
+
432
+ // How many are set, for the tests and the tree dump.
433
+ @property(nonatomic, readonly) NSInteger rnGradientCount;
434
+
435
+ // One CSS box shadow, in React Native's own terms: `BoxShadow` carries exactly
436
+ // these fields. The colour is RGBA components in that order, as the border
437
+ // colours arrive, so this header stays free of NSColor for the same reason the
438
+ // rest of it is.
439
+ typedef struct {
440
+ CGFloat dx;
441
+ CGFloat dy;
442
+ CGFloat blur;
443
+ CGFloat spread;
444
+ CGFloat color[4];
445
+ bool inset;
446
+ } RnAppKitBoxShadow;
447
+
448
+ // `boxShadow`. Replaces whatever was there; pass NULL or 0 for none.
449
+ //
450
+ // Order is CSS's: the first shadow in the list is the one on top. Each becomes a
451
+ // CALayer of its own with a shadow path and a mask, which is the only way to
452
+ // paint outside a view -- `drawRect:` is clipped to the bounds and an outset
453
+ // shadow is by definition outside them. It is also what React Native's own iOS
454
+ // half does, so a shadow lands in the same place on both.
455
+ - (void)setRnBoxShadows:(nullable const RnAppKitBoxShadow *)shadows count:(NSInteger)count;
456
+
457
+ // How many are set, for the tests and the tree dump.
458
+ @property(nonatomic, readonly) NSInteger rnBoxShadowCount;
459
+
460
+ // The layers those shadows became, bottom to top, for the tests: each one's
461
+ // `shadowPath`, `shadowRadius` and `mask` are the whole feature, and they are
462
+ // what macOS composites rather than anything this code draws.
463
+ @property(nonatomic, readonly) NSArray<CALayer *> *rnBoxShadowLayers;
464
+
465
+ // `filter`, already resolved: the colour matrix the list comes to, one blur
466
+ // radius and one opacity. See core/Filters.h, which is shared with the GTK host
467
+ // and does the arithmetic the Filter Effects spec specifies.
468
+ //
469
+ // `matrix` is row-major in R, G, B, A order and `offset` is added after it, both
470
+ // in straight colour, which is what CIColorMatrix takes. `blurRadius` is CSS's
471
+ // radius, twice the gaussian's sigma. `opacity` is 1 when the list has no
472
+ // `opacity()`.
473
+ // One `dropShadow()` from a filter list: a shadow of the subtree's alpha rather
474
+ // than of its box, which is what makes it a filter and not a box shadow.
475
+ //
476
+ // `standardDeviation` is the gaussian's, which is what React Native parses and
477
+ // what `CALayer.shadowRadius` takes, so it crosses unchanged. The colour is RGBA
478
+ // components, as the border and shadow colours are.
479
+ typedef struct {
480
+ CGFloat dx;
481
+ CGFloat dy;
482
+ CGFloat standardDeviation;
483
+ CGFloat color[4];
484
+ } RnAppKitFilterShadow;
485
+
486
+ typedef struct {
487
+ bool hasMatrix;
488
+ CGFloat matrix[16];
489
+ CGFloat offset[4];
490
+ CGFloat blurRadius;
491
+ CGFloat opacity;
492
+ // Borrowed for the duration of the call, in the order they were written: the
493
+ // first is the one nearest the content, which CSS applying a filter list
494
+ // left to right is what makes true. A CALayer carries one shadow, so the
495
+ // first is the view's own layer's -- cast from the content's alpha -- and
496
+ // each of the rest is a layer of its own cast from the view's box. The
497
+ // difference shows only on content with holes in it, and
498
+ // backlog/platform-macos.md records it.
499
+ const RnAppKitFilterShadow *shadows;
500
+ NSInteger shadowCount;
501
+ } RnAppKitFilters;
502
+
503
+ // `filter`. Pass NULL for none.
504
+ //
505
+ // Applies to this view and everything inside it, which is what CSS does.
506
+ // `CALayer.filters` is the mechanism and is the reason this is one of the few
507
+ // props macOS does better than React Native's own iOS half: Core Image filters
508
+ // on a layer are public API here and private there, so iOS builds a SwiftUI
509
+ // wrapper and a multiply-blend layer to approximate what this sets directly.
510
+ - (void)setRnFilters:(nullable const RnAppKitFilters *)filters;
511
+
512
+ // The Core Image filters the layer is carrying, for the tests: a shadow or a
513
+ // filter is composited by Core Animation and `renderInContext:` draws neither,
514
+ // so what each filter was given is the observable half here. The GTK side has
515
+ // the render tree and checks a pixel.
516
+ @property(nonatomic, readonly) NSArray<CIFilter *> *rnFilters;
517
+
518
+ // The drop shadow the filter list asked for, as the layer properties it became,
519
+ // or all zeroes when there is none. Public for the tests, for the reason
520
+ // `rnFilters` is: Core Animation composites a shadow and `renderInContext:` does
521
+ // not draw one.
522
+ @property(nonatomic, readonly) RnAppKitFilterShadow rnFilterShadow;
523
+
524
+ // `hitSlop`: how far outside its own box this view answers a press, in the order
525
+ // top, right, bottom, left -- the order CSS names edges and the order the border
526
+ // widths arrive in. Null or all zeroes is no slop.
527
+ //
528
+ // Positive values grow the target. A 44pt row with a 20pt icon in it is the
529
+ // reason the prop exists: the icon is what the eye aims at and the finger, or on
530
+ // a desktop a hurried cursor, misses it.
531
+ //
532
+ // It changes hit testing and nothing else: the view is drawn in its own frame,
533
+ // and `RnAppKitHitTest` is the only reader. That also means it applies to hover
534
+ // and to a drop as well as to a press, which is what iOS does, those all going
535
+ // through one hit test.
536
+ - (void)setRnHitSlop:(nullable const CGFloat *)insets;
537
+
538
+ // The box a press has to land in: the bounds, grown by `hitSlop`. Read by
539
+ // `RnAppKitHitTest` and by the tests, which is why it is here rather than in the
540
+ // implementation.
541
+ @property(nonatomic, readonly) NSRect rnHitArea;
542
+
543
+ // `borderStyle`, for the two values that are not solid.
544
+ typedef NS_ENUM(NSInteger, RnAppKitBorderStyle) {
545
+ RnAppKitBorderStyleSolid = 0,
546
+ RnAppKitBorderStyleDotted,
547
+ RnAppKitBorderStyleDashed,
548
+ };
549
+
550
+ // A dotted or dashed border is drawn differently from a solid one: one stroked
551
+ // path around the view's own rounded rectangle, with a dash pattern scaled to
552
+ // the border width the way every browser does it, since a fixed pattern looks
553
+ // like a hairline on a thick border and like a solid line on a thin one.
554
+ //
555
+ // It replaces the four-edge fill rather than joining it, or the outline would be
556
+ // painted twice and the dashes would sit on a solid line.
557
+ //
558
+ // One style for the whole outline rather than one per side, which is the limit of
559
+ // this and is the limit on the other hosts for the same reason: a stroked path
560
+ // carries one dash pattern, and React Native's per-side `borderStyles` would need
561
+ // four paths with the corners divided between them. The first side that asks for
562
+ // something other than solid decides the outline, and the backlog records that.
563
+ - (void)setRnBorderStyle:(RnAppKitBorderStyle)style;
564
+
565
+ // `outlineWidth`, `outlineColor`, `outlineOffset` and `outlineStyle`.
566
+ //
567
+ // CSS's outline, which is not a border: drawn *outside* the box, no layout space,
568
+ // and `offset` away from the border edge. A focus ring is what it is for.
569
+ //
570
+ // A layer rather than a `drawRect:`, for the reason an outset box shadow is one:
571
+ // `drawRect:` is clipped to the bounds and this is outside them. It follows the
572
+ // same placement rule as the shadows -- inside the view, or in the parent when
573
+ // the view clips its own layer -- so the two props behave alike.
574
+ //
575
+ // The colour is RGBA components, as the border colours arrive. A zero width is no
576
+ // outline.
577
+ - (void)setRnOutlineWidth:(CGFloat)width
578
+ offset:(CGFloat)offset
579
+ color:(nullable const CGFloat *)color
580
+ style:(RnAppKitBorderStyle)style;
581
+
582
+ // The layer that outline became, for the tests: a stroked shape outside the box,
583
+ // which `renderInContext:` will not draw and a `drawRect:` bitmap cannot see.
584
+ @property(nonatomic, readonly, nullable) CAShapeLayer *rnOutlineLayer;
585
+
288
586
  // Fabric's Insert and Remove carry an index into the parent's child list, so
289
587
  // that list has to stay in mutation order.
290
588
  - (void)insertRnChild:(RnAppKitView *)child atIndex:(NSInteger)index;
@@ -324,12 +622,70 @@ typedef NS_ENUM(NSInteger, RnAppKitImageFit) {
324
622
  // Accessible states. Each is a tri-state: unset leaves AppKit's default alone,
325
623
  // which is not the same as setting it false.
326
624
 
625
+ // accessibilityValue: a range and a position in it, or a text form that reads
626
+ // better than the number. nil for a part the app did not give, which is how an
627
+ // element says it has none. See the definition: this shares AppKit's one
628
+ // `accessibilityValue` with the checked state, and an explicit value wins.
629
+ - (void)setRnAccessibleValueMin:(nullable NSNumber *)min
630
+ max:(nullable NSNumber *)max
631
+ now:(nullable NSNumber *)now
632
+ text:(nullable NSString *)text;
633
+
327
634
  - (void)setRnAccessibleStateDisabled:(RnAppKitAccessibleFlag)disabled
328
635
  checked:(RnAppKitAccessibleFlag)checked
329
636
  selected:(RnAppKitAccessibleFlag)selected
330
637
  expanded:(RnAppKitAccessibleFlag)expanded
331
638
  busy:(RnAppKitAccessibleFlag)busy;
332
639
 
640
+ // `accessibilityLabelledBy`, already resolved: the views whose text names this
641
+ // one. Pass nil or an empty array to take the relation off again.
642
+ //
643
+ // The prop names other views by their `nativeID`, and the resolution -- and
644
+ // above all *when* to resolve, Fabric mounting a field before the label that
645
+ // follows it -- is shared with the GTK host in core/LabelRegistry.h. This layer
646
+ // is handed the answer.
647
+ //
648
+ // **AppKit takes one.** `accessibilityTitleUIElement` is a single element where
649
+ // the prop is a list and GTK's relation is a list, so the first is used and the
650
+ // rest are kept only for the tree dump, which reports what was resolved on both
651
+ // hosts. backlog/accessibility.md records that.
652
+ - (void)setRnLabelledBy:(nullable NSArray<RnAppKitView *> *)labels;
653
+
654
+ // `experimental_accessibilityOrder`, resolved: the children this view wants a
655
+ // screen reader to read, in that order. Sets `accessibilityChildren`, which is
656
+ // what VoiceOver walks in place of the view hierarchy.
657
+ - (void)setRnAccessibilityOrder:(nullable NSArray<RnAppKitView *> *)children;
658
+
659
+ // The text of this view and everything inside it, as a screen reader would read
660
+ // it: each paragraph's string in tree order, separated by single spaces.
661
+ //
662
+ // Here for `accessibilityLiveRegion`, which announces a status message when it
663
+ // changes and so needs to know what the message currently says. An accessible
664
+ // label stands in for the text where one was set, which the mounting manager
665
+ // decides before calling this, as the GTK host does.
666
+ @property(nonatomic, readonly) NSString *rnCollectedText;
667
+
668
+ // The text this view draws itself, or nil: the paragraph it holds, and nothing
669
+ // from its children. The recursive half of `rnCollectedText` reads it from each
670
+ // view it walks, which is why it is here rather than in the implementation.
671
+ @property(nonatomic, readonly, nullable) NSString *rnOwnText;
672
+
673
+ // Reads `text` out to VoiceOver now, as `accessibilityLiveRegion` asks. Not a
674
+ // property: AppKit posts an announcement, and whether anybody is listening is
675
+ // VoiceOver's business.
676
+ //
677
+ // `assertive` asks to interrupt what is being read; polite waits for a gap,
678
+ // which is what the two values of the prop mean.
679
+ //
680
+ // The announcement is also remembered, which is the only way to see that it
681
+ // happened: nothing in an automated run is running VoiceOver, so
682
+ // `rnLastAnnouncement` is what the tests read. Deliberately not in the tree dump
683
+ // -- an announcement is an event and the dump is state -- and the host logs it.
684
+ - (void)rnAnnounce:(NSString *)text assertive:(BOOL)assertive;
685
+
686
+ // The last text this view announced, or nil.
687
+ @property(nonatomic, readonly, nullable) NSString *rnLastAnnouncement;
688
+
333
689
  // Hidden from assistive technology, for accessible={false} and
334
690
  // accessibilityElementsHidden.
335
691
  - (void)setRnAccessibleHidden:(BOOL)hidden;
@@ -340,6 +696,120 @@ typedef NS_ENUM(NSInteger, RnAppKitImageFit) {
340
696
  // same idea. Stored and never drawn.
341
697
  @property(nonatomic, copy, nullable) NSString *rnNativeId;
342
698
 
699
+ // The app's `testID`, which is not `nativeID`: one is how an app names a view
700
+ // to itself, the other is how something driving the app from outside finds it.
701
+ // Setting it sets `accessibilityIdentifier`, which is what XCTest, Appium and
702
+ // the Accessibility Inspector all look for, and it is printed in the tree dump
703
+ // the way React Native spelled it.
704
+ @property(nonatomic, copy, nullable) NSString *rnTestId;
705
+
706
+ // `accessibilityViewIsModal`: a screen reader should stay inside this view
707
+ // rather than reading what is behind it. Sets AppKit's `accessibilityModal`,
708
+ // which is the same idea as ARIA's `aria-modal` and UIA's `IsDialog`.
709
+ @property(nonatomic, assign) BOOL rnAccessibleModal;
710
+
711
+ // `writingDirection`, for the tree dump: "ltr", "rtl", "natural", or nil when
712
+ // the app asked for nothing. The direction itself is in the paragraph style the
713
+ // layout carries; this is so the two hosts' dumps compare line by line.
714
+ @property(nonatomic, copy, nullable) NSString *rnWritingDirection;
715
+
716
+ // The edge the paragraph's text sits against, resolved: @"left", @"center",
717
+ // @"right", @"justified", or nil for a view that is not a paragraph. From
718
+ // core/TextAlignments.h, so the three hosts print the same word for the same
719
+ // answer -- `textAlign: 'end'` is one word and two edges.
720
+ @property(nonatomic, copy, nullable) NSString *rnTextAlign;
721
+
722
+ // `textAlignVertical`, for the tree dump: @"auto", @"top", @"center",
723
+ // @"bottom", or nil when the app said nothing. The prop rather than the offset,
724
+ // which core/TextAlignments.h argues.
725
+ @property(nonatomic, copy, nullable) NSString *rnTextVerticalAlign;
726
+
727
+ // Where the paragraph's box ends and its text begins: padding and border,
728
+ // which Fabric adds up into `layoutMetrics.contentInsets`.
729
+ //
730
+ // A <Text> may carry padding of its own, and React Native's own LogBox does:
731
+ // its message is a `<Text style={{paddingHorizontal: 12}}>` while the heading
732
+ // above it is a padded <View> around one, so the two lined up on a phone and
733
+ // did not here. Yoga lays the box out with the padding either way; what was
734
+ // missing is that the paragraph was drawn at the box's corner rather than at
735
+ // the content's.
736
+ //
737
+ // Everything that reads a point against the text uses it: the draw, the
738
+ // selection highlight that is drawn with it, the vertical alignment, and
739
+ // `rnTextIndexAtPoint:`. React Native offsets an inline view's frame by the
740
+ // same insets in `ParagraphShadowNode::layout`, so a paragraph drawn at the
741
+ // corner also disagreed with the views inside it.
742
+ @property(nonatomic) NSEdgeInsets rnTextInset;
743
+
744
+ // What this scroll view's snapping is set to, as core/ScrollSnap.h formats it:
745
+ // a fragment the tree dump prints verbatim, or nil for a view that does not
746
+ // snap. Pushed in by the scroll manager, which is where the props are read.
747
+ @property(nonatomic, copy, nullable) NSString *rnScrollSnap;
748
+
749
+ // `spellCheck` and `autoCorrect`, for the tree dump: @"on", @"off" or nil for a
750
+ // field that said nothing, which is the third state rather than a default. What
751
+ // this host does with them is two properties on the NSTextView a field is or
752
+ // borrows; see AppKitTextPeer.h.
753
+ @property(nonatomic, copy, nullable) NSString *rnSpellCheck;
754
+ @property(nonatomic, copy, nullable) NSString *rnAutoCorrect;
755
+
756
+ // `autoCapitalize` and `keyboardType`, for the tree dump, as React Native
757
+ // spells them. **This host acts on neither**, and says so rather than
758
+ // pretending: macOS has no per-field automatic capitalisation
759
+ // (`NSSpellChecker`'s is the person's system setting) and no software keyboard
760
+ // to choose a layout for. GTK turns both into input hints and an input
761
+ // purpose, so printing the same words is what keeps the two dumps comparable
762
+ // and puts the difference in the support page, where it belongs.
763
+ @property(nonatomic, copy, nullable) NSString *rnAutoCapitalize;
764
+ @property(nonatomic, copy, nullable) NSString *rnKeyboardType;
765
+
766
+ // `caretHidden` and `contextMenuHidden`, for the tree dump. What this host
767
+ // *does* with them is a clear insertion point and a peer that refuses to build
768
+ // a menu; see AppKitTextPeer.h, which also records the half of the second one a
769
+ // single-line field cannot answer. Printed because each is the absence of
770
+ // something -- a blink, a menu -- so nothing else can see that they arrived.
771
+ @property(nonatomic, assign) BOOL rnCaretHidden;
772
+ @property(nonatomic, assign) BOOL rnContextMenuHidden;
773
+
774
+ // The `cursor` style property, as the CSS keyword React Native uses: "pointer",
775
+ // "text", "grab", "ns-resize" and the rest. Nil or empty leaves the cursor to
776
+ // whatever encloses this view, which is what `cursor: 'auto'` means.
777
+ //
778
+ // A keyword rather than an NSCursor because the name is what crosses the seam
779
+ // from the mounting manager, shared with the GTK host through
780
+ // core/CursorNames.h, and because macOS has no cursor for some of them: the
781
+ // mapping and its holes belong on this side. See `rnResolvedCursor`.
782
+ - (void)setRnCursorName:(nullable NSString *)name;
783
+
784
+ // The NSCursor that name resolves to, or nil for a keyword macOS has no cursor
785
+ // for -- "wait", "help", "move", "progress", "cell", "all-scroll". Nil means
786
+ // this view installs no cursor rect at all, so the cursor is inherited rather
787
+ // than forced back to an arrow.
788
+ //
789
+ // Public for the tests, which assert the mapping, and read by nothing else: the
790
+ // cursor itself is applied through `resetCursorRects`, as AppKit expects.
791
+ @property(nonatomic, readonly, nullable) NSCursor *rnResolvedCursor;
792
+
793
+ // `mixBlendMode`, as the CSS keyword, shared with the GTK host through
794
+ // core/BlendModes.h: "multiply", "screen", "color-dodge" and the rest. Nil or
795
+ // empty is `normal`, meaning ordinary source-over compositing.
796
+ //
797
+ // Core Animation does the whole feature in one property on macOS:
798
+ // `CALayer.compositingFilter` takes a Core Image blend-mode filter and blends
799
+ // the layer with what is already beneath it. The keyword maps onto one
800
+ // `CI<Name>BlendMode` per CSS mode, including `plus-lighter`, which GSK has no
801
+ // node for.
802
+ - (void)setRnBlendModeName:(nullable NSString *)name;
803
+
804
+ // The keyword that was set, or nil: what the app asked for.
805
+ @property(nonatomic, readonly, nullable) NSString *rnBlendModeName;
806
+
807
+ // The name of the Core Image filter that keyword resolved to, or nil. Public for
808
+ // the tests, which assert the mapping and that Core Image has each filter: a
809
+ // compositing filter cannot be seen in a snapshot, because `renderInContext:`
810
+ // composites nothing.
811
+ @property(nonatomic, readonly, nullable) NSString *rnBlendFilterName;
812
+
343
813
  // Set on the surface root. See RnAppKitInputHandler.
344
814
  @property(nonatomic, weak, nullable) id<RnAppKitInputHandler> rnInputHandler;
345
815
 
@@ -404,6 +874,28 @@ typedef NS_ENUM(NSInteger, RnAppKitControlKind) {
404
874
  // job is to have one covering it. That view is not an RnAppKitView, so it stays
405
875
  // out of the paint order, out of hit testing and out of `describeTree`'s
406
876
  // children -- the numbers are reported on the ScrollView's own line instead.
877
+ // `indicatorStyle`, arriving as the colour `core/ScrollIndicator.h` resolved it
878
+ // to rather than as the enum: the mapping is the same on all three desktops and
879
+ // belongs in one place, and this layer knows nothing about React Native's
880
+ // vocabulary.
881
+ - (void)setRnScrollIndicatorColourRed:(CGFloat)red
882
+ green:(CGFloat)green
883
+ blue:(CGFloat)blue
884
+ alpha:(CGFloat)alpha;
885
+
886
+ // The hairline around the thumb, which `indicatorStyle: 'default'` has and the
887
+ // other two styles do not: iOS's default is black with a white border, and
888
+ // that border is the whole difference between `default` and `black`. A
889
+ // transparent colour means none. Resolved by `core/ScrollIndicator.h`, like
890
+ // the fill, so the three hosts cannot drift.
891
+ - (void)setRnScrollIndicatorBorderColourRed:(CGFloat)red
892
+ green:(CGFloat)green
893
+ blue:(CGFloat)blue
894
+ alpha:(CGFloat)alpha;
895
+ // What that colour is now, for the test suite: the thumb is drawn into a layer
896
+ // a test can rasterise, but reading the colour back is the cheaper assertion.
897
+ - (NSColor *)rnScrollIndicatorColour;
898
+
407
899
  - (void)setRnScrollIndicatorVerticalOffset:(CGFloat)verticalOffset
408
900
  verticalLength:(CGFloat)verticalLength
409
901
  horizontalOffset:(CGFloat)horizontalOffset