basalt-gtk 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/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- package/react-native.config.js +21 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
#include "GtkScrollView.h"
|
|
2
|
+
|
|
3
|
+
#include <react/renderer/components/scrollview/ScrollEvent.h>
|
|
4
|
+
#include <react/renderer/components/scrollview/ScrollViewEventEmitter.h>
|
|
5
|
+
#include <react/renderer/components/scrollview/ScrollViewProps.h>
|
|
6
|
+
#include <react/renderer/core/ConcreteState.h>
|
|
7
|
+
|
|
8
|
+
#include <algorithm>
|
|
9
|
+
#include <cmath>
|
|
10
|
+
|
|
11
|
+
namespace basalt {
|
|
12
|
+
|
|
13
|
+
using facebook::react::EdgeInsets;
|
|
14
|
+
using facebook::react::Float;
|
|
15
|
+
using facebook::react::Point;
|
|
16
|
+
using facebook::react::ScrollEvent;
|
|
17
|
+
using facebook::react::ScrollEndDragEvent;
|
|
18
|
+
using facebook::react::ScrollViewEventEmitter;
|
|
19
|
+
using facebook::react::ScrollViewProps;
|
|
20
|
+
using facebook::react::ScrollViewShadowNode;
|
|
21
|
+
using facebook::react::ShadowView;
|
|
22
|
+
using facebook::react::Size;
|
|
23
|
+
using facebook::react::Tag;
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
// One axis of the two inset props, in the order core/ScrollBounds.h wants them.
|
|
28
|
+
ScrollAxisInsets verticalInsets(const facebook::react::EdgeInsets &insets) {
|
|
29
|
+
return {insets.top, insets.bottom};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ScrollAxisInsets horizontalInsets(const facebook::react::EdgeInsets &insets) {
|
|
33
|
+
return {insets.left, insets.right};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
} // namespace
|
|
37
|
+
|
|
38
|
+
GtkScrollViewManager::GtkScrollViewManager(EmitterLookup lookup) : lookup_(std::move(lookup)) {}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Mutation handling
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
void GtkScrollViewManager::update(RnView *view, const ShadowView &shadowView) {
|
|
45
|
+
const Tag tag = shadowView.tag;
|
|
46
|
+
auto [it, inserted] = entries_.try_emplace(tag);
|
|
47
|
+
Entry &entry = it->second;
|
|
48
|
+
|
|
49
|
+
entry.view = view;
|
|
50
|
+
entry.tag = tag;
|
|
51
|
+
entry.owner = this;
|
|
52
|
+
|
|
53
|
+
// A ScrollView is the one view that must clip: its content is deliberately
|
|
54
|
+
// larger than its frame, and without this it paints over its siblings.
|
|
55
|
+
rn_view_set_clips_children(view, TRUE);
|
|
56
|
+
|
|
57
|
+
entry.containerSize = shadowView.layoutMetrics.frame.size;
|
|
58
|
+
|
|
59
|
+
if (const auto props = std::dynamic_pointer_cast<const ScrollViewProps>(shadowView.props)) {
|
|
60
|
+
entry.scrollEnabled = props->scrollEnabled;
|
|
61
|
+
entry.showsVerticalIndicator = props->showsVerticalScrollIndicator;
|
|
62
|
+
entry.showsHorizontalIndicator = props->showsHorizontalScrollIndicator;
|
|
63
|
+
entry.contentInset = props->contentInset;
|
|
64
|
+
entry.indicatorInset = props->scrollIndicatorInsets;
|
|
65
|
+
entry.eventThrottleMs = static_cast<double>(props->scrollEventThrottle);
|
|
66
|
+
// React Native resolves 'normal' and 'fast' to numbers before this sees
|
|
67
|
+
// them; zero is what an app that never set the prop leaves behind, and
|
|
68
|
+
// means the default rather than a fling that stops instantly.
|
|
69
|
+
if (props->decelerationRate > 0) {
|
|
70
|
+
entry.decelerationRate = static_cast<double>(props->decelerationRate);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
entry.snap.paging = props->pagingEnabled;
|
|
74
|
+
entry.snap.interval = static_cast<double>(props->snapToInterval);
|
|
75
|
+
entry.snap.offsets.clear();
|
|
76
|
+
for (const auto offset : props->snapToOffsets) {
|
|
77
|
+
entry.snap.offsets.push_back(static_cast<double>(offset));
|
|
78
|
+
}
|
|
79
|
+
switch (props->snapToAlignment) {
|
|
80
|
+
case facebook::react::ScrollViewSnapToAlignment::Center:
|
|
81
|
+
entry.snap.alignment = ScrollSnapAlignment::Center;
|
|
82
|
+
break;
|
|
83
|
+
case facebook::react::ScrollViewSnapToAlignment::End:
|
|
84
|
+
entry.snap.alignment = ScrollSnapAlignment::End;
|
|
85
|
+
break;
|
|
86
|
+
case facebook::react::ScrollViewSnapToAlignment::Start:
|
|
87
|
+
default:
|
|
88
|
+
entry.snap.alignment = ScrollSnapAlignment::Start;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (const auto state =
|
|
94
|
+
std::dynamic_pointer_cast<const ScrollViewShadowNode::ConcreteState>(shadowView.state)) {
|
|
95
|
+
entry.state = state;
|
|
96
|
+
const auto &data = state->getData();
|
|
97
|
+
entry.contentSize = data.getContentSize();
|
|
98
|
+
|
|
99
|
+
if (inserted) {
|
|
100
|
+
// First sight of this ScrollView. The state may already carry an offset,
|
|
101
|
+
// from the `contentOffset` prop or from a surface that was suspended and
|
|
102
|
+
// remounted, so adopt it rather than starting at zero.
|
|
103
|
+
entry.offsetX = data.contentOffset.x;
|
|
104
|
+
entry.offsetY = data.contentOffset.y;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Re-clamp: the content may have shrunk under a scrolled offset.
|
|
109
|
+
const double x = clampScrollOffset(entry.offsetX,
|
|
110
|
+
entry.containerSize.width,
|
|
111
|
+
entry.contentSize.width,
|
|
112
|
+
horizontalInsets(entry.contentInset));
|
|
113
|
+
const double y = clampScrollOffset(entry.offsetY,
|
|
114
|
+
entry.containerSize.height,
|
|
115
|
+
entry.contentSize.height,
|
|
116
|
+
verticalInsets(entry.contentInset));
|
|
117
|
+
entry.offsetX = x;
|
|
118
|
+
entry.offsetY = y;
|
|
119
|
+
rn_view_set_scroll_offset(view, x, y);
|
|
120
|
+
// Here as well as in applyOffset: a list that grew or a window that was
|
|
121
|
+
// resized changes the thumb without changing the offset at all.
|
|
122
|
+
updateIndicators(entry);
|
|
123
|
+
|
|
124
|
+
if (inserted) {
|
|
125
|
+
// GTK_EVENT_CONTROLLER_SCROLL_KINETIC asks GDK for one `decelerate` signal
|
|
126
|
+
// carrying the velocity a gesture ended at. It is all GTK offers: the
|
|
127
|
+
// coasting itself is this file's, through core/ScrollMomentum.h, which is
|
|
128
|
+
// the half macOS gets from the system.
|
|
129
|
+
entry.controller = gtk_event_controller_scroll_new(
|
|
130
|
+
static_cast<GtkEventControllerScrollFlags>(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES |
|
|
131
|
+
GTK_EVENT_CONTROLLER_SCROLL_KINETIC));
|
|
132
|
+
g_signal_connect(entry.controller, "scroll", G_CALLBACK(onScroll), &entry);
|
|
133
|
+
g_signal_connect(entry.controller, "scroll-begin", G_CALLBACK(onScrollBegin), &entry);
|
|
134
|
+
g_signal_connect(entry.controller, "scroll-end", G_CALLBACK(onScrollEnd), &entry);
|
|
135
|
+
g_signal_connect(entry.controller, "decelerate", G_CALLBACK(onDecelerate), &entry);
|
|
136
|
+
gtk_widget_add_controller(GTK_WIDGET(view), entry.controller);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void GtkScrollViewManager::remove(Tag tag) {
|
|
141
|
+
const auto it = entries_.find(tag);
|
|
142
|
+
if (it == entries_.end()) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Detach before the Entry goes away: the controller's callbacks hold a
|
|
146
|
+
// pointer to it, and gtk_widget_add_controller means the widget, not this,
|
|
147
|
+
// owns the controller's lifetime.
|
|
148
|
+
Entry &entry = it->second;
|
|
149
|
+
// The tick callbacks hold the same pointer the controller does, and outlive
|
|
150
|
+
// neither. Stopped without an event: the emitter is going away with the view.
|
|
151
|
+
stopMomentum(entry, false);
|
|
152
|
+
stopAnimation(entry);
|
|
153
|
+
if (entry.controller != nullptr && entry.view != nullptr && RN_IS_VIEW(entry.view)) {
|
|
154
|
+
gtk_widget_remove_controller(GTK_WIDGET(entry.view), entry.controller);
|
|
155
|
+
}
|
|
156
|
+
entries_.erase(it);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// GTK callbacks
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
gboolean GtkScrollViewManager::onScroll(GtkEventControllerScroll *controller,
|
|
164
|
+
double dx,
|
|
165
|
+
double dy,
|
|
166
|
+
gpointer userData) {
|
|
167
|
+
auto *entry = static_cast<Entry *>(userData);
|
|
168
|
+
if (!entry->scrollEnabled) {
|
|
169
|
+
return GDK_EVENT_PROPAGATE;
|
|
170
|
+
}
|
|
171
|
+
// A wheel arrives without a `scroll-begin`, so the cancellation there does not
|
|
172
|
+
// cover it.
|
|
173
|
+
entry->owner->stopAnimation(*entry);
|
|
174
|
+
|
|
175
|
+
// A wheel reports discrete notches, a touchpad reports pixels. Treating the
|
|
176
|
+
// first as pixels makes the wheel move the content by one pixel a click.
|
|
177
|
+
double stepX = dx;
|
|
178
|
+
double stepY = dy;
|
|
179
|
+
if (gtk_event_controller_scroll_get_unit(controller) == GDK_SCROLL_UNIT_WHEEL) {
|
|
180
|
+
stepX *= GtkScrollViewManager::kWheelStepPixels;
|
|
181
|
+
stepY *= GtkScrollViewManager::kWheelStepPixels;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
entry->owner->scrollEntry(*entry, stepX, stepY);
|
|
185
|
+
return GDK_EVENT_STOP;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
void GtkScrollViewManager::scrollEntry(Entry &entry, double dx, double dy) {
|
|
189
|
+
// The pull. A desktop scroll view has no rubber band to stretch, so what a
|
|
190
|
+
// <RefreshControl> gets instead is the wheel still asking to go up after the
|
|
191
|
+
// offset has already reached zero. Reported before the offset is applied,
|
|
192
|
+
// because applying it changes nothing at the top and there would be nothing
|
|
193
|
+
// left to see.
|
|
194
|
+
if (overscrollTop_) {
|
|
195
|
+
const bool pastTop = dy < 0.0 && entry.offsetY <= 0.0;
|
|
196
|
+
overscrollTop_(entry.tag, pastTop ? -dy : 0.0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
applyOffset(entry, entry.offsetX + dx, entry.offsetY + dy, true);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
bool GtkScrollViewManager::scrollAt(RnView *root, double x, double y, double dx, double dy) {
|
|
203
|
+
if (root == nullptr) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
GtkWidget *picked = gtk_widget_pick(
|
|
207
|
+
GTK_WIDGET(root), x, y, GTK_PICK_DEFAULT);
|
|
208
|
+
// Up from whatever was hit to the nearest view this manager knows about,
|
|
209
|
+
// which is what a wheel does anyway: a plain view inside a list scrolls the
|
|
210
|
+
// list, and a list inside a list scrolls the inner one.
|
|
211
|
+
for (GtkWidget *walk = picked; walk != nullptr; walk = gtk_widget_get_parent(walk)) {
|
|
212
|
+
if (!RN_IS_VIEW(walk)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const auto found = entries_.find(static_cast<facebook::react::Tag>(rn_view_get_tag(RN_VIEW(walk))));
|
|
216
|
+
if (found == entries_.end()) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!found->second.scrollEnabled) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
scrollEntry(found->second, dx, dy);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
void GtkScrollViewManager::onScrollBegin(GtkEventControllerScroll * /*controller*/, gpointer userData) {
|
|
229
|
+
auto *entry = static_cast<Entry *>(userData);
|
|
230
|
+
if (!entry->scrollEnabled) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// A new gesture takes the list off whatever it was coasting towards, which is
|
|
234
|
+
// what putting a finger on a moving list does everywhere else -- and off an
|
|
235
|
+
// animated `scrollTo` too, which is the same expectation: the person moving
|
|
236
|
+
// the list wins over the app moving it.
|
|
237
|
+
entry->owner->stopMomentum(*entry, true);
|
|
238
|
+
entry->owner->stopAnimation(*entry);
|
|
239
|
+
entry->dragging = true;
|
|
240
|
+
entry->owner->emitScrollEvent(*entry, "beginDrag");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
void GtkScrollViewManager::onScrollEnd(GtkEventControllerScroll * /*controller*/, gpointer userData) {
|
|
244
|
+
auto *entry = static_cast<Entry *>(userData);
|
|
245
|
+
if (!entry->dragging) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
entry->dragging = false;
|
|
249
|
+
entry->owner->emitScrollEvent(*entry, "endDrag");
|
|
250
|
+
// A release with no velocity. `decelerate` arrives separately when there was
|
|
251
|
+
// a flick, and settles with the velocity it carries; this is the other case.
|
|
252
|
+
entry->owner->settleOnSnapPoint(*entry, 0.0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
bool GtkScrollViewManager::settleOnSnapPoint(Entry &entry, double velocityY) {
|
|
256
|
+
const auto target = scrollSnapTarget(entry.snap,
|
|
257
|
+
entry.offsetY,
|
|
258
|
+
velocityY,
|
|
259
|
+
entry.containerSize.height,
|
|
260
|
+
entry.contentSize.height);
|
|
261
|
+
if (!target) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
// Animated, which is what makes it a snap rather than a jump -- and the
|
|
265
|
+
// reason `core/ScrollAnimation.h` came first.
|
|
266
|
+
scrollTowards(entry, entry.offsetX, *target, true);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Momentum
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
void GtkScrollViewManager::onDecelerate(GtkEventControllerScroll * /*controller*/,
|
|
275
|
+
double velocityX,
|
|
276
|
+
double velocityY,
|
|
277
|
+
gpointer userData) {
|
|
278
|
+
auto *entry = static_cast<Entry *>(userData);
|
|
279
|
+
entry->owner->fling(entry->tag, velocityX, velocityY);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
bool GtkScrollViewManager::fling(Tag tag, double velocityX, double velocityY) {
|
|
283
|
+
const auto it = entries_.find(tag);
|
|
284
|
+
if (it == entries_.end()) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
Entry &entry = it->second;
|
|
288
|
+
if (!entry.scrollEnabled || entry.view == nullptr) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// GTK emits `decelerate` around the same moment as `scroll-end`, and does not
|
|
293
|
+
// promise which comes first. React Native's order does promise: a drag ends
|
|
294
|
+
// before the momentum after it begins, and a JavaScript list that sees them
|
|
295
|
+
// the other way round decides a scroll is still in progress after it has
|
|
296
|
+
// finished. So the drag is ended here if it has not been already.
|
|
297
|
+
if (entry.dragging) {
|
|
298
|
+
entry.dragging = false;
|
|
299
|
+
emitScrollEvent(entry, "endDrag");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// A snapping list does not coast. React Native replaces the fling with a
|
|
303
|
+
// settle, which is what makes a page turn one page rather than however many
|
|
304
|
+
// the throw was worth -- the velocity decides *which* point rather than how
|
|
305
|
+
// far.
|
|
306
|
+
//
|
|
307
|
+
// Here rather than in the `decelerate` handler so that the test seam below
|
|
308
|
+
// reaches it: a real fling needs a touchscreen, and `fling()` is how the unit
|
|
309
|
+
// suite gets one.
|
|
310
|
+
if (settleOnSnapPoint(entry, velocityY)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!entry.momentum.start(velocityX, velocityY, entry.decelerationRate)) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
entry.momentumLastMicros = g_get_monotonic_time();
|
|
318
|
+
emitScrollEvent(entry, "momentumBegin");
|
|
319
|
+
entry.momentumTickId =
|
|
320
|
+
gtk_widget_add_tick_callback(GTK_WIDGET(entry.view), onMomentumTick, &entry, nullptr);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
bool GtkScrollViewManager::advanceFling(Tag tag, double seconds) {
|
|
325
|
+
const auto it = entries_.find(tag);
|
|
326
|
+
if (it == entries_.end()) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
Entry &entry = it->second;
|
|
330
|
+
if (!entry.momentum.isRunning()) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
double dx = 0;
|
|
335
|
+
double dy = 0;
|
|
336
|
+
const bool running = entry.momentum.advance(seconds, dx, dy);
|
|
337
|
+
|
|
338
|
+
const double beforeX = entry.offsetX;
|
|
339
|
+
const double beforeY = entry.offsetY;
|
|
340
|
+
applyOffset(entry, beforeX + dx, beforeY + dy, true);
|
|
341
|
+
|
|
342
|
+
// Stopped, or run into an edge and gone nowhere. The second is not the same
|
|
343
|
+
// check as the first: a fling at the top of a list has velocity left and
|
|
344
|
+
// nothing to spend it on, and without this it would coast silently for a
|
|
345
|
+
// second before reporting that it had finished.
|
|
346
|
+
const bool moved = entry.offsetX != beforeX || entry.offsetY != beforeY;
|
|
347
|
+
if (running && moved) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
entry.momentum.stop();
|
|
351
|
+
if (entry.momentumTickId != 0) {
|
|
352
|
+
// Cleared rather than removed: a tick callback that returns G_SOURCE_REMOVE
|
|
353
|
+
// is already gone, and removing it again is a GTK warning.
|
|
354
|
+
entry.momentumTickId = 0;
|
|
355
|
+
}
|
|
356
|
+
emitScrollEvent(entry, "momentumEnd");
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
gboolean GtkScrollViewManager::onAnimationTick(GtkWidget * /*widget*/,
|
|
361
|
+
GdkFrameClock *clock,
|
|
362
|
+
gpointer userData) {
|
|
363
|
+
auto *entry = static_cast<Entry *>(userData);
|
|
364
|
+
// The frame clock's time, for the same reason the fling uses it: it is the
|
|
365
|
+
// time the frame is *for*, so a late callback does not shorten the curve.
|
|
366
|
+
const gint64 now = gdk_frame_clock_get_frame_time(clock);
|
|
367
|
+
const double seconds = static_cast<double>(now - entry->animationLastMicros) / 1e6;
|
|
368
|
+
entry->animationLastMicros = now;
|
|
369
|
+
|
|
370
|
+
return entry->owner->advanceAnimation(entry->tag, seconds) ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
void GtkScrollViewManager::scrollTowards(Entry &entry, double x, double y, bool animated) {
|
|
374
|
+
stopAnimation(entry);
|
|
375
|
+
if (!animated || !entry.animation.start(entry.offsetX, entry.offsetY, x, y)) {
|
|
376
|
+
// Not animated, or already there. Either way the offset is the answer and
|
|
377
|
+
// there is no curve to run.
|
|
378
|
+
applyOffset(entry, x, y, true);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
entry.animationLastMicros = g_get_monotonic_time();
|
|
382
|
+
entry.animationTickId =
|
|
383
|
+
gtk_widget_add_tick_callback(GTK_WIDGET(entry.view), onAnimationTick, &entry, nullptr);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
bool GtkScrollViewManager::advanceAnimation(Tag tag, double seconds) {
|
|
387
|
+
const auto it = entries_.find(tag);
|
|
388
|
+
if (it == entries_.end()) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
Entry &entry = it->second;
|
|
392
|
+
if (!entry.animation.isRunning()) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
double x = entry.offsetX;
|
|
397
|
+
double y = entry.offsetY;
|
|
398
|
+
const bool running = entry.animation.advance(seconds, x, y);
|
|
399
|
+
applyOffset(entry, x, y, true);
|
|
400
|
+
|
|
401
|
+
if (!running) {
|
|
402
|
+
entry.animationTickId = 0;
|
|
403
|
+
}
|
|
404
|
+
return running;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
void GtkScrollViewManager::stopAnimation(Entry &entry) {
|
|
408
|
+
entry.animation.stop();
|
|
409
|
+
if (entry.animationTickId != 0) {
|
|
410
|
+
gtk_widget_remove_tick_callback(GTK_WIDGET(entry.view), entry.animationTickId);
|
|
411
|
+
entry.animationTickId = 0;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
gboolean GtkScrollViewManager::onMomentumTick(GtkWidget * /*widget*/,
|
|
416
|
+
GdkFrameClock *clock,
|
|
417
|
+
gpointer userData) {
|
|
418
|
+
auto *entry = static_cast<Entry *>(userData);
|
|
419
|
+
|
|
420
|
+
// The frame clock's own time rather than g_get_monotonic_time: it is the time
|
|
421
|
+
// the frame is *for*, so a fling advances by exactly one frame's worth even
|
|
422
|
+
// when the callback runs late.
|
|
423
|
+
const gint64 now = gdk_frame_clock_get_frame_time(clock);
|
|
424
|
+
const double seconds = static_cast<double>(now - entry->momentumLastMicros) / 1e6;
|
|
425
|
+
entry->momentumLastMicros = now;
|
|
426
|
+
|
|
427
|
+
return entry->owner->advanceFling(entry->tag, seconds) ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
void GtkScrollViewManager::stopMomentum(Entry &entry, bool emitEnd) {
|
|
431
|
+
if (entry.momentumTickId != 0) {
|
|
432
|
+
if (entry.view != nullptr && RN_IS_VIEW(entry.view)) {
|
|
433
|
+
gtk_widget_remove_tick_callback(GTK_WIDGET(entry.view), entry.momentumTickId);
|
|
434
|
+
}
|
|
435
|
+
entry.momentumTickId = 0;
|
|
436
|
+
if (emitEnd) {
|
|
437
|
+
emitScrollEvent(entry, "momentumEnd");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
entry.momentum.stop();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// Offset, events and state
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
void GtkScrollViewManager::applyOffset(Entry &entry, double x, double y, bool emitEvent) {
|
|
448
|
+
const double clampedX = clampScrollOffset(x,
|
|
449
|
+
entry.containerSize.width,
|
|
450
|
+
entry.contentSize.width,
|
|
451
|
+
horizontalInsets(entry.contentInset));
|
|
452
|
+
const double clampedY = clampScrollOffset(y,
|
|
453
|
+
entry.containerSize.height,
|
|
454
|
+
entry.contentSize.height,
|
|
455
|
+
verticalInsets(entry.contentInset));
|
|
456
|
+
|
|
457
|
+
if (clampedX == entry.offsetX && clampedY == entry.offsetY) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
entry.offsetX = clampedX;
|
|
461
|
+
entry.offsetY = clampedY;
|
|
462
|
+
|
|
463
|
+
rn_view_set_scroll_offset(entry.view, clampedX, clampedY);
|
|
464
|
+
updateIndicators(entry);
|
|
465
|
+
|
|
466
|
+
// Unthrottled, and separate from onScroll on purpose.
|
|
467
|
+
// ScrollViewShadowNode::getContentOriginOffset reads this, and through it so
|
|
468
|
+
// do measure, measureLayout, C++ hit testing and view culling. Throttling it
|
|
469
|
+
// would leave all of those reporting a stale scroll position.
|
|
470
|
+
writeStateOffset(entry);
|
|
471
|
+
|
|
472
|
+
if (!emitEvent) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// scrollEventThrottle is the platform's job. Zero means every scroll.
|
|
477
|
+
const gint64 now = g_get_monotonic_time();
|
|
478
|
+
if (entry.eventThrottleMs > 0) {
|
|
479
|
+
const gint64 interval = static_cast<gint64>(entry.eventThrottleMs * 1000.0);
|
|
480
|
+
if (now - entry.lastEmitMicros < interval) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
entry.lastEmitMicros = now;
|
|
485
|
+
emitScrollEvent(entry, "scroll");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// The overlay scrollbars. The geometry is core/ScrollIndicator.h's, so GTK,
|
|
489
|
+
// AppKit and Win32 place the same thumb in the same place.
|
|
490
|
+
//
|
|
491
|
+
// Always drawn, rather than faded in while scrolling and out after: there is no
|
|
492
|
+
// timer here and no animation, which is why `flashScrollIndicators` stays a
|
|
493
|
+
// no-op -- there is nothing to flash something already on screen.
|
|
494
|
+
void GtkScrollViewManager::updateIndicators(const Entry &entry) {
|
|
495
|
+
if (entry.view == nullptr) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const ScrollIndicator vertical = entry.showsVerticalIndicator
|
|
500
|
+
? scrollIndicatorFor(entry.containerSize.height,
|
|
501
|
+
entry.contentSize.height,
|
|
502
|
+
entry.offsetY,
|
|
503
|
+
verticalInsets(entry.contentInset),
|
|
504
|
+
verticalInsets(entry.indicatorInset))
|
|
505
|
+
: ScrollIndicator{};
|
|
506
|
+
const ScrollIndicator horizontal =
|
|
507
|
+
entry.showsHorizontalIndicator ? scrollIndicatorFor(entry.containerSize.width,
|
|
508
|
+
entry.contentSize.width,
|
|
509
|
+
entry.offsetX,
|
|
510
|
+
horizontalInsets(entry.contentInset),
|
|
511
|
+
horizontalInsets(entry.indicatorInset))
|
|
512
|
+
: ScrollIndicator{};
|
|
513
|
+
|
|
514
|
+
rn_view_set_scroll_indicators(
|
|
515
|
+
entry.view, vertical.offset, vertical.length, horizontal.offset, horizontal.length);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
void GtkScrollViewManager::emitScrollEvent(Entry &entry, const char *which) {
|
|
519
|
+
const auto emitter = std::dynamic_pointer_cast<const ScrollViewEventEmitter>(lookup_(entry.tag));
|
|
520
|
+
if (emitter == nullptr) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const auto fill = [&entry](ScrollEvent &event) {
|
|
525
|
+
event.contentOffset = Point{.x = static_cast<Float>(entry.offsetX), .y = static_cast<Float>(entry.offsetY)};
|
|
526
|
+
event.contentSize = entry.contentSize;
|
|
527
|
+
event.containerSize = entry.containerSize;
|
|
528
|
+
event.contentInset = entry.contentInset;
|
|
529
|
+
// Not a default worth trusting: ScrollEvent initialises zoomScale to 0, and
|
|
530
|
+
// VirtualizedList only repairs negative values, so leaving it would make
|
|
531
|
+
// every list measurement come out as zero.
|
|
532
|
+
event.zoomScale = 1.0F;
|
|
533
|
+
event.timestamp = static_cast<Float>(g_get_monotonic_time()) / 1e6F;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (std::string_view(which) == "endDrag") {
|
|
537
|
+
ScrollEndDragEvent event{};
|
|
538
|
+
fill(event);
|
|
539
|
+
// No velocity model yet, so a drag ends where it stopped. Without a
|
|
540
|
+
// targetContentOffset the JS side treats the drag as ending immediately,
|
|
541
|
+
// which is what actually happens here.
|
|
542
|
+
event.targetContentOffset = event.contentOffset;
|
|
543
|
+
event.velocity = Point{.x = 0, .y = 0};
|
|
544
|
+
emitter->onScrollEndDrag(event);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
ScrollEvent event{};
|
|
549
|
+
fill(event);
|
|
550
|
+
const std::string_view kind(which);
|
|
551
|
+
if (kind == "beginDrag") {
|
|
552
|
+
emitter->onScrollBeginDrag(event);
|
|
553
|
+
} else if (kind == "momentumBegin") {
|
|
554
|
+
emitter->onMomentumScrollBegin(event);
|
|
555
|
+
} else if (kind == "momentumEnd") {
|
|
556
|
+
emitter->onMomentumScrollEnd(event);
|
|
557
|
+
} else {
|
|
558
|
+
emitter->onScroll(event);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
void GtkScrollViewManager::writeStateOffset(const Entry &entry) {
|
|
563
|
+
if (entry.state == nullptr) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const Point offset{.x = static_cast<Float>(entry.offsetX), .y = static_cast<Float>(entry.offsetY)};
|
|
567
|
+
|
|
568
|
+
entry.state->updateState(
|
|
569
|
+
[offset](const ScrollViewShadowNode::ConcreteState::Data &oldData)
|
|
570
|
+
-> ScrollViewShadowNode::ConcreteState::SharedData {
|
|
571
|
+
if (oldData.contentOffset == offset) {
|
|
572
|
+
// Returning null cancels the update. The callback can run more than
|
|
573
|
+
// once when commits race, so it must stay a pure function of oldData.
|
|
574
|
+
return nullptr;
|
|
575
|
+
}
|
|
576
|
+
auto newData = oldData;
|
|
577
|
+
newData.contentOffset = offset;
|
|
578
|
+
return std::make_shared<const ScrollViewShadowNode::ConcreteState::Data>(newData);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// Commands
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
bool GtkScrollViewManager::dispatchCommand(Tag tag, const std::string &name, const folly::dynamic &args) {
|
|
587
|
+
const auto it = entries_.find(tag);
|
|
588
|
+
if (it == entries_.end()) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
Entry &entry = it->second;
|
|
592
|
+
// A programmatic scroll wins over a fling, which is what an app calling
|
|
593
|
+
// scrollTo means by it. Told to JavaScript, because the fling was announced.
|
|
594
|
+
if (name == "scrollTo" || name == "scrollToEnd") {
|
|
595
|
+
stopMomentum(entry, true);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// [x, y, animated] for scrollTo, [animated] for scrollToEnd.
|
|
599
|
+
if (name == "scrollTo" && args.isArray() && args.size() >= 2) {
|
|
600
|
+
scrollTowards(entry,
|
|
601
|
+
args[0].asDouble(),
|
|
602
|
+
args[1].asDouble(),
|
|
603
|
+
args.size() >= 3 && args[2].asBool());
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (name == "scrollToEnd") {
|
|
608
|
+
scrollTowards(entry,
|
|
609
|
+
entry.contentSize.width - entry.containerSize.width,
|
|
610
|
+
entry.contentSize.height - entry.containerSize.height,
|
|
611
|
+
args.isArray() && args.size() >= 1 && args[0].asBool());
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (name == "flashScrollIndicators") {
|
|
616
|
+
// Nothing to flash: the indicator is always on screen while there is one to
|
|
617
|
+
// draw -- see updateIndicators above. Claimed and ignored rather than left
|
|
618
|
+
// to fall through, which would log an unimplemented command every time a
|
|
619
|
+
// list settles.
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
} // namespace basalt
|