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
|
@@ -38,13 +38,16 @@
|
|
|
38
38
|
#include "ScrollBounds.h"
|
|
39
39
|
#include "ScrollIndicator.h"
|
|
40
40
|
#include "ScrollSnap.h"
|
|
41
|
+
#include "ScrollVisiblePosition.h"
|
|
41
42
|
|
|
42
43
|
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
|
|
43
44
|
#include <react/renderer/core/EventEmitter.h>
|
|
44
45
|
#include <react/renderer/mounting/ShadowView.h>
|
|
45
46
|
|
|
46
47
|
#include <functional>
|
|
48
|
+
#include <optional>
|
|
47
49
|
#include <unordered_map>
|
|
50
|
+
#include <vector>
|
|
48
51
|
|
|
49
52
|
namespace basalt {
|
|
50
53
|
|
|
@@ -65,6 +68,17 @@ class AppKitScrollViewManager {
|
|
|
65
68
|
void update(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
66
69
|
|
|
67
70
|
void remove(facebook::react::Tag tag);
|
|
71
|
+
// `maintainVisibleContentPosition`, in the two halves a mounting transaction
|
|
72
|
+
// needs: the children are measured before the mutations and the offset is
|
|
73
|
+
// moved after, so content under the eye stays where it is while items are
|
|
74
|
+
// inserted above it. The arithmetic is core/ScrollVisiblePosition.h's.
|
|
75
|
+
//
|
|
76
|
+
// Public because the mounting manager is what knows when a transaction starts
|
|
77
|
+
// and ends, which is upstream's division too: `mountingTransactionWillMount`
|
|
78
|
+
// and `...DidMount` on iOS.
|
|
79
|
+
void prepareMaintainVisiblePosition();
|
|
80
|
+
void adjustForMaintainVisiblePosition();
|
|
81
|
+
|
|
68
82
|
|
|
69
83
|
// ScrollView's imperative commands: scrollTo, scrollToEnd. Returns false if
|
|
70
84
|
// the command is not one this handles.
|
|
@@ -124,6 +138,35 @@ class AppKitScrollViewManager {
|
|
|
124
138
|
|
|
125
139
|
double offsetX{0};
|
|
126
140
|
double offsetY{0};
|
|
141
|
+
|
|
142
|
+
// `maintainVisibleContentPosition`, as React Native hands it over: a
|
|
143
|
+
// minimum index and an optional threshold, or nothing at all when the list
|
|
144
|
+
// never asked.
|
|
145
|
+
std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisible{};
|
|
146
|
+
// The child being held still across the current transaction, measured by
|
|
147
|
+
// `prepareMaintainVisiblePosition` and used by the adjust that follows.
|
|
148
|
+
ScrollPinnedChild pinned{};
|
|
149
|
+
// `indicatorStyle`: which colour the overlay thumb is drawn in. Held as
|
|
150
|
+
// core's enum rather than React Native's, the view layer below knowing
|
|
151
|
+
// nothing about either.
|
|
152
|
+
ScrollIndicatorStyle indicatorStyle{ScrollIndicatorStyle::Default};
|
|
153
|
+
|
|
154
|
+
// `centerContent`: whether content smaller than the container is centred in
|
|
155
|
+
// it. Acted on through the content inset, which is how iOS does it too --
|
|
156
|
+
// see `update`.
|
|
157
|
+
bool centerContent{false};
|
|
158
|
+
|
|
159
|
+
// The last `contentOffset` prop seen, and nothing when none has been.
|
|
160
|
+
//
|
|
161
|
+
// The *initial* value of that prop arrives through the state rather than
|
|
162
|
+
// here -- `ScrollViewShadowNode::initialStateData` seeds the state with it
|
|
163
|
+
// -- and is adopted below. What this is for is every later change, which
|
|
164
|
+
// arrives as an ordinary prop update and is applied when it *changes*,
|
|
165
|
+
// never merely when it differs from where the list is: an app that sets the
|
|
166
|
+
// prop once and re-renders for an unrelated reason must not have the list
|
|
167
|
+
// dragged back under the person reading it. The same rule a `<TextInput>`'s
|
|
168
|
+
// `text` is under, for the same reason.
|
|
169
|
+
std::optional<facebook::react::Point> lastPropContentOffset{};
|
|
127
170
|
double lastEmitSeconds{0};
|
|
128
171
|
bool dragging{false};
|
|
129
172
|
// Coasting after the fingers left. Tracked for the same reason `dragging`
|
|
@@ -140,6 +183,11 @@ class AppKitScrollViewManager {
|
|
|
140
183
|
double animationLastSeconds{0};
|
|
141
184
|
};
|
|
142
185
|
|
|
186
|
+
// The rows of the content view, on whichever axis scrolls, for
|
|
187
|
+
// `maintainVisibleContentPosition`. The one part of that prop a toolkit
|
|
188
|
+
// knows: core decides what to do with the boxes.
|
|
189
|
+
std::vector<ScrollChildBox> contentChildBoxes(const Entry &entry) const;
|
|
190
|
+
|
|
143
191
|
void applyOffset(Entry &entry, double x, double y, bool emitEvent);
|
|
144
192
|
// Recomputes both overlay scrollbars and hands them to the view to draw.
|
|
145
193
|
// Called wherever the offset, the content size or the container size can
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#import "AppKitScrollView.h"
|
|
2
2
|
|
|
3
|
+
#include "ScrollSnap.h"
|
|
4
|
+
|
|
3
5
|
#import <QuartzCore/QuartzCore.h>
|
|
4
6
|
|
|
5
7
|
#include <react/renderer/components/scrollview/ScrollEvent.h>
|
|
@@ -178,6 +180,71 @@ void AppKitScrollViewManager::update(RnAppKitView *view, const ShadowView &shado
|
|
|
178
180
|
entry.snap.alignment = ScrollSnapAlignment::Start;
|
|
179
181
|
break;
|
|
180
182
|
}
|
|
183
|
+
// The three modifiers of snapping, all of which `core/ScrollSnap.h` acts
|
|
184
|
+
// on: whether the content's own edges count as snap points beside the
|
|
185
|
+
// listed ones, and whether a fling across a spacing carries or stops at the
|
|
186
|
+
// next point.
|
|
187
|
+
// `centerContent`, applied below once the content size is known.
|
|
188
|
+
entry.centerContent = props->centerContent;
|
|
189
|
+
|
|
190
|
+
// `maintainVisibleContentPosition`, remembered for the next transaction:
|
|
191
|
+
// the work it asks for happens around the mutations rather than while a
|
|
192
|
+
// prop is being read. See `prepareMaintainVisiblePosition`.
|
|
193
|
+
entry.maintainVisible = props->maintainVisibleContentPosition;
|
|
194
|
+
|
|
195
|
+
// `indicatorStyle`, resolved to a colour by core so that all three hosts
|
|
196
|
+
// draw the same thumb for the same prop.
|
|
197
|
+
switch (props->indicatorStyle) {
|
|
198
|
+
case facebook::react::ScrollViewIndicatorStyle::White:
|
|
199
|
+
entry.indicatorStyle = ScrollIndicatorStyle::White;
|
|
200
|
+
break;
|
|
201
|
+
case facebook::react::ScrollViewIndicatorStyle::Black:
|
|
202
|
+
entry.indicatorStyle = ScrollIndicatorStyle::Black;
|
|
203
|
+
break;
|
|
204
|
+
case facebook::react::ScrollViewIndicatorStyle::Default:
|
|
205
|
+
default:
|
|
206
|
+
entry.indicatorStyle = ScrollIndicatorStyle::Default;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
entry.snap.snapToStart = props->snapToStart;
|
|
211
|
+
entry.snap.snapToEnd = props->snapToEnd;
|
|
212
|
+
entry.snap.disableIntervalMomentum = props->disableIntervalMomentum;
|
|
213
|
+
|
|
214
|
+
// `contentOffset`, which an app writes to open a list part way down. Its
|
|
215
|
+
// first value reached here through the state and is adopted below; this is
|
|
216
|
+
// every later change, applied the way upstream applies it --
|
|
217
|
+
// `oldScrollViewProps.contentOffset != newScrollViewProps.contentOffset` in
|
|
218
|
+
// `RCTScrollViewComponentView` -- rather than whenever the prop differs
|
|
219
|
+
// from where the list is.
|
|
220
|
+
//
|
|
221
|
+
// The offset is set rather than animated to, which is what assigning
|
|
222
|
+
// `UIScrollView.contentOffset` does, and the clamp below is what keeps a
|
|
223
|
+
// value past the end of the content from showing empty space.
|
|
224
|
+
if (!entry.lastPropContentOffset.has_value()
|
|
225
|
+
|| *entry.lastPropContentOffset != props->contentOffset) {
|
|
226
|
+
entry.lastPropContentOffset = props->contentOffset;
|
|
227
|
+
entry.offsetX = props->contentOffset.x;
|
|
228
|
+
entry.offsetY = props->contentOffset.y;
|
|
229
|
+
// Whatever the list was animating towards is somewhere the app has just
|
|
230
|
+
// said it does not want to be. No fling to stop: the momentum on this
|
|
231
|
+
// host is not this host's to run.
|
|
232
|
+
stopAnimation(entry);
|
|
233
|
+
}
|
|
234
|
+
// `decelerationRate`, which this host does not animate with -- the fling
|
|
235
|
+
// itself is AppKit's, at its own friction -- but does need for the
|
|
236
|
+
// *projection*: where a fling would have landed is the point a snap is
|
|
237
|
+
// chosen from, and a fast list stops sooner. Zero is what an app that
|
|
238
|
+
// never set the prop leaves behind.
|
|
239
|
+
if (props->decelerationRate > 0) {
|
|
240
|
+
entry.snap.decelerationRate = static_cast<double>(props->decelerationRate);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// What all of that came to, for the tree dump. None of these props is
|
|
244
|
+
// visible any other way: they decide where a fling *ends*, and a dump is
|
|
245
|
+
// taken when nothing is moving. The string is core's, so the three hosts
|
|
246
|
+
// cannot drift; see describeScrollSnap.
|
|
247
|
+
view.rnScrollSnap = @(basalt::describeScrollSnap(entry.snap).c_str());
|
|
181
248
|
}
|
|
182
249
|
|
|
183
250
|
if (const auto state =
|
|
@@ -195,6 +262,27 @@ void AppKitScrollViewManager::update(RnAppKitView *view, const ShadowView &shado
|
|
|
195
262
|
}
|
|
196
263
|
}
|
|
197
264
|
|
|
265
|
+
// `centerContent`: content smaller than its container sits in the middle of
|
|
266
|
+
// it. Expressed as an inset of half the slack at each end, which leaves the
|
|
267
|
+
// range below a single offset to be at -- so the clamp that was already here
|
|
268
|
+
// is what centres the content and what keeps it centred.
|
|
269
|
+
//
|
|
270
|
+
// It replaces the app's own `contentInset` rather than adding to it, which is
|
|
271
|
+
// what iOS does: `centerContentIfNeeded` assigns the inset outright, and
|
|
272
|
+
// `RCTScrollViewComponentView` skips the `contentInset` prop entirely while
|
|
273
|
+
// `centerContent` is set. Recomputed here rather than where the prop is read
|
|
274
|
+
// because it needs the content size, which arrives with the state.
|
|
275
|
+
if (entry.centerContent) {
|
|
276
|
+
const ScrollAxisInsets horizontal =
|
|
277
|
+
centeringInsets(entry.containerSize.width, entry.contentSize.width);
|
|
278
|
+
const ScrollAxisInsets vertical =
|
|
279
|
+
centeringInsets(entry.containerSize.height, entry.contentSize.height);
|
|
280
|
+
entry.contentInset.left = static_cast<facebook::react::Float>(horizontal.leading);
|
|
281
|
+
entry.contentInset.right = static_cast<facebook::react::Float>(horizontal.trailing);
|
|
282
|
+
entry.contentInset.top = static_cast<facebook::react::Float>(vertical.leading);
|
|
283
|
+
entry.contentInset.bottom = static_cast<facebook::react::Float>(vertical.trailing);
|
|
284
|
+
}
|
|
285
|
+
|
|
198
286
|
// Re-clamp: the content may have shrunk under a scrolled offset.
|
|
199
287
|
const double x = clampScrollOffset(entry.offsetX,
|
|
200
288
|
entry.containerSize.width,
|
|
@@ -212,6 +300,117 @@ void AppKitScrollViewManager::update(RnAppKitView *view, const ShadowView &shado
|
|
|
212
300
|
updateIndicators(entry);
|
|
213
301
|
}
|
|
214
302
|
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// maintainVisibleContentPosition
|
|
305
|
+
//
|
|
306
|
+
// Two halves around a mounting transaction: measure the child being held still
|
|
307
|
+
// before the mutations, and move the offset by however far it moved after. The
|
|
308
|
+
// arithmetic -- which child, how far, and whether the list was near enough to
|
|
309
|
+
// the start to follow the new content instead -- is core/ScrollVisiblePosition.h.
|
|
310
|
+
//
|
|
311
|
+
// What is here is the part only a toolkit knows: where this host's children
|
|
312
|
+
// are. React Native wraps a ScrollView's children in exactly one content view,
|
|
313
|
+
// so the rows are that view's children.
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
namespace {
|
|
317
|
+
|
|
318
|
+
// Whether the list scrolls sideways, decided the way upstream decides it: by
|
|
319
|
+
// the content being wider than the view rather than by a prop, there being no
|
|
320
|
+
// prop for it.
|
|
321
|
+
bool scrollsHorizontally(const Size &container, const Size &content) {
|
|
322
|
+
return content.width > container.width;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
} // namespace
|
|
326
|
+
|
|
327
|
+
std::vector<ScrollChildBox> AppKitScrollViewManager::contentChildBoxes(const Entry &entry) const {
|
|
328
|
+
std::vector<ScrollChildBox> boxes;
|
|
329
|
+
if (entry.view == nil) {
|
|
330
|
+
return boxes;
|
|
331
|
+
}
|
|
332
|
+
// The content view, which React Native always makes and always makes exactly
|
|
333
|
+
// one of. `rnChildrenInPaintOrder` rather than `subviews` because the overlay
|
|
334
|
+
// scrollbar is a subview too and is not a row; for children with no zIndex --
|
|
335
|
+
// which rows do not have -- that order is the order they were inserted in.
|
|
336
|
+
RnAppKitView *const content = [entry.view rnChildrenInPaintOrder].firstObject;
|
|
337
|
+
if (content == nil) {
|
|
338
|
+
return boxes;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const bool horizontal = scrollsHorizontally(entry.containerSize, entry.contentSize);
|
|
342
|
+
for (RnAppKitView *row in [content rnChildrenInPaintOrder]) {
|
|
343
|
+
const NSRect frame = row.frame;
|
|
344
|
+
boxes.push_back(ScrollChildBox{
|
|
345
|
+
.tag = static_cast<int>(row.rnTag),
|
|
346
|
+
.leading = horizontal ? frame.origin.x : frame.origin.y,
|
|
347
|
+
.length = horizontal ? frame.size.width : frame.size.height});
|
|
348
|
+
}
|
|
349
|
+
return boxes;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
void AppKitScrollViewManager::prepareMaintainVisiblePosition() {
|
|
353
|
+
for (auto &[tag, entry] : entries_) {
|
|
354
|
+
entry.pinned = {};
|
|
355
|
+
if (!entry.maintainVisible.has_value()) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const bool horizontal = scrollsHorizontally(entry.containerSize, entry.contentSize);
|
|
359
|
+
entry.pinned = firstVisibleChild(contentChildBoxes(entry),
|
|
360
|
+
entry.maintainVisible->minIndexForVisible,
|
|
361
|
+
horizontal ? entry.offsetX : entry.offsetY);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
void AppKitScrollViewManager::adjustForMaintainVisiblePosition() {
|
|
366
|
+
for (auto &[tag, entry] : entries_) {
|
|
367
|
+
if (!entry.maintainVisible.has_value() || !entry.pinned.has) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const bool horizontal = scrollsHorizontally(entry.containerSize, entry.contentSize);
|
|
371
|
+
const double offset = horizontal ? entry.offsetX : entry.offsetY;
|
|
372
|
+
|
|
373
|
+
// The same child, found again by tag: its *position in the list* is exactly
|
|
374
|
+
// what the mutation may have changed, so an index would find a different
|
|
375
|
+
// row. A child that is gone is not adjusted for, which is upstream's
|
|
376
|
+
// behaviour too -- it gives up rather than guessing.
|
|
377
|
+
const ScrollPinnedChild pinned = entry.pinned;
|
|
378
|
+
entry.pinned = {};
|
|
379
|
+
std::optional<double> leading;
|
|
380
|
+
for (const ScrollChildBox &box : contentChildBoxes(entry)) {
|
|
381
|
+
if (box.tag == pinned.tag) {
|
|
382
|
+
leading = box.leading;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!leading.has_value()) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const ScrollVisibleAdjustment adjustment = visiblePositionAdjustment(
|
|
391
|
+
pinned.leading, *leading, offset, entry.maintainVisible->autoscrollToTopThreshold);
|
|
392
|
+
if (!adjustment.move) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (horizontal) {
|
|
397
|
+
applyOffset(entry, offset + adjustment.delta, entry.offsetY, true);
|
|
398
|
+
} else {
|
|
399
|
+
applyOffset(entry, entry.offsetX, offset + adjustment.delta, true);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// And the start, animated, when the list was near enough to it to be
|
|
403
|
+
// following the new content rather than holding its place.
|
|
404
|
+
if (adjustment.autoscrollToStart) {
|
|
405
|
+
if (horizontal) {
|
|
406
|
+
scrollTowards(entry, 0.0, entry.offsetY, true);
|
|
407
|
+
} else {
|
|
408
|
+
scrollTowards(entry, entry.offsetX, 0.0, true);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
215
414
|
void AppKitScrollViewManager::remove(Tag tag) {
|
|
216
415
|
const auto it = entries_.find(tag);
|
|
217
416
|
if (it == entries_.end()) {
|
|
@@ -394,6 +593,17 @@ void AppKitScrollViewManager::updateIndicators(const Entry &entry) {
|
|
|
394
593
|
horizontalInsets(entry.indicatorInset))
|
|
395
594
|
: ScrollIndicator{};
|
|
396
595
|
|
|
596
|
+
const ScrollIndicatorColour thumb = scrollIndicatorColourFor(entry.indicatorStyle);
|
|
597
|
+
[entry.view setRnScrollIndicatorColourRed:thumb.red
|
|
598
|
+
green:thumb.green
|
|
599
|
+
blue:thumb.blue
|
|
600
|
+
alpha:thumb.alpha];
|
|
601
|
+
const basalt::ScrollIndicatorColour edge =
|
|
602
|
+
basalt::scrollIndicatorBorderColourFor(entry.indicatorStyle);
|
|
603
|
+
[entry.view setRnScrollIndicatorBorderColourRed:edge.red
|
|
604
|
+
green:edge.green
|
|
605
|
+
blue:edge.blue
|
|
606
|
+
alpha:edge.alpha];
|
|
397
607
|
[entry.view setRnScrollIndicatorVerticalOffset:vertical.offset
|
|
398
608
|
verticalLength:vertical.length
|
|
399
609
|
horizontalOffset:horizontal.offset
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
#include <optional>
|
|
33
33
|
#include <string>
|
|
34
34
|
#include <unordered_map>
|
|
35
|
+
#include <vector>
|
|
35
36
|
|
|
36
37
|
namespace basalt {
|
|
37
38
|
|
|
@@ -51,6 +52,19 @@ class AppKitTextInputManager {
|
|
|
51
52
|
|
|
52
53
|
void remove(facebook::react::Tag tag);
|
|
53
54
|
|
|
55
|
+
// Focuses whatever `autoFocus` asked for, once the transaction is on screen.
|
|
56
|
+
//
|
|
57
|
+
// It cannot be done where the prop is read. Props are applied by
|
|
58
|
+
// MountingWalk::create, immediately after `createView` and before any Insert,
|
|
59
|
+
// so the view has no superview and `view.window` is nil -- and a
|
|
60
|
+
// `makeFirstResponder:` to nil is a silent no-op. Worse, the flag that guarded
|
|
61
|
+
// it is `try_emplace`'s "first time this tag was seen", which is false by the
|
|
62
|
+
// time the Insert arrives, so it never ran twice either. Deferred to the end of
|
|
63
|
+
// the transaction, which is where resolveRefreshControls already lives for the
|
|
64
|
+
// same reason: Fabric inserts a subtree bottom-up, so even "just after
|
|
65
|
+
// insertChild" is too early.
|
|
66
|
+
void flushAutoFocus();
|
|
67
|
+
|
|
54
68
|
// focus, blur, and setTextAndSelection. Returns false for anything else.
|
|
55
69
|
bool dispatchCommand(facebook::react::Tag tag,
|
|
56
70
|
const std::string &name,
|
|
@@ -59,7 +73,10 @@ class AppKitTextInputManager {
|
|
|
59
73
|
// Called by the Objective-C delegate. Public because the trampoline has to
|
|
60
74
|
// reach them; not part of anything a caller would use.
|
|
61
75
|
void handleChanged(facebook::react::Tag tag);
|
|
62
|
-
|
|
76
|
+
// Return. What it does is `submitBehavior`'s to say: report a submit, report
|
|
77
|
+
// one and give up focus, or insert a newline. Returns true when the key was
|
|
78
|
+
// used up, which is what tells AppKit whether to insert the newline itself.
|
|
79
|
+
bool handleSubmit(facebook::react::Tag tag);
|
|
63
80
|
void handleFocus(facebook::react::Tag tag);
|
|
64
81
|
void handleBlur(facebook::react::Tag tag);
|
|
65
82
|
|
|
@@ -74,6 +91,11 @@ class AppKitTextInputManager {
|
|
|
74
91
|
bool handleKeyDown(void *event);
|
|
75
92
|
|
|
76
93
|
private:
|
|
94
|
+
// Set when a field is created with `autoFocus`, cleared by flushAutoFocus.
|
|
95
|
+
// A list rather than one tag because nothing stops two fields asking; the last
|
|
96
|
+
// one to be focused wins, which is what AppKit would do anyway.
|
|
97
|
+
std::vector<facebook::react::Tag> pendingAutoFocus_;
|
|
98
|
+
|
|
77
99
|
struct Entry {
|
|
78
100
|
// The selection as JavaScript last saw it, so one movement is one event.
|
|
79
101
|
facebook::react::AttributedString::Range lastReportedSelection{0, 0};
|
|
@@ -94,9 +116,24 @@ class AppKitTextInputManager {
|
|
|
94
116
|
bool applying{false};
|
|
95
117
|
|
|
96
118
|
bool secure{false};
|
|
119
|
+
// `submitBehavior`, already resolved: React Native's `Default` means
|
|
120
|
+
// `newline` for a multiline field and `blurAndSubmit` for a single-line
|
|
121
|
+
// one, and `getNonDefaultSubmitBehavior()` is what says so. Read where
|
|
122
|
+
// Return arrives, which is a delegate callback on both peers.
|
|
123
|
+
facebook::react::SubmitBehavior submitBehavior{
|
|
124
|
+
facebook::react::SubmitBehavior::BlurAndSubmit};
|
|
125
|
+
|
|
97
126
|
// A multiline field is an NSTextView rather than an NSTextField, so a
|
|
98
127
|
// change here rebuilds the peer exactly as `secure` does.
|
|
99
128
|
bool multiline{false};
|
|
129
|
+
|
|
130
|
+
// `clearTextOnFocus` and `selectTextOnFocus`: what happens when the field
|
|
131
|
+
// takes focus. Kept here because focus arrives long after the props do, and
|
|
132
|
+
// because the second has to be known in `flushAutoFocus` as well -- a field
|
|
133
|
+
// that asked to select its text must not have the selection collapsed
|
|
134
|
+
// behind it.
|
|
135
|
+
bool clearTextOnFocus{false};
|
|
136
|
+
bool selectTextOnFocus{false};
|
|
100
137
|
std::string lastReportedText;
|
|
101
138
|
|
|
102
139
|
// The last `text` prop actually seen, and whether one has been seen at all.
|