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.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- 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
|