basalt-appkit 0.0.0-rc5
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/LICENSE +21 -0
- package/cli/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#import "AppKitScrollView.h"
|
|
2
|
+
|
|
3
|
+
#import <QuartzCore/QuartzCore.h>
|
|
4
|
+
|
|
5
|
+
#include <react/renderer/components/scrollview/ScrollEvent.h>
|
|
6
|
+
#include <react/renderer/components/scrollview/ScrollViewEventEmitter.h>
|
|
7
|
+
#include <react/renderer/components/scrollview/ScrollViewProps.h>
|
|
8
|
+
#include <react/renderer/core/ConcreteState.h>
|
|
9
|
+
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
#include <cmath>
|
|
12
|
+
|
|
13
|
+
// A wheel notch carries no pixel distance of its own, so a step has to be
|
|
14
|
+
// chosen. The same 53 the GTK side uses -- roughly three lines of 16pt text --
|
|
15
|
+
// so one notch moves a list by the same amount on both desktops. Matching the
|
|
16
|
+
// other platform matters more here than matching either toolkit's own default,
|
|
17
|
+
// which is the whole argument of this project in one constant.
|
|
18
|
+
static constexpr double kWheelStepPixels = 53.0;
|
|
19
|
+
|
|
20
|
+
// The bridge between AppKit's protocol and the C++ manager, for the same reason
|
|
21
|
+
// the touch dispatcher has one: a C++ object cannot conform to an Objective-C
|
|
22
|
+
// protocol, and the views hold their handler weakly.
|
|
23
|
+
// The display link's target for an animated `scrollTo`. One per animating
|
|
24
|
+
// scroll view rather than one for all of them: the link belongs to the view it
|
|
25
|
+
// animates, which is what keeps it on that view's display and stops it when the
|
|
26
|
+
// view goes.
|
|
27
|
+
@interface RnAppKitScrollAnimationTarget : NSObject
|
|
28
|
+
@property(nonatomic, assign) basalt::AppKitScrollViewManager *manager;
|
|
29
|
+
@property(nonatomic, assign) facebook::react::Tag tag;
|
|
30
|
+
@property(nonatomic, assign) CFTimeInterval last;
|
|
31
|
+
- (void)step:(CADisplayLink *)sender;
|
|
32
|
+
@end
|
|
33
|
+
|
|
34
|
+
@implementation RnAppKitScrollAnimationTarget
|
|
35
|
+
|
|
36
|
+
- (void)step:(CADisplayLink *)sender {
|
|
37
|
+
if (_manager == nullptr) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// The frame's own timestamp, for the same reason GTK uses the frame clock's:
|
|
41
|
+
// a callback that runs late must not shorten the curve.
|
|
42
|
+
const CFTimeInterval now = sender.targetTimestamp;
|
|
43
|
+
const double seconds = _last == 0 ? 0 : now - _last;
|
|
44
|
+
_last = now;
|
|
45
|
+
_manager->advanceAnimation(_tag, seconds);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@end
|
|
49
|
+
|
|
50
|
+
@interface RnAppKitScrollTarget : NSObject <RnAppKitScrollHandler>
|
|
51
|
+
@property(nonatomic, assign) basalt::AppKitScrollViewManager *manager;
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
@implementation RnAppKitScrollTarget
|
|
55
|
+
|
|
56
|
+
- (BOOL)rnScrollView:(RnAppKitView *)view
|
|
57
|
+
by:(NSPoint)delta
|
|
58
|
+
precise:(BOOL)precise
|
|
59
|
+
phase:(NSEventPhase)phase
|
|
60
|
+
momentum:(NSEventPhase)momentum {
|
|
61
|
+
if (_manager == nullptr) {
|
|
62
|
+
return NO;
|
|
63
|
+
}
|
|
64
|
+
// A touchpad reports phases; a wheel reports none. The two streams do not
|
|
65
|
+
// overlap: while the fingers are down `momentum` is NSEventPhaseNone, and
|
|
66
|
+
// once the system is coasting `phase` is. So a drag and the momentum after it
|
|
67
|
+
// are told apart here and nowhere else.
|
|
68
|
+
const bool began = phase == NSEventPhaseBegan;
|
|
69
|
+
const bool ended = phase == NSEventPhaseEnded || phase == NSEventPhaseCancelled;
|
|
70
|
+
const bool momentumBegan = momentum == NSEventPhaseBegan;
|
|
71
|
+
const bool momentumEnded =
|
|
72
|
+
momentum == NSEventPhaseEnded || momentum == NSEventPhaseCancelled;
|
|
73
|
+
|
|
74
|
+
// A precise device reports pixels; a wheel reports line counts. Treating a
|
|
75
|
+
// line as a pixel makes the wheel move the content by one pixel a notch,
|
|
76
|
+
// which reads as the wheel not working.
|
|
77
|
+
const double scale = precise ? 1.0 : kWheelStepPixels;
|
|
78
|
+
|
|
79
|
+
// AppKit's positive Y is a scroll *up*, which moves content down and reduces
|
|
80
|
+
// the offset. React Native's contentOffset grows downward, like GTK's delta,
|
|
81
|
+
// so both axes are inverted here. scrollingDeltaY already accounts for the
|
|
82
|
+
// user's natural-scrolling preference, so nothing else has to.
|
|
83
|
+
return _manager->scrollBy(static_cast<facebook::react::Tag>(view.rnTag),
|
|
84
|
+
-delta.x * scale,
|
|
85
|
+
-delta.y * scale,
|
|
86
|
+
began,
|
|
87
|
+
ended,
|
|
88
|
+
momentumBegan,
|
|
89
|
+
momentumEnded);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@end
|
|
93
|
+
|
|
94
|
+
namespace basalt {
|
|
95
|
+
|
|
96
|
+
using facebook::react::EdgeInsets;
|
|
97
|
+
using facebook::react::Float;
|
|
98
|
+
using facebook::react::Point;
|
|
99
|
+
using facebook::react::ScrollEndDragEvent;
|
|
100
|
+
using facebook::react::ScrollEvent;
|
|
101
|
+
using facebook::react::ScrollViewEventEmitter;
|
|
102
|
+
using facebook::react::ScrollViewProps;
|
|
103
|
+
using facebook::react::ScrollViewShadowNode;
|
|
104
|
+
using facebook::react::ShadowView;
|
|
105
|
+
using facebook::react::Size;
|
|
106
|
+
using facebook::react::Tag;
|
|
107
|
+
|
|
108
|
+
namespace {
|
|
109
|
+
|
|
110
|
+
// One axis of the two inset props, in the order core/ScrollBounds.h wants them.
|
|
111
|
+
ScrollAxisInsets verticalInsets(const facebook::react::EdgeInsets &insets) {
|
|
112
|
+
return {insets.top, insets.bottom};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ScrollAxisInsets horizontalInsets(const facebook::react::EdgeInsets &insets) {
|
|
116
|
+
return {insets.left, insets.right};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
double nowSeconds() {
|
|
120
|
+
return NSProcessInfo.processInfo.systemUptime;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
} // namespace
|
|
124
|
+
|
|
125
|
+
AppKitScrollViewManager::AppKitScrollViewManager(EmitterLookup lookup) : lookup_(std::move(lookup)) {
|
|
126
|
+
RnAppKitScrollTarget *target = [[RnAppKitScrollTarget alloc] init];
|
|
127
|
+
target.manager = this;
|
|
128
|
+
scrollTarget_ = target;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
AppKitScrollViewManager::~AppKitScrollViewManager() {
|
|
132
|
+
((RnAppKitScrollTarget *)scrollTarget_).manager = nullptr;
|
|
133
|
+
scrollTarget_ = nil;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Mutation handling
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
void AppKitScrollViewManager::update(RnAppKitView *view, const ShadowView &shadowView) {
|
|
141
|
+
const Tag tag = shadowView.tag;
|
|
142
|
+
auto [it, inserted] = entries_.try_emplace(tag);
|
|
143
|
+
Entry &entry = it->second;
|
|
144
|
+
|
|
145
|
+
entry.view = view;
|
|
146
|
+
entry.tag = tag;
|
|
147
|
+
|
|
148
|
+
// A ScrollView is the one view that must clip: its content is deliberately
|
|
149
|
+
// larger than its frame, and without this it paints over its siblings.
|
|
150
|
+
[view setRnClipsChildren:YES];
|
|
151
|
+
view.rnScrollHandler = (id<RnAppKitScrollHandler>)scrollTarget_;
|
|
152
|
+
|
|
153
|
+
entry.containerSize = shadowView.layoutMetrics.frame.size;
|
|
154
|
+
|
|
155
|
+
if (const auto props = std::dynamic_pointer_cast<const ScrollViewProps>(shadowView.props)) {
|
|
156
|
+
entry.scrollEnabled = props->scrollEnabled;
|
|
157
|
+
entry.showsVerticalIndicator = props->showsVerticalScrollIndicator;
|
|
158
|
+
entry.showsHorizontalIndicator = props->showsHorizontalScrollIndicator;
|
|
159
|
+
entry.contentInset = props->contentInset;
|
|
160
|
+
entry.indicatorInset = props->scrollIndicatorInsets;
|
|
161
|
+
entry.eventThrottleMs = static_cast<double>(props->scrollEventThrottle);
|
|
162
|
+
|
|
163
|
+
entry.snap.paging = props->pagingEnabled;
|
|
164
|
+
entry.snap.interval = static_cast<double>(props->snapToInterval);
|
|
165
|
+
entry.snap.offsets.clear();
|
|
166
|
+
for (const auto offset : props->snapToOffsets) {
|
|
167
|
+
entry.snap.offsets.push_back(static_cast<double>(offset));
|
|
168
|
+
}
|
|
169
|
+
switch (props->snapToAlignment) {
|
|
170
|
+
case facebook::react::ScrollViewSnapToAlignment::Center:
|
|
171
|
+
entry.snap.alignment = ScrollSnapAlignment::Center;
|
|
172
|
+
break;
|
|
173
|
+
case facebook::react::ScrollViewSnapToAlignment::End:
|
|
174
|
+
entry.snap.alignment = ScrollSnapAlignment::End;
|
|
175
|
+
break;
|
|
176
|
+
case facebook::react::ScrollViewSnapToAlignment::Start:
|
|
177
|
+
default:
|
|
178
|
+
entry.snap.alignment = ScrollSnapAlignment::Start;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (const auto state =
|
|
184
|
+
std::dynamic_pointer_cast<const ScrollViewShadowNode::ConcreteState>(shadowView.state)) {
|
|
185
|
+
entry.state = state;
|
|
186
|
+
const auto &data = state->getData();
|
|
187
|
+
entry.contentSize = data.getContentSize();
|
|
188
|
+
|
|
189
|
+
if (inserted) {
|
|
190
|
+
// First sight of this ScrollView. The state may already carry an offset,
|
|
191
|
+
// from the `contentOffset` prop or from a surface that was suspended and
|
|
192
|
+
// remounted, so adopt it rather than starting at zero.
|
|
193
|
+
entry.offsetX = data.contentOffset.x;
|
|
194
|
+
entry.offsetY = data.contentOffset.y;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Re-clamp: the content may have shrunk under a scrolled offset.
|
|
199
|
+
const double x = clampScrollOffset(entry.offsetX,
|
|
200
|
+
entry.containerSize.width,
|
|
201
|
+
entry.contentSize.width,
|
|
202
|
+
horizontalInsets(entry.contentInset));
|
|
203
|
+
const double y = clampScrollOffset(entry.offsetY,
|
|
204
|
+
entry.containerSize.height,
|
|
205
|
+
entry.contentSize.height,
|
|
206
|
+
verticalInsets(entry.contentInset));
|
|
207
|
+
entry.offsetX = x;
|
|
208
|
+
entry.offsetY = y;
|
|
209
|
+
[view setRnScrollOffsetX:x y:y];
|
|
210
|
+
// Here as well as in applyOffset: a list that grew or a window that was
|
|
211
|
+
// resized changes the thumb without changing the offset at all.
|
|
212
|
+
updateIndicators(entry);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
void AppKitScrollViewManager::remove(Tag tag) {
|
|
216
|
+
const auto it = entries_.find(tag);
|
|
217
|
+
if (it == entries_.end()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// The view's reference to the target is weak and the target dispatches by
|
|
221
|
+
// tag, so dropping the entry is enough for the wheel: one arriving afterwards
|
|
222
|
+
// finds no entry and is passed up the responder chain instead.
|
|
223
|
+
//
|
|
224
|
+
// A display link is not like that. It is retained by the run loop and holds a
|
|
225
|
+
// tag it would go on dispatching, so it has to be invalidated here rather than
|
|
226
|
+
// dropped -- the same lesson as the touch dispatcher's controllers.
|
|
227
|
+
stopAnimation(it->second);
|
|
228
|
+
entries_.erase(it);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// The wheel
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
bool AppKitScrollViewManager::scrollBy(Tag tag,
|
|
236
|
+
double dx,
|
|
237
|
+
double dy,
|
|
238
|
+
bool began,
|
|
239
|
+
bool ended,
|
|
240
|
+
bool momentumBegan,
|
|
241
|
+
bool momentumEnded) {
|
|
242
|
+
const auto it = entries_.find(tag);
|
|
243
|
+
if (it == entries_.end()) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
Entry &entry = it->second;
|
|
247
|
+
if (!entry.scrollEnabled) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
// The person moving the list wins over the app moving it, which is what a
|
|
251
|
+
// wheel or a two-finger drag during an animated `scrollTo` means.
|
|
252
|
+
stopAnimation(entry);
|
|
253
|
+
|
|
254
|
+
if (began && !entry.dragging) {
|
|
255
|
+
entry.dragging = true;
|
|
256
|
+
emitScrollEvent(entry, "beginDrag");
|
|
257
|
+
}
|
|
258
|
+
if (momentumBegan && !entry.coasting) {
|
|
259
|
+
entry.coasting = true;
|
|
260
|
+
emitScrollEvent(entry, "momentumBegin");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The pull. A desktop scroll view has no rubber band to stretch, so what a
|
|
264
|
+
// <RefreshControl> gets instead is the wheel still asking to go up after the
|
|
265
|
+
// offset has already reached zero. Reported before the offset is applied,
|
|
266
|
+
// because applying it changes nothing at the top and there would be nothing
|
|
267
|
+
// left to see.
|
|
268
|
+
if (overscrollTop_) {
|
|
269
|
+
const bool pastTop = dy < 0.0 && entry.offsetY <= 0.0;
|
|
270
|
+
overscrollTop_(entry.tag, pastTop ? -dy : 0.0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Already in pixels: the trampoline resolved lines against kWheelStepPixels,
|
|
274
|
+
// because only the event knows which it was.
|
|
275
|
+
applyOffset(entry, entry.offsetX + dx, entry.offsetY + dy, true);
|
|
276
|
+
|
|
277
|
+
if (ended && entry.dragging) {
|
|
278
|
+
entry.dragging = false;
|
|
279
|
+
emitScrollEvent(entry, "endDrag");
|
|
280
|
+
// A release. On this host the system may still be coasting, so the settle
|
|
281
|
+
// below waits for it to finish -- but a release with no momentum at all
|
|
282
|
+
// ends here and has to snap now.
|
|
283
|
+
if (!entry.coasting) {
|
|
284
|
+
settleOnSnapPoint(entry, 0.0);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (momentumEnded && entry.coasting) {
|
|
288
|
+
entry.coasting = false;
|
|
289
|
+
emitScrollEvent(entry, "momentumEnd");
|
|
290
|
+
// macOS decelerates for us, so unlike GTK there is no velocity to take over
|
|
291
|
+
// from: the system has already carried the list as far as it was thrown,
|
|
292
|
+
// and what is left is to settle where it stopped.
|
|
293
|
+
settleOnSnapPoint(entry, 0.0);
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Offset, events and state
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
void AppKitScrollViewManager::scrollTowards(Entry &entry, double x, double y, bool animated) {
|
|
303
|
+
stopAnimation(entry);
|
|
304
|
+
if (!animated || !entry.animation.start(entry.offsetX, entry.offsetY, x, y)) {
|
|
305
|
+
// Not animated, or already there. Either way the offset is the answer.
|
|
306
|
+
applyOffset(entry, x, y, true);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (@available(macOS 14.0, *)) {
|
|
311
|
+
RnAppKitScrollAnimationTarget *target = [[RnAppKitScrollAnimationTarget alloc] init];
|
|
312
|
+
target.manager = this;
|
|
313
|
+
target.tag = entry.tag;
|
|
314
|
+
target.last = 0;
|
|
315
|
+
CADisplayLink *link = [entry.view displayLinkWithTarget:target
|
|
316
|
+
selector:@selector(step:)];
|
|
317
|
+
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
318
|
+
entry.displayLink = link;
|
|
319
|
+
entry.animationLastSeconds = 0;
|
|
320
|
+
} else {
|
|
321
|
+
// Same trade the animation choreographer makes below 14: no frame source
|
|
322
|
+
// worth carrying, so the scroll arrives rather than moves.
|
|
323
|
+
entry.animation.stop();
|
|
324
|
+
applyOffset(entry, x, y, true);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
bool AppKitScrollViewManager::settleOnSnapPoint(Entry &entry, double velocityY) {
|
|
329
|
+
const auto target = scrollSnapTarget(entry.snap,
|
|
330
|
+
entry.offsetY,
|
|
331
|
+
velocityY,
|
|
332
|
+
entry.containerSize.height,
|
|
333
|
+
entry.contentSize.height);
|
|
334
|
+
if (!target) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
scrollTowards(entry, entry.offsetX, *target, true);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
void AppKitScrollViewManager::stopAnimation(Entry &entry) {
|
|
342
|
+
entry.animation.stop();
|
|
343
|
+
if (entry.displayLink != nil) {
|
|
344
|
+
[(CADisplayLink *)entry.displayLink invalidate];
|
|
345
|
+
entry.displayLink = nil;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
void AppKitScrollViewManager::advanceAnimation(facebook::react::Tag tag, double seconds) {
|
|
350
|
+
const auto it = entries_.find(tag);
|
|
351
|
+
if (it == entries_.end()) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
Entry &entry = it->second;
|
|
355
|
+
if (!entry.animation.isRunning()) {
|
|
356
|
+
stopAnimation(entry);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
double x = entry.offsetX;
|
|
361
|
+
double y = entry.offsetY;
|
|
362
|
+
const bool running = entry.animation.advance(seconds, x, y);
|
|
363
|
+
applyOffset(entry, x, y, true);
|
|
364
|
+
if (!running) {
|
|
365
|
+
stopAnimation(entry);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The overlay scrollbars. The geometry is core/ScrollIndicator.h's, so GTK,
|
|
370
|
+
// AppKit and Win32 place the same thumb in the same place.
|
|
371
|
+
//
|
|
372
|
+
// Always drawn, rather than faded in while scrolling and out after: there is no
|
|
373
|
+
// timer here and no animation, which is why `flashScrollIndicators` stays a
|
|
374
|
+
// no-op -- there is nothing to flash something already on screen.
|
|
375
|
+
void AppKitScrollViewManager::updateIndicators(const Entry &entry) {
|
|
376
|
+
if (entry.view == nil) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const ScrollIndicator vertical =
|
|
381
|
+
entry.showsVerticalIndicator
|
|
382
|
+
? scrollIndicatorFor(entry.containerSize.height,
|
|
383
|
+
entry.contentSize.height,
|
|
384
|
+
entry.offsetY,
|
|
385
|
+
verticalInsets(entry.contentInset),
|
|
386
|
+
verticalInsets(entry.indicatorInset))
|
|
387
|
+
: ScrollIndicator{};
|
|
388
|
+
const ScrollIndicator horizontal =
|
|
389
|
+
entry.showsHorizontalIndicator
|
|
390
|
+
? scrollIndicatorFor(entry.containerSize.width,
|
|
391
|
+
entry.contentSize.width,
|
|
392
|
+
entry.offsetX,
|
|
393
|
+
horizontalInsets(entry.contentInset),
|
|
394
|
+
horizontalInsets(entry.indicatorInset))
|
|
395
|
+
: ScrollIndicator{};
|
|
396
|
+
|
|
397
|
+
[entry.view setRnScrollIndicatorVerticalOffset:vertical.offset
|
|
398
|
+
verticalLength:vertical.length
|
|
399
|
+
horizontalOffset:horizontal.offset
|
|
400
|
+
horizontalLength:horizontal.length];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
void AppKitScrollViewManager::applyOffset(Entry &entry, double x, double y, bool emitEvent) {
|
|
404
|
+
const double clampedX = clampScrollOffset(x,
|
|
405
|
+
entry.containerSize.width,
|
|
406
|
+
entry.contentSize.width,
|
|
407
|
+
horizontalInsets(entry.contentInset));
|
|
408
|
+
const double clampedY = clampScrollOffset(y,
|
|
409
|
+
entry.containerSize.height,
|
|
410
|
+
entry.contentSize.height,
|
|
411
|
+
verticalInsets(entry.contentInset));
|
|
412
|
+
|
|
413
|
+
if (clampedX == entry.offsetX && clampedY == entry.offsetY) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
entry.offsetX = clampedX;
|
|
417
|
+
entry.offsetY = clampedY;
|
|
418
|
+
|
|
419
|
+
[entry.view setRnScrollOffsetX:clampedX y:clampedY];
|
|
420
|
+
updateIndicators(entry);
|
|
421
|
+
|
|
422
|
+
// Unthrottled, and separate from onScroll on purpose.
|
|
423
|
+
// ScrollViewShadowNode::getContentOriginOffset reads this, and through it so
|
|
424
|
+
// do measure, measureLayout, C++ hit testing and view culling. Throttling it
|
|
425
|
+
// would leave all of those reporting a stale scroll position.
|
|
426
|
+
writeStateOffset(entry);
|
|
427
|
+
|
|
428
|
+
if (!emitEvent) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// scrollEventThrottle is the platform's job. Zero means every scroll.
|
|
433
|
+
const double now = nowSeconds();
|
|
434
|
+
if (entry.eventThrottleMs > 0) {
|
|
435
|
+
if (now - entry.lastEmitSeconds < entry.eventThrottleMs / 1000.0) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
entry.lastEmitSeconds = now;
|
|
440
|
+
emitScrollEvent(entry, "scroll");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
void AppKitScrollViewManager::emitScrollEvent(Entry &entry, const char *which) {
|
|
444
|
+
const auto emitter = std::dynamic_pointer_cast<const ScrollViewEventEmitter>(lookup_(entry.tag));
|
|
445
|
+
if (emitter == nullptr) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const auto fill = [&entry](ScrollEvent &event) {
|
|
450
|
+
event.contentOffset =
|
|
451
|
+
Point{.x = static_cast<Float>(entry.offsetX), .y = static_cast<Float>(entry.offsetY)};
|
|
452
|
+
event.contentSize = entry.contentSize;
|
|
453
|
+
event.containerSize = entry.containerSize;
|
|
454
|
+
event.contentInset = entry.contentInset;
|
|
455
|
+
// Not a default worth trusting: ScrollEvent initialises zoomScale to 0, and
|
|
456
|
+
// VirtualizedList only repairs negative values, so leaving it would make
|
|
457
|
+
// every list measurement come out as zero.
|
|
458
|
+
event.zoomScale = 1.0F;
|
|
459
|
+
event.timestamp = static_cast<Float>(nowSeconds());
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
if (std::string_view(which) == "endDrag") {
|
|
463
|
+
ScrollEndDragEvent event{};
|
|
464
|
+
fill(event);
|
|
465
|
+
// No velocity model yet, so a drag ends where it stopped. Without a
|
|
466
|
+
// targetContentOffset the JS side treats the drag as ending immediately,
|
|
467
|
+
// which is what actually happens here.
|
|
468
|
+
event.targetContentOffset = event.contentOffset;
|
|
469
|
+
event.velocity = Point{.x = 0, .y = 0};
|
|
470
|
+
emitter->onScrollEndDrag(event);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
ScrollEvent event{};
|
|
475
|
+
fill(event);
|
|
476
|
+
const std::string_view kind(which);
|
|
477
|
+
if (kind == "beginDrag") {
|
|
478
|
+
emitter->onScrollBeginDrag(event);
|
|
479
|
+
} else if (kind == "momentumBegin") {
|
|
480
|
+
emitter->onMomentumScrollBegin(event);
|
|
481
|
+
} else if (kind == "momentumEnd") {
|
|
482
|
+
emitter->onMomentumScrollEnd(event);
|
|
483
|
+
} else {
|
|
484
|
+
emitter->onScroll(event);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
void AppKitScrollViewManager::writeStateOffset(const Entry &entry) {
|
|
489
|
+
if (entry.state == nullptr) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const Point offset{.x = static_cast<Float>(entry.offsetX),
|
|
493
|
+
.y = static_cast<Float>(entry.offsetY)};
|
|
494
|
+
|
|
495
|
+
entry.state->updateState(
|
|
496
|
+
[offset](const ScrollViewShadowNode::ConcreteState::Data &oldData)
|
|
497
|
+
-> ScrollViewShadowNode::ConcreteState::SharedData {
|
|
498
|
+
if (oldData.contentOffset == offset) {
|
|
499
|
+
// Returning null cancels the update. The callback can run more than
|
|
500
|
+
// once when commits race, so it must stay a pure function of oldData.
|
|
501
|
+
return nullptr;
|
|
502
|
+
}
|
|
503
|
+
auto newData = oldData;
|
|
504
|
+
newData.contentOffset = offset;
|
|
505
|
+
return std::make_shared<const ScrollViewShadowNode::ConcreteState::Data>(newData);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
// Commands
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
bool AppKitScrollViewManager::dispatchCommand(Tag tag,
|
|
514
|
+
const std::string &name,
|
|
515
|
+
const folly::dynamic &args) {
|
|
516
|
+
const auto it = entries_.find(tag);
|
|
517
|
+
if (it == entries_.end()) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
Entry &entry = it->second;
|
|
521
|
+
|
|
522
|
+
// [x, y, animated] for scrollTo, [animated] for scrollToEnd.
|
|
523
|
+
if (name == "scrollTo" && args.isArray() && args.size() >= 2) {
|
|
524
|
+
scrollTowards(entry,
|
|
525
|
+
args[0].asDouble(),
|
|
526
|
+
args[1].asDouble(),
|
|
527
|
+
args.size() >= 3 && args[2].asBool());
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (name == "scrollToEnd") {
|
|
532
|
+
scrollTowards(entry,
|
|
533
|
+
entry.contentSize.width - entry.containerSize.width,
|
|
534
|
+
entry.contentSize.height - entry.containerSize.height,
|
|
535
|
+
args.isArray() && args.size() >= 1 && args[0].asBool());
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (name == "flashScrollIndicators") {
|
|
540
|
+
// Nothing to flash: the indicator is always on screen while there is one to
|
|
541
|
+
// draw -- see updateIndicators above. Claimed and ignored rather than left
|
|
542
|
+
// to fall through, which would log an unimplemented command every time a
|
|
543
|
+
// list settles.
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
} // namespace basalt
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// What @shopify/react-native-skia needs from this host.
|
|
2
|
+
//
|
|
3
|
+
// `RNSkPlatformContext` is the package's one seam: it hands Skia a runtime, a
|
|
4
|
+
// call invoker and this. The package ships an Apple implementation and basalt
|
|
5
|
+
// cannot use it -- not because its body is bridge-bound, which it almost is not,
|
|
6
|
+
// but because its *header* takes an `RCTBridge *` and builds a screenshot
|
|
7
|
+
// service from `bridge.uiManager`. So this is basalt's, and most of it delegates
|
|
8
|
+
// to `MetalContext`, which the package ships and which touches no React at all.
|
|
9
|
+
//
|
|
10
|
+
// Three of the seventeen overrides throw rather than work, each naming itself:
|
|
11
|
+
// the native-buffer trio, which is CVPixelBuffer interchange nothing here asks
|
|
12
|
+
// for, and the view screenshot, which needs a tag-to-view lookup this host has
|
|
13
|
+
// no reason to expose yet. A throw that says which call was not implemented is
|
|
14
|
+
// worth more than a plausible wrong answer -- `makeImageSnapshot` of a view
|
|
15
|
+
// returning a blank image would look like a rendering bug for a long time.
|
|
16
|
+
|
|
17
|
+
#pragma once
|
|
18
|
+
|
|
19
|
+
#include "RNSkPlatformContext.h"
|
|
20
|
+
|
|
21
|
+
#include <functional>
|
|
22
|
+
#include <memory>
|
|
23
|
+
#include <string>
|
|
24
|
+
|
|
25
|
+
namespace basalt {
|
|
26
|
+
|
|
27
|
+
// A pixel density, because Skia asks for one up front and AppKit's is per
|
|
28
|
+
// screen. The main screen's, read once; a window moved between displays of
|
|
29
|
+
// different scale is a thing to fix when something notices.
|
|
30
|
+
std::shared_ptr<RNSkia::RNSkPlatformContext>
|
|
31
|
+
makeSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker);
|
|
32
|
+
|
|
33
|
+
} // namespace basalt
|