basalt-win32 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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,515 @@
1
+ #include "Win32ScrollView.h"
2
+
3
+ #include <unordered_map>
4
+ #include <utility>
5
+
6
+ #include "PlatformServices.h"
7
+
8
+ #include <react/renderer/components/scrollview/ScrollEvent.h>
9
+ #include <react/renderer/components/scrollview/ScrollViewEventEmitter.h>
10
+ #include <react/renderer/components/scrollview/ScrollViewProps.h>
11
+ #include <react/renderer/core/ConcreteState.h>
12
+
13
+ #include <algorithm>
14
+ #include <chrono>
15
+ #include <string_view>
16
+
17
+ namespace basalt {
18
+
19
+ using facebook::react::EdgeInsets;
20
+ using facebook::react::Float;
21
+ using facebook::react::Point;
22
+ using facebook::react::ScrollEndDragEvent;
23
+ using facebook::react::ScrollEvent;
24
+ using facebook::react::ScrollViewEventEmitter;
25
+ using facebook::react::ScrollViewProps;
26
+ using facebook::react::ScrollViewShadowNode;
27
+ using facebook::react::ShadowView;
28
+ using facebook::react::Size;
29
+ using facebook::react::Tag;
30
+ using win32::RnWin32View;
31
+
32
+ namespace {
33
+
34
+ // How long a run of wheel notches may pause before it counts as over. Long
35
+ // enough that turning a wheel steadily is one drag rather than a dozen; short
36
+ // enough that letting go feels like letting go. Neither other platform needs a
37
+ // number here, because both are told.
38
+ constexpr double kWheelIdleMs = 150.0;
39
+
40
+ // One axis of the two inset props, in the order core/ScrollBounds.h wants them.
41
+ ScrollAxisInsets verticalInsets(const facebook::react::EdgeInsets &insets) {
42
+ return {insets.top, insets.bottom};
43
+ }
44
+
45
+ ScrollAxisInsets horizontalInsets(const facebook::react::EdgeInsets &insets) {
46
+ return {insets.left, insets.right};
47
+ }
48
+
49
+ double nowMilliseconds() {
50
+ const auto since = std::chrono::steady_clock::now().time_since_epoch();
51
+ return std::chrono::duration<double, std::milli>(since).count();
52
+ }
53
+
54
+ } // namespace
55
+
56
+ Win32ScrollViewManager::Win32ScrollViewManager(EmitterLookup lookup) : lookup_(std::move(lookup)) {}
57
+
58
+ Win32ScrollViewManager::~Win32ScrollViewManager() {
59
+ *alive_ = false;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Mutation handling
64
+ // ---------------------------------------------------------------------------
65
+
66
+ void Win32ScrollViewManager::update(RnWin32View *view, const ShadowView &shadowView) {
67
+ const Tag tag = shadowView.tag;
68
+ auto [it, inserted] = entries_.try_emplace(tag);
69
+ Entry &entry = it->second;
70
+
71
+ entry.view = view;
72
+ entry.tag = tag;
73
+
74
+ // A ScrollView is the one view that must clip: its content is deliberately
75
+ // larger than its frame, and without this it paints over its siblings. Set
76
+ // here rather than left to `overflow`, and after applyProps has run, so that
77
+ // a ScrollView whose props say `visible` still behaves like one.
78
+ view->setClipsChildren(true);
79
+
80
+ entry.containerSize = shadowView.layoutMetrics.frame.size;
81
+
82
+ if (const auto props = std::dynamic_pointer_cast<const ScrollViewProps>(shadowView.props)) {
83
+ entry.scrollEnabled = props->scrollEnabled;
84
+ entry.showsVerticalIndicator = props->showsVerticalScrollIndicator;
85
+ entry.showsHorizontalIndicator = props->showsHorizontalScrollIndicator;
86
+ entry.contentInset = props->contentInset;
87
+ entry.indicatorInset = props->scrollIndicatorInsets;
88
+ entry.eventThrottleMs = static_cast<double>(props->scrollEventThrottle);
89
+
90
+ entry.snap.paging = props->pagingEnabled;
91
+ entry.snap.interval = static_cast<double>(props->snapToInterval);
92
+ entry.snap.offsets.clear();
93
+ for (const auto offset : props->snapToOffsets) {
94
+ entry.snap.offsets.push_back(static_cast<double>(offset));
95
+ }
96
+ switch (props->snapToAlignment) {
97
+ case facebook::react::ScrollViewSnapToAlignment::Center:
98
+ entry.snap.alignment = ScrollSnapAlignment::Center;
99
+ break;
100
+ case facebook::react::ScrollViewSnapToAlignment::End:
101
+ entry.snap.alignment = ScrollSnapAlignment::End;
102
+ break;
103
+ case facebook::react::ScrollViewSnapToAlignment::Start:
104
+ default:
105
+ entry.snap.alignment = ScrollSnapAlignment::Start;
106
+ break;
107
+ }
108
+ }
109
+
110
+ if (const auto state =
111
+ std::dynamic_pointer_cast<const ScrollViewShadowNode::ConcreteState>(shadowView.state)) {
112
+ entry.state = state;
113
+ const auto &data = state->getData();
114
+ entry.contentSize = data.getContentSize();
115
+
116
+ if (inserted) {
117
+ // First sight of this ScrollView. The state may already carry an offset,
118
+ // from the `contentOffset` prop or from a surface that was suspended and
119
+ // remounted, so adopt it rather than starting at zero.
120
+ entry.offsetX = data.contentOffset.x;
121
+ entry.offsetY = data.contentOffset.y;
122
+ }
123
+ }
124
+
125
+ // Re-clamp: the content may have shrunk under a scrolled offset.
126
+ entry.offsetX = clampScrollOffset(entry.offsetX,
127
+ entry.containerSize.width,
128
+ entry.contentSize.width,
129
+ horizontalInsets(entry.contentInset));
130
+ entry.offsetY = clampScrollOffset(entry.offsetY,
131
+ entry.containerSize.height,
132
+ entry.contentSize.height,
133
+ verticalInsets(entry.contentInset));
134
+ view->setScrollOffset(static_cast<float>(entry.offsetX), static_cast<float>(entry.offsetY));
135
+ // Here as well as in applyOffset: a list that grew or a window that was
136
+ // resized changes the thumb without changing the offset at all.
137
+ updateIndicators(entry);
138
+ }
139
+
140
+ void Win32ScrollViewManager::remove(Tag tag) {
141
+ // The view is about to be deleted, so the entry must go with it: an idle
142
+ // timer still in flight looks the tag up again and finds nothing, which is
143
+ // the whole reason it is keyed by tag rather than holding the Entry.
144
+ //
145
+ // An animation timer is not idle, though, and the map it registered in
146
+ // outlives the entry -- so it is killed here rather than left to expire
147
+ // against a tag that no longer resolves.
148
+ const auto it = entries_.find(tag);
149
+ if (it != entries_.end()) {
150
+ stopAnimation(it->second);
151
+ }
152
+ entries_.erase(tag);
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // The wheel
157
+ // ---------------------------------------------------------------------------
158
+
159
+ bool Win32ScrollViewManager::scrollAt(RnWin32View *root, double x, double y, double dx, double dy) {
160
+ if (root == nullptr || entries_.empty()) {
161
+ return false;
162
+ }
163
+ RnWin32View *hit = win32::hitTest(root, static_cast<float>(x), static_cast<float>(y));
164
+
165
+ // Innermost first, which is what makes a list inside a list scroll the inner
166
+ // one. GTK and AppKit get the same ordering from their toolkits; here it is
167
+ // the parent walk.
168
+ for (RnWin32View *view = hit; view != nullptr; view = view->parent()) {
169
+ const auto it = entries_.find(static_cast<Tag>(view->tag()));
170
+ if (it == entries_.end()) {
171
+ continue;
172
+ }
173
+ if (scrollEntry(it->second, dx, dy)) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+
180
+ // Consumed means "this is a ScrollView and it is enabled", not "it moved" --
181
+ // matching AppKit. A list already at its bottom still eats the wheel rather
182
+ // than handing it to the list behind it, which is what every desktop does and
183
+ // what stops a nested list from dragging its parent along at the end of every
184
+ // scroll.
185
+ bool Win32ScrollViewManager::scrollEntry(Entry &entry, double dx, double dy) {
186
+ if (!entry.scrollEnabled) {
187
+ return false;
188
+ }
189
+ // The person moving the list wins over the app moving it.
190
+ stopAnimation(entry);
191
+
192
+ if (!entry.dragging) {
193
+ entry.dragging = true;
194
+ emitScrollEvent(entry, "beginDrag");
195
+ }
196
+
197
+ // The pull. A desktop scroll view has no rubber band to stretch, so what a
198
+ // <RefreshControl> gets instead is the wheel still asking to go up after the
199
+ // offset has already reached zero. Reported before the offset is applied,
200
+ // because applying it changes nothing at the top and there would be nothing
201
+ // left to see.
202
+ if (overscrollTop_) {
203
+ const bool pastTop = dy < 0.0 && entry.offsetY <= 0.0;
204
+ overscrollTop_(entry.tag, pastTop ? -dy : 0.0);
205
+ }
206
+
207
+ applyOffset(entry, entry.offsetX + dx, entry.offsetY + dy, true);
208
+
209
+ // Last, because with no UI thread installed -- which is every test --
210
+ // postDelayed runs its work inline, and this ends the drag before returning.
211
+ const Tag tag = entry.tag;
212
+ const std::uint64_t generation = ++entry.wheelGeneration;
213
+ postDelayed(kWheelIdleMs, [this, alive = alive_, tag, generation] {
214
+ if (*alive) {
215
+ endWheelDrag(tag, generation);
216
+ }
217
+ });
218
+ return true;
219
+ }
220
+
221
+ void Win32ScrollViewManager::endWheelDrag(Tag tag, std::uint64_t generation) {
222
+ const auto it = entries_.find(tag);
223
+ if (it == entries_.end()) {
224
+ return;
225
+ }
226
+ Entry &entry = it->second;
227
+ if (entry.wheelGeneration != generation || !entry.dragging) {
228
+ return;
229
+ }
230
+ entry.dragging = false;
231
+ emitScrollEvent(entry, "endDrag");
232
+ // A wheel supplies no velocity, so there is never a flick to take over from
233
+ // here: the settle is always to the nearest point. That is also why this host
234
+ // models no momentum -- see the header.
235
+ settleOnSnapPoint(entry, 0.0);
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Offset, events and state
240
+ // ---------------------------------------------------------------------------
241
+
242
+ namespace {
243
+
244
+ // Which manager and view a timer is animating. A TIMERPROC carries no state, so
245
+ // the id is the key -- and the map is the reason `stopAnimation` must run before
246
+ // an entry goes, exactly as the display link does on AppKit.
247
+ std::unordered_map<UINT_PTR, std::pair<Win32ScrollViewManager *, facebook::react::Tag>> &
248
+ animations() {
249
+ static std::unordered_map<UINT_PTR, std::pair<Win32ScrollViewManager *, facebook::react::Tag>>
250
+ value;
251
+ return value;
252
+ }
253
+
254
+ } // namespace
255
+
256
+ void CALLBACK Win32ScrollViewManager::onAnimationTimer(HWND /*hwnd*/,
257
+ UINT /*message*/,
258
+ UINT_PTR id,
259
+ DWORD now) {
260
+ const auto it = animations().find(id);
261
+ if (it == animations().end()) {
262
+ KillTimer(nullptr, id);
263
+ return;
264
+ }
265
+ Win32ScrollViewManager *manager = it->second.first;
266
+ const facebook::react::Tag tag = it->second.second;
267
+ manager->advanceAnimation(tag, static_cast<double>(now) / 1000.0);
268
+ }
269
+
270
+ void Win32ScrollViewManager::scrollTowards(Entry &entry, double x, double y, bool animated) {
271
+ stopAnimation(entry);
272
+ if (!animated || !entry.animation.start(entry.offsetX, entry.offsetY, x, y)) {
273
+ applyOffset(entry, x, y, true);
274
+ return;
275
+ }
276
+ // Sixteen milliseconds: this host's choreographer runs on a timer of the same
277
+ // period, and matching it keeps a scroll and an animation stepping together
278
+ // rather than beating against each other.
279
+ entry.animationTimer = SetTimer(nullptr, 0, 16, onAnimationTimer);
280
+ if (entry.animationTimer == 0) {
281
+ entry.animation.stop();
282
+ applyOffset(entry, x, y, true);
283
+ return;
284
+ }
285
+ entry.animationLastMillis = 0;
286
+ animations()[entry.animationTimer] = {this, entry.tag};
287
+ }
288
+
289
+ bool Win32ScrollViewManager::settleOnSnapPoint(Entry &entry, double velocityY) {
290
+ const auto target = scrollSnapTarget(entry.snap,
291
+ entry.offsetY,
292
+ velocityY,
293
+ entry.containerSize.height,
294
+ entry.contentSize.height);
295
+ if (!target) {
296
+ return false;
297
+ }
298
+ scrollTowards(entry, entry.offsetX, *target, true);
299
+ return true;
300
+ }
301
+
302
+ void Win32ScrollViewManager::stopAnimation(Entry &entry) {
303
+ entry.animation.stop();
304
+ if (entry.animationTimer != 0) {
305
+ KillTimer(nullptr, entry.animationTimer);
306
+ animations().erase(entry.animationTimer);
307
+ entry.animationTimer = 0;
308
+ }
309
+ }
310
+
311
+ void Win32ScrollViewManager::advanceAnimation(facebook::react::Tag tag, double nowSeconds) {
312
+ const auto it = entries_.find(tag);
313
+ if (it == entries_.end()) {
314
+ return;
315
+ }
316
+ Entry &entry = it->second;
317
+ if (!entry.animation.isRunning()) {
318
+ stopAnimation(entry);
319
+ return;
320
+ }
321
+
322
+ // The tick carries the system time rather than a delta, so the first frame
323
+ // has nothing to measure against and advances by one period.
324
+ const auto nowMillis = static_cast<unsigned long long>(nowSeconds * 1000.0);
325
+ const double seconds = entry.animationLastMillis == 0
326
+ ? 0.016
327
+ : static_cast<double>(nowMillis - entry.animationLastMillis) / 1000.0;
328
+ entry.animationLastMillis = nowMillis;
329
+
330
+ double x = entry.offsetX;
331
+ double y = entry.offsetY;
332
+ const bool running = entry.animation.advance(seconds, x, y);
333
+ applyOffset(entry, x, y, true);
334
+ if (!running) {
335
+ stopAnimation(entry);
336
+ }
337
+ }
338
+
339
+ // The overlay scrollbars. The geometry is core/ScrollIndicator.h's, so GTK,
340
+ // AppKit and Win32 place the same thumb in the same place.
341
+ //
342
+ // Always drawn, rather than faded in while scrolling and out after: there is no
343
+ // timer here and no animation, which is why `flashScrollIndicators` stays a
344
+ // no-op -- there is nothing to flash something already on screen.
345
+ void Win32ScrollViewManager::updateIndicators(const Entry &entry) {
346
+ if (entry.view == nullptr) {
347
+ return;
348
+ }
349
+
350
+ const ScrollIndicator vertical =
351
+ entry.showsVerticalIndicator
352
+ ? scrollIndicatorFor(entry.containerSize.height,
353
+ entry.contentSize.height,
354
+ entry.offsetY,
355
+ verticalInsets(entry.contentInset),
356
+ verticalInsets(entry.indicatorInset))
357
+ : ScrollIndicator{};
358
+ const ScrollIndicator horizontal =
359
+ entry.showsHorizontalIndicator
360
+ ? scrollIndicatorFor(entry.containerSize.width,
361
+ entry.contentSize.width,
362
+ entry.offsetX,
363
+ horizontalInsets(entry.contentInset),
364
+ horizontalInsets(entry.indicatorInset))
365
+ : ScrollIndicator{};
366
+
367
+ entry.view->setScrollIndicators(static_cast<float>(vertical.offset),
368
+ static_cast<float>(vertical.length),
369
+ static_cast<float>(horizontal.offset),
370
+ static_cast<float>(horizontal.length));
371
+ }
372
+
373
+ void Win32ScrollViewManager::applyOffset(Entry &entry, double x, double y, bool emitEvent) {
374
+ const double clampedX = clampScrollOffset(x,
375
+ entry.containerSize.width,
376
+ entry.contentSize.width,
377
+ horizontalInsets(entry.contentInset));
378
+ const double clampedY = clampScrollOffset(y,
379
+ entry.containerSize.height,
380
+ entry.contentSize.height,
381
+ verticalInsets(entry.contentInset));
382
+
383
+ if (clampedX == entry.offsetX && clampedY == entry.offsetY) {
384
+ return;
385
+ }
386
+ entry.offsetX = clampedX;
387
+ entry.offsetY = clampedY;
388
+
389
+ if (entry.view != nullptr) {
390
+ entry.view->setScrollOffset(static_cast<float>(clampedX), static_cast<float>(clampedY));
391
+ }
392
+ updateIndicators(entry);
393
+
394
+ // Unthrottled, and separate from onScroll on purpose.
395
+ // ScrollViewShadowNode::getContentOriginOffset reads this, and through it so
396
+ // do measure, measureLayout, C++ hit testing and view culling. Throttling it
397
+ // would leave all of those reporting a stale scroll position.
398
+ writeStateOffset(entry);
399
+
400
+ if (!emitEvent) {
401
+ return;
402
+ }
403
+
404
+ // scrollEventThrottle is the platform's job. Zero means every scroll.
405
+ const double now = nowMilliseconds();
406
+ if (entry.eventThrottleMs > 0 && now - entry.lastEmitMs < entry.eventThrottleMs) {
407
+ return;
408
+ }
409
+ entry.lastEmitMs = now;
410
+ emitScrollEvent(entry, "scroll");
411
+ }
412
+
413
+ void Win32ScrollViewManager::emitScrollEvent(Entry &entry, const char *which) {
414
+ const auto emitter = std::dynamic_pointer_cast<const ScrollViewEventEmitter>(lookup_(entry.tag));
415
+ if (emitter == nullptr) {
416
+ return;
417
+ }
418
+
419
+ const auto fill = [&entry](ScrollEvent &event) {
420
+ event.contentOffset = Point{.x = static_cast<Float>(entry.offsetX),
421
+ .y = static_cast<Float>(entry.offsetY)};
422
+ event.contentSize = entry.contentSize;
423
+ event.containerSize = entry.containerSize;
424
+ event.contentInset = entry.contentInset;
425
+ // Not a default worth trusting: ScrollEvent initialises zoomScale to 0, and
426
+ // VirtualizedList only repairs negative values, so leaving it would make
427
+ // every list measurement come out as zero.
428
+ event.zoomScale = 1.0F;
429
+ event.timestamp = static_cast<Float>(nowMilliseconds() / 1000.0);
430
+ };
431
+
432
+ if (std::string_view(which) == "endDrag") {
433
+ ScrollEndDragEvent event{};
434
+ fill(event);
435
+ // No velocity model yet, so a drag ends where it stopped. Without a
436
+ // targetContentOffset the JS side treats the drag as ending immediately,
437
+ // which is what actually happens here.
438
+ event.targetContentOffset = event.contentOffset;
439
+ event.velocity = Point{.x = 0, .y = 0};
440
+ emitter->onScrollEndDrag(event);
441
+ return;
442
+ }
443
+
444
+ ScrollEvent event{};
445
+ fill(event);
446
+ if (std::string_view(which) == "beginDrag") {
447
+ emitter->onScrollBeginDrag(event);
448
+ } else {
449
+ emitter->onScroll(event);
450
+ }
451
+ }
452
+
453
+ void Win32ScrollViewManager::writeStateOffset(const Entry &entry) {
454
+ if (entry.state == nullptr) {
455
+ return;
456
+ }
457
+ const Point offset{.x = static_cast<Float>(entry.offsetX),
458
+ .y = static_cast<Float>(entry.offsetY)};
459
+
460
+ entry.state->updateState(
461
+ [offset](const ScrollViewShadowNode::ConcreteState::Data &oldData)
462
+ -> ScrollViewShadowNode::ConcreteState::SharedData {
463
+ if (oldData.contentOffset == offset) {
464
+ // Returning null cancels the update. The callback can run more than
465
+ // once when commits race, so it must stay a pure function of oldData.
466
+ return nullptr;
467
+ }
468
+ auto newData = oldData;
469
+ newData.contentOffset = offset;
470
+ return std::make_shared<const ScrollViewShadowNode::ConcreteState::Data>(newData);
471
+ });
472
+ }
473
+
474
+ // ---------------------------------------------------------------------------
475
+ // Commands
476
+ // ---------------------------------------------------------------------------
477
+
478
+ bool Win32ScrollViewManager::dispatchCommand(Tag tag,
479
+ const std::string &name,
480
+ const folly::dynamic &args) {
481
+ const auto it = entries_.find(tag);
482
+ if (it == entries_.end()) {
483
+ return false;
484
+ }
485
+ Entry &entry = it->second;
486
+
487
+ // [x, y, animated] for scrollTo, [animated] for scrollToEnd.
488
+ if (name == "scrollTo" && args.isArray() && args.size() >= 2) {
489
+ scrollTowards(entry,
490
+ args[0].asDouble(),
491
+ args[1].asDouble(),
492
+ args.size() >= 3 && args[2].asBool());
493
+ return true;
494
+ }
495
+
496
+ if (name == "scrollToEnd") {
497
+ scrollTowards(entry,
498
+ entry.contentSize.width - entry.containerSize.width,
499
+ entry.contentSize.height - entry.containerSize.height,
500
+ args.isArray() && args.size() >= 1 && args[0].asBool());
501
+ return true;
502
+ }
503
+
504
+ if (name == "flashScrollIndicators") {
505
+ // Nothing to flash: the indicator is always on screen while there is one to
506
+ // draw -- see updateIndicators above. Claimed and ignored rather than left
507
+ // to fall through, which would log an unimplemented command every time a
508
+ // list settles.
509
+ return true;
510
+ }
511
+
512
+ return false;
513
+ }
514
+
515
+ } // namespace basalt