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
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
#import "AppKitTextInput.h"
|
|
2
2
|
|
|
3
|
+
#include "TextChecking.h"
|
|
4
|
+
|
|
5
|
+
#include "FontScaling.h"
|
|
6
|
+
|
|
3
7
|
#import "AppKitTextPeer.h"
|
|
4
8
|
|
|
5
9
|
#import "CoreTextLayout.h"
|
|
6
10
|
|
|
7
11
|
#include <react/renderer/components/iostextinput/TextInputProps.h>
|
|
8
12
|
|
|
13
|
+
#include <glog/logging.h>
|
|
14
|
+
|
|
9
15
|
#include <algorithm>
|
|
10
16
|
#include <cmath>
|
|
11
17
|
#include <string_view>
|
|
12
18
|
|
|
13
19
|
// The bridge between AppKit's delegate protocol and the C++ manager, for the
|
|
20
|
+
// A colour the app asked for, or nil when it asked for nothing.
|
|
21
|
+
//
|
|
22
|
+
// nil is the whole point: `SharedColor`'s unset value is zero, so a conversion
|
|
23
|
+
// that always answered a colour would make "the app said nothing" and "the app
|
|
24
|
+
// said transparent black" the same request, and every caret would come out
|
|
25
|
+
// black. Explicitly sRGB, for the reason `toColor` in CoreTextLayout.mm is: a
|
|
26
|
+
// device colour space shifts every colour on a wide-gamut display, and a field
|
|
27
|
+
// that does not match Linux is the bug this project is least able to afford.
|
|
28
|
+
static NSColor *_Nullable RnColourOrNil(facebook::react::SharedColor colour) {
|
|
29
|
+
if (!colour) {
|
|
30
|
+
return nil;
|
|
31
|
+
}
|
|
32
|
+
const auto components = facebook::react::colorComponentsFromColor(colour);
|
|
33
|
+
return [NSColor colorWithSRGBRed:components.red
|
|
34
|
+
green:components.green
|
|
35
|
+
blue:components.blue
|
|
36
|
+
alpha:components.alpha];
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
// same reason the touch dispatcher and the scroll manager have one.
|
|
15
40
|
@interface RnAppKitTextInputDelegate
|
|
16
41
|
: NSObject <NSTextFieldDelegate, NSTextViewDelegate, RnAppKitTextPeerOwner>
|
|
@@ -127,8 +152,23 @@ static NSInteger RnTagOf(id object) {
|
|
|
127
152
|
doCommandBySelector:(SEL)selector {
|
|
128
153
|
(void)textView;
|
|
129
154
|
if (selector == @selector(insertNewline:) && _manager != nullptr) {
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
// YES means "used up", which is what stops AppKit inserting a newline --
|
|
156
|
+
// and for `submitBehavior: 'newline'` on a single-line field there is
|
|
157
|
+
// nothing to insert anyway, so either answer looks the same.
|
|
158
|
+
return _manager->handleSubmit(static_cast<facebook::react::Tag>(RnTagOf(control))) ? YES
|
|
159
|
+
: NO;
|
|
160
|
+
}
|
|
161
|
+
return NO;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The same key on the multiline peer, which is an NSTextView and posts through
|
|
165
|
+
// NSTextViewDelegate rather than NSControl's. Without this a `submitBehavior`
|
|
166
|
+
// of `submit` on a multiline field would insert a newline *and* report
|
|
167
|
+
// nothing: the field's own insertion is what has to be refused.
|
|
168
|
+
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)selector {
|
|
169
|
+
if (selector == @selector(insertNewline:) && _manager != nullptr) {
|
|
170
|
+
return _manager->handleSubmit(static_cast<facebook::react::Tag>(RnTagOf(textView))) ? YES
|
|
171
|
+
: NO;
|
|
132
172
|
}
|
|
133
173
|
return NO;
|
|
134
174
|
}
|
|
@@ -254,8 +294,10 @@ void AppKitTextInputManager::update(RnAppKitView *view, const ShadowView &shadow
|
|
|
254
294
|
// text on dark. buildTextAttributes is what the style becomes, and it was
|
|
255
295
|
// written for the text layer with this as its eventual second caller.
|
|
256
296
|
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
297
|
+
// 1, and not the text scale, although this is the argument React Native leaves
|
|
298
|
+
// for one: buildTextAttributes applies the scale itself through
|
|
299
|
+
// core/FontScaling.h, so passing it here would square it. The field still
|
|
300
|
+
// scales, and still stops when `allowFontScaling={false}`.
|
|
259
301
|
NSDictionary<NSAttributedStringKey, id> *attributes =
|
|
260
302
|
buildTextAttributes(props->getEffectiveTextAttributes(1.0F));
|
|
261
303
|
NSParagraphStyle *paragraph = attributes[NSParagraphStyleAttributeName];
|
|
@@ -294,6 +336,43 @@ void AppKitTextInputManager::update(RnAppKitView *view, const ShadowView &shadow
|
|
|
294
336
|
}
|
|
295
337
|
}
|
|
296
338
|
|
|
339
|
+
// The three colours React Native has for a field, which this host read none
|
|
340
|
+
// of until 2026-10-10: the placeholder, the selection and the caret.
|
|
341
|
+
//
|
|
342
|
+
// Each crosses as nil when the app did not ask, which is the distinction that
|
|
343
|
+
// matters: `SharedColor`'s unset value is zero, so passing it through
|
|
344
|
+
// unconditionally would paint every caret black and every selection
|
|
345
|
+
// transparent. nil leaves AppKit's own colour alone.
|
|
346
|
+
//
|
|
347
|
+
// `cursorColor` falls back to `selectionColor`, which is React Native's
|
|
348
|
+
// documented contract rather than an invention here: `selectionColor` is "the
|
|
349
|
+
// highlight, selection handle and cursor color of the text input", and
|
|
350
|
+
// `cursorColor` exists to override the caret on its own. The GTK host reads
|
|
351
|
+
// the pair the same way, and on iOS both are the view's tintColor.
|
|
352
|
+
NSColor *const selectionColour =
|
|
353
|
+
RnColourOrNil(props->selectionColor);
|
|
354
|
+
NSColor *caretColour =
|
|
355
|
+
props->cursorColor ? RnColourOrNil(props->cursorColor) : selectionColour;
|
|
356
|
+
|
|
357
|
+
// `caretHidden`, through the same channel and winning over both of the above:
|
|
358
|
+
// a caret the app asked to hide is hidden whatever colour it also asked for.
|
|
359
|
+
// Clear rather than nil, nil meaning "AppKit's own" -- which is a visible
|
|
360
|
+
// caret. An NSTextView draws its insertion point as a filled rectangle, so a
|
|
361
|
+
// clear one is drawn and invisible.
|
|
362
|
+
if (props->traits.caretHidden) {
|
|
363
|
+
caretColour = NSColor.clearColor;
|
|
364
|
+
}
|
|
365
|
+
RnPeerSetEditorColours(entry.field, selectionColour, caretColour);
|
|
366
|
+
|
|
367
|
+
// `clearTextOnFocus` and `selectTextOnFocus`, remembered for the moment focus
|
|
368
|
+
// arrives -- which is long after the props do. See `handleFocus`.
|
|
369
|
+
entry.clearTextOnFocus = props->traits.clearTextOnFocus;
|
|
370
|
+
entry.selectTextOnFocus = props->traits.selectTextOnFocus;
|
|
371
|
+
|
|
372
|
+
// `submitBehavior`, resolved by React Native rather than here: `Default` is
|
|
373
|
+
// `newline` for a multiline field and `blurAndSubmit` for a single-line one.
|
|
374
|
+
entry.submitBehavior = props->getNonDefaultSubmitBehavior();
|
|
375
|
+
|
|
297
376
|
if (props->placeholder.empty()) {
|
|
298
377
|
RnPeerSetPlaceholder(entry.field, nil);
|
|
299
378
|
} else {
|
|
@@ -302,17 +381,64 @@ void AppKitTextInputManager::update(RnAppKitView *view, const ShadowView &shadow
|
|
|
302
381
|
placeholder = @"";
|
|
303
382
|
}
|
|
304
383
|
// Styled like the text, so a placeholder in a 20pt field is not 13pt.
|
|
384
|
+
//
|
|
385
|
+
// `placeholderTextColor` when the app asked, and the system's placeholder
|
|
386
|
+
// grey otherwise. This host painted the grey unconditionally until
|
|
387
|
+
// 2026-10-10 and the backlog recorded the prop as done, which is what the
|
|
388
|
+
// support page's per-row audit caught: an app asking for a red placeholder
|
|
389
|
+
// got grey.
|
|
305
390
|
NSMutableDictionary *placeholderAttributes = [attributes mutableCopy];
|
|
306
|
-
|
|
391
|
+
NSColor *const asked = RnColourOrNil(props->placeholderTextColor);
|
|
392
|
+
placeholderAttributes[NSForegroundColorAttributeName] =
|
|
393
|
+
asked != nil ? asked : NSColor.placeholderTextColor;
|
|
307
394
|
RnPeerSetPlaceholder(entry.field,
|
|
308
395
|
[[NSAttributedString alloc] initWithString:placeholder
|
|
309
396
|
attributes:placeholderAttributes]);
|
|
310
397
|
}
|
|
311
398
|
|
|
399
|
+
// `contextMenuHidden`, which the peers answer from `-menuForEvent:`.
|
|
400
|
+
RnPeerSetContextMenuHidden(entry.field, props->traits.contextMenuHidden);
|
|
401
|
+
|
|
312
402
|
// `editable` is the prop; `readOnly` is the newer spelling of its inverse,
|
|
313
403
|
// and React Native honours both.
|
|
314
404
|
RnPeerSetEditable(entry.field, props->traits.editable && !props->readOnly);
|
|
315
405
|
|
|
406
|
+
// `spellCheck` and `autoCorrect`, resolved in core/TextChecking.h so that the
|
|
407
|
+
// two hosts agree that unset is not false. AppKit has both, on the NSTextView
|
|
408
|
+
// a field is or borrows; GTK has only the first, as an input hint.
|
|
409
|
+
const auto checking = [](std::optional<bool> asked) {
|
|
410
|
+
switch (basalt::textCheckingFlag(asked)) {
|
|
411
|
+
case basalt::TextCheckingFlag::On:
|
|
412
|
+
return RnTextCheckingOn;
|
|
413
|
+
case basalt::TextCheckingFlag::Off:
|
|
414
|
+
return RnTextCheckingOff;
|
|
415
|
+
case basalt::TextCheckingFlag::Unset:
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
return RnTextCheckingUnset;
|
|
419
|
+
};
|
|
420
|
+
RnPeerSetTextChecking(entry.field,
|
|
421
|
+
checking(props->traits.spellCheck),
|
|
422
|
+
checking(props->traits.autoCorrect));
|
|
423
|
+
// And both in the dump, as the app asked for them, in the words GTK prints.
|
|
424
|
+
const auto name = [](std::optional<bool> asked) -> NSString * {
|
|
425
|
+
const char *word = basalt::textCheckingName(basalt::textCheckingFlag(asked));
|
|
426
|
+
return word != nullptr ? @(word) : nil;
|
|
427
|
+
};
|
|
428
|
+
entry.view.rnSpellCheck = name(props->traits.spellCheck);
|
|
429
|
+
entry.view.rnAutoCorrect = name(props->traits.autoCorrect);
|
|
430
|
+
|
|
431
|
+
// `autoCapitalize` and `keyboardType`, reported and not acted on: this
|
|
432
|
+
// platform has no per-field automatic capitalisation and no software
|
|
433
|
+
// keyboard. backlog/platform-macos.md says so with the calls that do not
|
|
434
|
+
// exist, and the GTK host honours both.
|
|
435
|
+
entry.view.rnAutoCapitalize = @(basalt::autoCapitalizeName(props->traits.autocapitalizationType));
|
|
436
|
+
entry.view.rnKeyboardType = @(basalt::keyboardTypeName(props->traits.keyboardType));
|
|
437
|
+
// And the two hiding props, for the dump: each is the absence of something,
|
|
438
|
+
// so the dump is the only place anything can see that they arrived.
|
|
439
|
+
entry.view.rnCaretHidden = props->traits.caretHidden;
|
|
440
|
+
entry.view.rnContextMenuHidden = props->traits.contextMenuHidden;
|
|
441
|
+
|
|
316
442
|
// Zero means no limit, and so does the absurd default React Native uses when
|
|
317
443
|
// the prop is absent.
|
|
318
444
|
RnPeerSetMaxLength(entry.field,
|
|
@@ -321,7 +447,65 @@ void AppKitTextInputManager::update(RnAppKitView *view, const ShadowView &shadow
|
|
|
321
447
|
: 0);
|
|
322
448
|
|
|
323
449
|
if (inserted && props->autoFocus) {
|
|
324
|
-
|
|
450
|
+
// Recorded, not performed: see flushAutoFocus in the header for why here is
|
|
451
|
+
// too early. `inserted` is the only moment the prop can be acted on at all,
|
|
452
|
+
// since it is the only moment this manager can tell a new field from an
|
|
453
|
+
// updated one, so the tag is kept and the focusing happens later.
|
|
454
|
+
pendingAutoFocus_.push_back(tag);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
void AppKitTextInputManager::flushAutoFocus() {
|
|
459
|
+
if (pendingAutoFocus_.empty()) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Taken by value and cleared first, so a field that has since been removed, or
|
|
463
|
+
// one whose window is still not there, does not leave the list growing.
|
|
464
|
+
const std::vector<Tag> pending = std::move(pendingAutoFocus_);
|
|
465
|
+
pendingAutoFocus_.clear();
|
|
466
|
+
|
|
467
|
+
for (const Tag tag : pending) {
|
|
468
|
+
const auto it = entries_.find(tag);
|
|
469
|
+
if (it == entries_.end()) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Not silently. `makeFirstResponder:` answers whether it took, messaging a
|
|
473
|
+
// nil window is a no-op rather than an error, and this used to discard both:
|
|
474
|
+
// an autoFocus that did nothing looked exactly like one that worked. The
|
|
475
|
+
// window is non-nil on every path there is today, a view being in the
|
|
476
|
+
// window's hierarchy from the moment the surface root is set, so these are
|
|
477
|
+
// claims about what is believed rather than cases anyone has seen.
|
|
478
|
+
NSWindow *const window = it->second.view.window;
|
|
479
|
+
if (window == nil) {
|
|
480
|
+
LOG(WARNING) << "autoFocus: tag " << tag << " is not in a window yet";
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (![window makeFirstResponder:it->second.field]) {
|
|
484
|
+
LOG(WARNING) << "autoFocus: tag " << tag << " refused first responder";
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// A field that asked to select its text on focus keeps what
|
|
489
|
+
// `becomeFirstResponder` gave it, which is the whole selection: the collapse
|
|
490
|
+
// below is what `selectTextOnFocus` is the absence of.
|
|
491
|
+
if (it->second.selectTextOnFocus) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// And then collapse the selection, which is the whole of the bug GTK had.
|
|
496
|
+
// `NSTextField` installs the window's shared field editor on becoming first
|
|
497
|
+
// responder and selects all of its text, so autoFocus on a field with a
|
|
498
|
+
// `defaultValue` armed the next keystroke to replace it. The repository had
|
|
499
|
+
// already written the symptom down without recognising it: the paste role's
|
|
500
|
+
// scenario says "both hosts here replace", and a paste replaces what is
|
|
501
|
+
// selected.
|
|
502
|
+
//
|
|
503
|
+
// A caret at the end is what focusing a field is meant to leave, and what
|
|
504
|
+
// `gtk_text_grab_focus_without_selecting` gives on the other host.
|
|
505
|
+
// `NSTextView` keeps its own collapsed selection and needs none of this;
|
|
506
|
+
// setting it anyway costs nothing and keeps the two from drifting.
|
|
507
|
+
const NSUInteger length = RnPeerText(it->second.field).length;
|
|
508
|
+
RnPeerSetSelection(it->second.field, NSMakeRange(length, 0));
|
|
325
509
|
}
|
|
326
510
|
}
|
|
327
511
|
|
|
@@ -409,14 +593,35 @@ void AppKitTextInputManager::handleChanged(Tag tag) {
|
|
|
409
593
|
}
|
|
410
594
|
}
|
|
411
595
|
|
|
412
|
-
|
|
596
|
+
bool AppKitTextInputManager::handleSubmit(Tag tag) {
|
|
413
597
|
Entry *entry = entryFor(tag);
|
|
414
598
|
if (entry == nullptr) {
|
|
415
|
-
return;
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
// **`submitBehavior` decides what Return does**, already resolved to one of
|
|
602
|
+
// the three: `newline` inserts one, `submit` reports a submit, and
|
|
603
|
+
// `blurAndSubmit` reports one and gives up focus. React Native's default is
|
|
604
|
+
// the last for a single-line field and the first for a multiline one, so a
|
|
605
|
+
// field that never set the prop behaves as it does on iOS -- where this host
|
|
606
|
+
// used to keep focus whatever the prop said, because an NSTextField does.
|
|
607
|
+
if (entry->submitBehavior == facebook::react::SubmitBehavior::Newline) {
|
|
608
|
+
// Not used up: a multiline peer inserts the newline, and a single-line one
|
|
609
|
+
// has nowhere to put it and does nothing.
|
|
610
|
+
return false;
|
|
416
611
|
}
|
|
417
612
|
if (const auto emitter = emitterFor(tag)) {
|
|
418
613
|
emitter->onSubmitEditing(metricsFor(*entry));
|
|
419
614
|
}
|
|
615
|
+
if (entry->submitBehavior == facebook::react::SubmitBehavior::BlurAndSubmit) {
|
|
616
|
+
// Handing focus back to the window is the closest thing AppKit has to
|
|
617
|
+
// "unfocus this", which the `blur` command says at more length. It ends the
|
|
618
|
+
// edit, so the delegate reports a blur and JavaScript sees onBlur.
|
|
619
|
+
NSWindow *window = entry->view.window;
|
|
620
|
+
if (window != nil && window.firstResponder != window) {
|
|
621
|
+
[window makeFirstResponder:window];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return true;
|
|
420
625
|
}
|
|
421
626
|
|
|
422
627
|
void AppKitTextInputManager::handleFocus(Tag tag) {
|
|
@@ -427,6 +632,28 @@ void AppKitTextInputManager::handleFocus(Tag tag) {
|
|
|
427
632
|
if (const auto emitter = emitterFor(tag)) {
|
|
428
633
|
emitter->onFocus(metricsFor(*entry));
|
|
429
634
|
}
|
|
635
|
+
|
|
636
|
+
// `clearTextOnFocus` before `selectTextOnFocus`, which is the only order that
|
|
637
|
+
// means anything: selecting what has just been cleared is selecting nothing,
|
|
638
|
+
// and clearing what has just been selected throws the selection away. Both
|
|
639
|
+
// hosts do them in this order for that reason.
|
|
640
|
+
if (entry->clearTextOnFocus && RnPeerText(entry->field).length > 0) {
|
|
641
|
+
// Reported, not applied silently. A controlled field whose text was cleared
|
|
642
|
+
// behind JavaScript's back is a field that puts the old value back on the
|
|
643
|
+
// next unrelated render, with the user having done nothing -- so the clear
|
|
644
|
+
// goes through the same path a keystroke does.
|
|
645
|
+
RnPeerSetText(entry->field, @"");
|
|
646
|
+
handleChanged(tag);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (entry->selectTextOnFocus) {
|
|
650
|
+
// An NSTextField selects everything on becoming first responder, and this
|
|
651
|
+
// host collapses that by hand -- see `flushAutoFocus`, where the prop is
|
|
652
|
+
// the reason not to. An NSTextView does not, so the selection is made here
|
|
653
|
+
// for both and neither depends on which peer it is.
|
|
654
|
+
RnPeerSetSelection(entry->field,
|
|
655
|
+
NSMakeRange(0, RnPeerText(entry->field).length));
|
|
656
|
+
}
|
|
430
657
|
}
|
|
431
658
|
|
|
432
659
|
bool AppKitTextInputManager::handleKeyDown(void *event) {
|
|
@@ -46,6 +46,66 @@ NSRange RnPeerSelection(NSView *_Nullable peer);
|
|
|
46
46
|
void RnPeerSetSelection(NSView *_Nullable peer, NSRange range);
|
|
47
47
|
|
|
48
48
|
void RnPeerSetEditable(NSView *_Nullable peer, BOOL editable);
|
|
49
|
+
|
|
50
|
+
// `contextMenuHidden`: whether a right-click or a Control-click opens the
|
|
51
|
+
// peer's own editing menu.
|
|
52
|
+
//
|
|
53
|
+
// AppKit builds that menu on demand through `-menuForEvent:`, so the peers
|
|
54
|
+
// refuse there rather than having a menu taken away from them -- which is the
|
|
55
|
+
// one place that covers both gestures, and leaves the menu intact for a field
|
|
56
|
+
// that stops asking.
|
|
57
|
+
void RnPeerSetContextMenuHidden(NSView *_Nullable peer, BOOL hidden);
|
|
58
|
+
BOOL RnPeerContextMenuHidden(NSView *_Nullable peer);
|
|
59
|
+
|
|
60
|
+
// `spellCheck` and `autoCorrect`, as three states: see core/TextChecking.h for
|
|
61
|
+
// why unset is not false. Mirrored here rather than taken from that header
|
|
62
|
+
// because this one is plain Objective-C and its callers should stay that way.
|
|
63
|
+
typedef NS_ENUM(NSInteger, RnTextChecking) {
|
|
64
|
+
RnTextCheckingUnset,
|
|
65
|
+
RnTextCheckingOn,
|
|
66
|
+
RnTextCheckingOff,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Applies both, and remembers them.
|
|
70
|
+
//
|
|
71
|
+
// Remembering is the point: the two properties live on an NSTextView, and a
|
|
72
|
+
// single-line field is an NSTextField which *borrows* one -- its field editor,
|
|
73
|
+
// which exists only while it is focused. So a field asked before it is focused
|
|
74
|
+
// has nowhere to put them, and the peer applies them again when the editor
|
|
75
|
+
// appears.
|
|
76
|
+
void RnPeerSetTextChecking(NSView *_Nullable peer,
|
|
77
|
+
RnTextChecking spellCheck,
|
|
78
|
+
RnTextChecking autoCorrect);
|
|
79
|
+
|
|
80
|
+
// What the peer was last asked for, which is not always what AppKit did with
|
|
81
|
+
// it: macOS gates continuous spell checking on a user-wide setting
|
|
82
|
+
// (`NSAllowContinuousSpellChecking`), and refuses a per-view request when the
|
|
83
|
+
// person has turned the feature off for everything. So the request is what a
|
|
84
|
+
// test can assert on any machine, and AppKit's answer is asserted only where
|
|
85
|
+
// the machine allows it.
|
|
86
|
+
RnTextChecking RnPeerSpellCheck(NSView *_Nullable peer);
|
|
87
|
+
RnTextChecking RnPeerAutoCorrect(NSView *_Nullable peer);
|
|
88
|
+
|
|
89
|
+
// `selectionColor` and `cursorColor`, which live on the same NSTextView the
|
|
90
|
+
// spell-checking flags do and are remembered for the same reason: a single-line
|
|
91
|
+
// field borrows the window's field editor and only has one while it is focused.
|
|
92
|
+
// So these are stored and applied again in `becomeFirstResponder`.
|
|
93
|
+
//
|
|
94
|
+
// Null means "the app did not ask", which leaves AppKit's own colour alone --
|
|
95
|
+
// the system accent for a selection and the text colour for a caret. That is a
|
|
96
|
+
// third state rather than a default, exactly as it is for the checking flags:
|
|
97
|
+
// `SharedColor`'s unset value is zero, and passing that through would paint
|
|
98
|
+
// every caret black.
|
|
99
|
+
//
|
|
100
|
+
// The caret is `insertionPointColor` and the selection is the background colour
|
|
101
|
+
// inside `selectedTextAttributes`, which is a dictionary this replaces one key
|
|
102
|
+
// of rather than wholesale: the rest of it is AppKit's, including whether the
|
|
103
|
+
// selection dims when the window loses focus.
|
|
104
|
+
void RnPeerSetEditorColours(NSView *_Nullable peer,
|
|
105
|
+
NSColor *_Nullable selection,
|
|
106
|
+
NSColor *_Nullable caret);
|
|
107
|
+
NSColor *_Nullable RnPeerSelectionColour(NSView *_Nullable peer);
|
|
108
|
+
NSColor *_Nullable RnPeerCaretColour(NSView *_Nullable peer);
|
|
49
109
|
void RnPeerSetTextStyle(NSView *_Nullable peer, NSFont *_Nullable font,
|
|
50
110
|
NSColor *_Nullable colour, NSTextAlignment alignment);
|
|
51
111
|
|