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,296 @@
|
|
|
1
|
+
#include "GtkFocus.h"
|
|
2
|
+
|
|
3
|
+
#include "GtkKeyEvents.h"
|
|
4
|
+
|
|
5
|
+
#include <optional>
|
|
6
|
+
#include <vector>
|
|
7
|
+
|
|
8
|
+
#include <folly/dynamic.h>
|
|
9
|
+
|
|
10
|
+
#include <react/renderer/core/EventEmitter.h>
|
|
11
|
+
|
|
12
|
+
namespace basalt {
|
|
13
|
+
|
|
14
|
+
using facebook::react::EventEmitter;
|
|
15
|
+
using facebook::react::RawEvent;
|
|
16
|
+
using facebook::react::Tag;
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
// The React Native view a widget belongs to, or null. A <TextInput>'s peer is a
|
|
21
|
+
// GtkText inside an RnView, so focus landing on the peer is focus landing on
|
|
22
|
+
// the wrapper as far as React Native is concerned.
|
|
23
|
+
RnView *owningView(GtkWidget *widget) {
|
|
24
|
+
while (widget != nullptr && !RN_IS_VIEW(widget)) {
|
|
25
|
+
widget = gtk_widget_get_parent(widget);
|
|
26
|
+
}
|
|
27
|
+
return widget == nullptr ? nullptr : RN_VIEW(widget);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
} // namespace
|
|
31
|
+
|
|
32
|
+
GtkFocusManager::GtkFocusManager(GtkMountingManager *mountingManager, RnView *surfaceRoot)
|
|
33
|
+
: mountingManager_(mountingManager), surfaceRoot_(surfaceRoot) {
|
|
34
|
+
keyController_ = gtk_event_controller_key_new();
|
|
35
|
+
// Bubble rather than capture: a <TextInput> with focus must see space and
|
|
36
|
+
// Enter first, or typing a space in a field would activate it instead.
|
|
37
|
+
gtk_event_controller_set_propagation_phase(keyController_, GTK_PHASE_BUBBLE);
|
|
38
|
+
g_signal_connect(keyController_, "key-pressed", G_CALLBACK(onKeyPressed), this);
|
|
39
|
+
gtk_widget_add_controller(GTK_WIDGET(surfaceRoot_), keyController_);
|
|
40
|
+
// Weak, so it becomes NULL when the widget takes it down rather than
|
|
41
|
+
// dangling. See the destructor.
|
|
42
|
+
g_object_add_weak_pointer(G_OBJECT(keyController_),
|
|
43
|
+
reinterpret_cast<gpointer *>(&keyController_));
|
|
44
|
+
|
|
45
|
+
// The root may or may not be in a window yet, depending on the order the host
|
|
46
|
+
// built things in, so both are handled rather than one being assumed.
|
|
47
|
+
attachToWindow();
|
|
48
|
+
g_signal_connect(surfaceRoot_, "notify::root", G_CALLBACK(onRootChanged), this);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
GtkFocusManager::~GtkFocusManager() {
|
|
52
|
+
if (window_ != nullptr) {
|
|
53
|
+
g_signal_handlers_disconnect_by_data(window_, this);
|
|
54
|
+
window_ = nullptr;
|
|
55
|
+
}
|
|
56
|
+
if (surfaceRoot_ != nullptr && RN_IS_VIEW(surfaceRoot_)) {
|
|
57
|
+
g_signal_handlers_disconnect_by_data(surfaceRoot_, this);
|
|
58
|
+
}
|
|
59
|
+
// The key controller belongs to the widget, and that is exactly why it has to
|
|
60
|
+
// be disconnected: it outlives *this*, because both teardown paths reset the
|
|
61
|
+
// focus manager before destroying the window. A key press delivered in
|
|
62
|
+
// between would call `onKeyPressed` with a freed pointer.
|
|
63
|
+
//
|
|
64
|
+
// The dispatcher next door had the same hazard and it was not theoretical --
|
|
65
|
+
// a leave crossing between the two crashed in `dispatchHoverLeave`. Weak
|
|
66
|
+
// pointer rather than a flag, because the widget may have gone first and
|
|
67
|
+
// taken the controller with it.
|
|
68
|
+
if (keyController_ != nullptr) {
|
|
69
|
+
g_signal_handlers_disconnect_by_data(keyController_, this);
|
|
70
|
+
g_object_remove_weak_pointer(G_OBJECT(keyController_),
|
|
71
|
+
reinterpret_cast<gpointer *>(&keyController_));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void GtkFocusManager::onRootChanged(GObject * /*widget*/, GParamSpec * /*spec*/, gpointer userData) {
|
|
76
|
+
static_cast<GtkFocusManager *>(userData)->attachToWindow();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void GtkFocusManager::attachToWindow() {
|
|
80
|
+
GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(surfaceRoot_));
|
|
81
|
+
GtkWindow *window = GTK_IS_WINDOW(root) ? GTK_WINDOW(root) : nullptr;
|
|
82
|
+
if (window == window_) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (window_ != nullptr) {
|
|
86
|
+
g_signal_handlers_disconnect_by_data(window_, this);
|
|
87
|
+
}
|
|
88
|
+
window_ = window;
|
|
89
|
+
if (window_ == nullptr) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// One signal for the whole tree. GtkWindow reports which widget holds focus,
|
|
93
|
+
// so there is nothing to connect per view -- which matters, because views
|
|
94
|
+
// arrive and leave with every mutation and the window does not.
|
|
95
|
+
g_signal_connect(window_, "notify::focus-widget", G_CALLBACK(onFocusChanged), this);
|
|
96
|
+
refreshFocus();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void GtkFocusManager::onFocusChanged(GObject * /*window*/, GParamSpec * /*spec*/, gpointer userData) {
|
|
100
|
+
static_cast<GtkFocusManager *>(userData)->refreshFocus();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
void GtkFocusManager::refreshFocus() {
|
|
104
|
+
Tag tag = 0;
|
|
105
|
+
if (window_ != nullptr) {
|
|
106
|
+
if (RnView *view = owningView(gtk_window_get_focus(window_))) {
|
|
107
|
+
// A <TextInput> reports its own focus and blur, through the peer that
|
|
108
|
+
// actually has them. Reporting them here as well would fire onFocus twice
|
|
109
|
+
// for every field in the app.
|
|
110
|
+
if (rn_view_get_editable(view) == nullptr) {
|
|
111
|
+
tag = static_cast<Tag>(rn_view_get_tag(view));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (tag == focusedTag_) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const Tag previous = focusedTag_;
|
|
119
|
+
focusedTag_ = tag;
|
|
120
|
+
if (previous != 0) {
|
|
121
|
+
emitFocus(previous, false);
|
|
122
|
+
}
|
|
123
|
+
if (tag != 0) {
|
|
124
|
+
emitFocus(tag, true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void GtkFocusManager::emitFocus(Tag tag, bool focused) {
|
|
129
|
+
const auto emitter = mountingManager_->eventEmitterForTag(tag);
|
|
130
|
+
if (emitter == nullptr) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// `topFocus` and `topBlur`, which React Native registers as bubbling events
|
|
134
|
+
// with an empty payload. BaseViewEventEmitter has both, so nothing here has
|
|
135
|
+
// to know the names.
|
|
136
|
+
const auto view = std::dynamic_pointer_cast<const facebook::react::ViewEventEmitter>(emitter);
|
|
137
|
+
if (view == nullptr) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (focused) {
|
|
141
|
+
view->onFocus();
|
|
142
|
+
} else {
|
|
143
|
+
view->onBlur();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Tag GtkFocusManager::focusedTag() const {
|
|
148
|
+
return focusedTag_;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
gboolean GtkFocusManager::onKeyPressed(GtkEventControllerKey * /*controller*/,
|
|
152
|
+
guint keyval,
|
|
153
|
+
guint /*keycode*/,
|
|
154
|
+
GdkModifierType state,
|
|
155
|
+
gpointer userData) {
|
|
156
|
+
auto *self = static_cast<GtkFocusManager *>(userData);
|
|
157
|
+
|
|
158
|
+
// An app's own declared shortcuts first, before Escape and before activation,
|
|
159
|
+
// for the reason the AppKit host gives: an app that binds Escape means it, and
|
|
160
|
+
// a platform default that beat an explicit declaration could not be overridden.
|
|
161
|
+
if (self->handleKey(keyval, state)) {
|
|
162
|
+
return GDK_EVENT_STOP;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Escape closes the topmost <Modal> -- or rather, asks the app to. React
|
|
166
|
+
// Native's `onRequestClose` is documented as the hardware back button on
|
|
167
|
+
// Android and the swipe-down on iOS; on a desktop it is Escape, and a modal
|
|
168
|
+
// the app does not close in response stays up, which is deliberate.
|
|
169
|
+
//
|
|
170
|
+
// Handled here because this is the only key controller on the window, and
|
|
171
|
+
// because a modal is a surface-wide thing rather than a focused view's.
|
|
172
|
+
if (keyval == GDK_KEY_Escape) {
|
|
173
|
+
return self->mountingManager_->requestCloseTopModal() ? GDK_EVENT_STOP : GDK_EVENT_PROPAGATE;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (keyval != GDK_KEY_Return && keyval != GDK_KEY_KP_Enter && keyval != GDK_KEY_space) {
|
|
177
|
+
return GDK_EVENT_PROPAGATE;
|
|
178
|
+
}
|
|
179
|
+
return self->activateFocused() ? GDK_EVENT_STOP : GDK_EVENT_PROPAGATE;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
bool GtkFocusManager::handleKey(guint keyval, GdkModifierType state) {
|
|
183
|
+
// Almost every app claims nothing and presses plenty; without this the walk
|
|
184
|
+
// below would run on every keystroke in every text field.
|
|
185
|
+
if (handledKeyViewCount() == 0) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const std::optional<KeyCombination> pressed = keyCombinationFrom(keyval, state);
|
|
189
|
+
if (!pressed.has_value()) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return deliverTestKey(*pressed);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
bool GtkFocusManager::deliverTestKey(const KeyCombination &pressed) {
|
|
196
|
+
// From the focused widget outwards, so the innermost claim wins. Only RnViews,
|
|
197
|
+
// because only they carry a React tag -- the box a scrolled window puts in
|
|
198
|
+
// between is not a step in the React tree. With nothing focused this walks from
|
|
199
|
+
// nothing and the root's own claim is added below, which is the ordinary case
|
|
200
|
+
// for a window-level shortcut holder.
|
|
201
|
+
std::vector<Tag> path;
|
|
202
|
+
for (GtkWidget *widget = gtk_window_get_focus(window_); widget != nullptr;
|
|
203
|
+
widget = gtk_widget_get_parent(widget)) {
|
|
204
|
+
if (RN_IS_VIEW(widget)) {
|
|
205
|
+
path.push_back(static_cast<Tag>(rn_view_get_tag(RN_VIEW(widget))));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (path.empty() && mountingManager_ != nullptr) {
|
|
209
|
+
// Surface 1, which is the app's own -- the same constant main_gtk.cpp uses
|
|
210
|
+
// for it. A second window's surface has its own focus manager.
|
|
211
|
+
if (RnView *root = mountingManager_->getSurfaceRoot(1); root != nullptr) {
|
|
212
|
+
path.push_back(static_cast<Tag>(rn_view_get_tag(root)));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
std::optional<Tag> handler = handledBy(path, pressed);
|
|
217
|
+
if (!handler.has_value() && focusedTag_ == 0) {
|
|
218
|
+
// See the AppKit host and core/KeyEvents.h: unfocused, a window-level
|
|
219
|
+
// shortcut holder is not on any path and would otherwise never fire.
|
|
220
|
+
handler = handledByAny(pressed);
|
|
221
|
+
}
|
|
222
|
+
if (!handler.has_value()) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
reportKey(*handler, pressed);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
bool GtkFocusManager::activateFocused() {
|
|
230
|
+
if (focusedTag_ == 0) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
// True whether or not anything is listening: the key belongs to the focused
|
|
234
|
+
// view either way, and letting it travel on because a view between a Remove
|
|
235
|
+
// and its Delete has no emitter would deliver it somewhere else.
|
|
236
|
+
// A <Switch> is toggled from here too. It listens for no click -- React
|
|
237
|
+
// Native's is a controlled component driven by its own native control -- so
|
|
238
|
+
// without this a switch that Tab reaches is one Enter cannot work. The same
|
|
239
|
+
// call the touch path makes, and a no-op for every view that is not a switch.
|
|
240
|
+
mountingManager_->pressedView(focusedTag_);
|
|
241
|
+
|
|
242
|
+
const auto emitter = mountingManager_->eventEmitterForTag(focusedTag_);
|
|
243
|
+
if (emitter == nullptr) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// `topClick` with an empty payload, which is exactly what React Native for
|
|
248
|
+
// Android sends from a focusable view's OnClickListener. It cannot go through
|
|
249
|
+
// TouchEventEmitter::onClick: that carries a PointerEvent, and Pressability
|
|
250
|
+
// ignores a click with a `pointerType` on it so that a real click does not
|
|
251
|
+
// fire onPress twice. See the header.
|
|
252
|
+
emitter->dispatchEvent("click", folly::dynamic::object(), RawEvent::Category::Discrete);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
bool GtkFocusManager::moveFocus(bool forward) {
|
|
257
|
+
if (surfaceRoot_ == nullptr) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
// The blur first, before anything moves.
|
|
261
|
+
//
|
|
262
|
+
// React Native's own order is blur-then-focus, and a <TextInput> gaining
|
|
263
|
+
// focus reports it from its peer's own focus controller, which runs *before*
|
|
264
|
+
// the window gets round to saying which widget holds focus now. Left alone
|
|
265
|
+
// that produces focus-then-blur for a Tab from a button into a field -- both
|
|
266
|
+
// events, in the wrong order. A real Tab keypress is GTK's own and can still
|
|
267
|
+
// interleave that way; this is what the automated path and anything calling
|
|
268
|
+
// moveFocus get right.
|
|
269
|
+
if (focusedTag_ != 0) {
|
|
270
|
+
const Tag leaving = focusedTag_;
|
|
271
|
+
focusedTag_ = 0;
|
|
272
|
+
emitFocus(leaving, false);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// GTK's own chain, which is the point: a focusable widget joins the window's
|
|
276
|
+
// focus order, so the order here is the order Tab produces and nothing in
|
|
277
|
+
// this project decides what "next" means.
|
|
278
|
+
const GtkDirectionType direction = forward ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
|
|
279
|
+
gboolean moved = gtk_widget_child_focus(GTK_WIDGET(surfaceRoot_), direction);
|
|
280
|
+
|
|
281
|
+
// Wrapping. `gtk_widget_child_focus` on a container stops at its last child,
|
|
282
|
+
// because a real window would carry on into whatever is next -- and here the
|
|
283
|
+
// surface root *is* the window's content, so there is nothing after it. The
|
|
284
|
+
// AppKit side wraps, and a Tab order that runs out on one desktop and goes
|
|
285
|
+
// round on the other is a difference an app did not ask for.
|
|
286
|
+
if (moved == FALSE) {
|
|
287
|
+
if (window_ != nullptr) {
|
|
288
|
+
gtk_window_set_focus(window_, nullptr);
|
|
289
|
+
}
|
|
290
|
+
moved = gtk_widget_child_focus(GTK_WIDGET(surfaceRoot_), direction);
|
|
291
|
+
}
|
|
292
|
+
refreshFocus();
|
|
293
|
+
return moved != FALSE;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
} // namespace basalt
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Keyboard focus, for everything that is not a <TextInput>.
|
|
2
|
+
//
|
|
3
|
+
// A text field takes focus because it is a real GtkText and GTK gives it focus;
|
|
4
|
+
// nothing else in a React Native tree was reachable by Tab at all, and an
|
|
5
|
+
// application whose buttons cannot be reached from the keyboard is not really a
|
|
6
|
+
// desktop application.
|
|
7
|
+
//
|
|
8
|
+
// ## What makes a view focusable
|
|
9
|
+
//
|
|
10
|
+
// `accessible`. React Native has a `focusable` prop and it does not reach this
|
|
11
|
+
// platform: ReactCommon parses it only into Android's and tvOS's
|
|
12
|
+
// `HostPlatformViewProps`, and the C++ host's is a bare alias of
|
|
13
|
+
// `BaseViewProps`. So the signal has to be something that does arrive, and
|
|
14
|
+
// `accessible` is both the nearest true thing and the right one -- it is what
|
|
15
|
+
// `<Pressable>` sets on everything it renders, what an app sets on anything
|
|
16
|
+
// else it means as a control, and what a screen reader already stops on. Tab
|
|
17
|
+
// order following the accessibility tree is also what GTK and macOS do with
|
|
18
|
+
// their own widgets.
|
|
19
|
+
//
|
|
20
|
+
// ## What activating one does
|
|
21
|
+
//
|
|
22
|
+
// Dispatches `topClick` with an empty payload, which is exactly what React
|
|
23
|
+
// Native for Android does from `ReactViewManager.setFocusable`. Pressability
|
|
24
|
+
// listens for it and calls `onPress`, but only when the payload has no
|
|
25
|
+
// `pointerType` -- a click that came from a pointer would fire `onPress` twice.
|
|
26
|
+
// So this cannot go through `TouchEventEmitter::onClick`, which always carries
|
|
27
|
+
// one; it is `dispatchEvent("click", folly::dynamic::object())`, the same shape
|
|
28
|
+
// Android's ViewGroupClickEvent has.
|
|
29
|
+
//
|
|
30
|
+
// Enter and space, because those are the two keys that activate a control on
|
|
31
|
+
// every desktop and in the browser.
|
|
32
|
+
//
|
|
33
|
+
// ## What GTK contributes
|
|
34
|
+
//
|
|
35
|
+
// The chain. A focusable widget joins the window's focus order, so Tab and
|
|
36
|
+
// Shift+Tab already work and nothing here decides what "next" means -- which
|
|
37
|
+
// also means a <TextInput>'s peer sits in the same order as the buttons around
|
|
38
|
+
// it, without this file knowing that text inputs exist. The Win32 host has no
|
|
39
|
+
// such chain and has to build one; that is the difference between the two, and
|
|
40
|
+
// it is GTK's to take credit for.
|
|
41
|
+
|
|
42
|
+
#pragma once
|
|
43
|
+
|
|
44
|
+
#include "KeyEvents.h"
|
|
45
|
+
|
|
46
|
+
#include "GtkMountingManager.h"
|
|
47
|
+
#include "RnView.h"
|
|
48
|
+
|
|
49
|
+
namespace basalt {
|
|
50
|
+
|
|
51
|
+
class GtkFocusManager {
|
|
52
|
+
public:
|
|
53
|
+
GtkFocusManager(GtkMountingManager *mountingManager, RnView *surfaceRoot);
|
|
54
|
+
~GtkFocusManager();
|
|
55
|
+
|
|
56
|
+
GtkFocusManager(const GtkFocusManager &) = delete;
|
|
57
|
+
GtkFocusManager &operator=(const GtkFocusManager &) = delete;
|
|
58
|
+
GtkFocusManager(GtkFocusManager &&) = delete;
|
|
59
|
+
GtkFocusManager &operator=(GtkFocusManager &&) = delete;
|
|
60
|
+
|
|
61
|
+
// Moves focus to the next or previous focusable view, and reports whether
|
|
62
|
+
// anything took it. The keyboard's own Tab goes through GTK; this is the
|
|
63
|
+
// entry point automation uses, for the same reason `synthesiseTap` exists --
|
|
64
|
+
// a real Tab needs a window the display server considers focused, which a
|
|
65
|
+
// headless run does not have.
|
|
66
|
+
bool moveFocus(bool forward);
|
|
67
|
+
|
|
68
|
+
// Activates whatever has focus, as Enter or space does.
|
|
69
|
+
bool activateFocused();
|
|
70
|
+
|
|
71
|
+
// The tag of the focused view, or 0. For tests and for the tree dump.
|
|
72
|
+
facebook::react::Tag focusedTag() const;
|
|
73
|
+
|
|
74
|
+
// A key press. Answers whether some view from the focused one outwards declared
|
|
75
|
+
// this combination, and reports it when one did. The answer comes back in this
|
|
76
|
+
// call stack because `key-pressed` consumes or propagates by it; see
|
|
77
|
+
// core/KeyEvents.h.
|
|
78
|
+
bool handleKey(guint keyval, GdkModifierType state);
|
|
79
|
+
|
|
80
|
+
// BASALT_TEST_KEY. The same delivery a real press gets, minus the keyval
|
|
81
|
+
// translation, which tests/test_key_events_gtk.cpp covers instead. See
|
|
82
|
+
// core/TestKeys.h.
|
|
83
|
+
bool deliverTestKey(const KeyCombination &pressed);
|
|
84
|
+
|
|
85
|
+
private:
|
|
86
|
+
static gboolean onKeyPressed(GtkEventControllerKey *controller,
|
|
87
|
+
guint keyval,
|
|
88
|
+
guint keycode,
|
|
89
|
+
GdkModifierType state,
|
|
90
|
+
gpointer userData);
|
|
91
|
+
static void onFocusChanged(GObject *window, GParamSpec *spec, gpointer userData);
|
|
92
|
+
static void onRootChanged(GObject *widget, GParamSpec *spec, gpointer userData);
|
|
93
|
+
|
|
94
|
+
// Connects to the window's focus-widget notification. One signal for the
|
|
95
|
+
// whole tree, rather than one per view: views come and go with every
|
|
96
|
+
// mutation, and the window does not.
|
|
97
|
+
void attachToWindow();
|
|
98
|
+
|
|
99
|
+
// Works out which React Native view holds focus now and reports the change.
|
|
100
|
+
void refreshFocus();
|
|
101
|
+
|
|
102
|
+
void emitFocus(facebook::react::Tag tag, bool focused);
|
|
103
|
+
|
|
104
|
+
GtkMountingManager *mountingManager_;
|
|
105
|
+
RnView *surfaceRoot_;
|
|
106
|
+
GtkEventController *keyController_{nullptr};
|
|
107
|
+
// The window this is listening to, held weakly: it outlives the manager in
|
|
108
|
+
// the normal case and does not during teardown.
|
|
109
|
+
GtkWindow *window_{nullptr};
|
|
110
|
+
facebook::react::Tag focusedTag_{0};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
} // namespace basalt
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#include "GtkImageLoader.h"
|
|
2
|
+
|
|
3
|
+
#include "ImageBytes.h"
|
|
4
|
+
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <thread>
|
|
7
|
+
#include <utility>
|
|
8
|
+
|
|
9
|
+
namespace basalt {
|
|
10
|
+
|
|
11
|
+
// Carries one completed load from the worker thread to the main thread.
|
|
12
|
+
struct GtkImageLoader::Pending {
|
|
13
|
+
GtkImageLoader *loader;
|
|
14
|
+
std::string uri;
|
|
15
|
+
std::string bytes;
|
|
16
|
+
std::string error;
|
|
17
|
+
Callback callback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
GtkImageLoader::GtkImageLoader() = default;
|
|
21
|
+
|
|
22
|
+
GtkImageLoader::~GtkImageLoader() {
|
|
23
|
+
for (auto &[uri, texture] : cache_) {
|
|
24
|
+
g_clear_object(&texture);
|
|
25
|
+
}
|
|
26
|
+
cache_.clear();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Caches the texture and drops whatever that pushed out. See core/ImageCache.h
|
|
30
|
+
// for the policy; the bytes are this file's to measure, because only GDK knows
|
|
31
|
+
// how big a decoded texture is.
|
|
32
|
+
void GtkImageLoader::remember(const std::string &uri, GdkTexture *texture) {
|
|
33
|
+
cache_[uri] = texture;
|
|
34
|
+
|
|
35
|
+
// Four bytes a pixel, which is what GDK decodes to. Not asked of the texture
|
|
36
|
+
// because `gdk_texture_get_format` reports the *source* format and the
|
|
37
|
+
// memory is what matters here.
|
|
38
|
+
const size_t bytes = static_cast<size_t>(gdk_texture_get_width(texture)) *
|
|
39
|
+
static_cast<size_t>(gdk_texture_get_height(texture)) * 4u;
|
|
40
|
+
|
|
41
|
+
for (const std::string &evicted : policy_.insert(uri, bytes)) {
|
|
42
|
+
const auto it = cache_.find(evicted);
|
|
43
|
+
if (it != cache_.end()) {
|
|
44
|
+
g_clear_object(&it->second);
|
|
45
|
+
cache_.erase(it);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
gboolean GtkImageLoader::deliver(gpointer data) {
|
|
51
|
+
auto *pending = static_cast<Pending *>(data);
|
|
52
|
+
|
|
53
|
+
GdkTexture *texture = nullptr;
|
|
54
|
+
std::string error = pending->error;
|
|
55
|
+
|
|
56
|
+
if (error.empty()) {
|
|
57
|
+
// Decoding happens here rather than on the worker thread: GdkTexture is a
|
|
58
|
+
// GObject, and creating one is only safe once, on the thread that will use
|
|
59
|
+
// it. The expensive part -- the I/O -- already happened off-thread.
|
|
60
|
+
GBytes *bytes = g_bytes_new(pending->bytes.data(), pending->bytes.size());
|
|
61
|
+
GError *decodeError = nullptr;
|
|
62
|
+
texture = gdk_texture_new_from_bytes(bytes, &decodeError);
|
|
63
|
+
g_bytes_unref(bytes);
|
|
64
|
+
if (texture == nullptr) {
|
|
65
|
+
error = decodeError != nullptr ? decodeError->message : "could not decode image";
|
|
66
|
+
g_clear_error(&decodeError);
|
|
67
|
+
} else {
|
|
68
|
+
pending->loader->remember(pending->uri, texture);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pending->callback(texture, error);
|
|
73
|
+
delete pending;
|
|
74
|
+
return G_SOURCE_REMOVE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void GtkImageLoader::load(const std::string &uri, Callback &&callback) {
|
|
78
|
+
if (uri.empty()) {
|
|
79
|
+
callback(nullptr, "empty source uri");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (const auto it = cache_.find(uri); it != cache_.end()) {
|
|
84
|
+
policy_.noteUse(uri);
|
|
85
|
+
callback(it->second, {});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
auto *pending = new Pending{this, uri, {}, {}, std::move(callback)};
|
|
90
|
+
|
|
91
|
+
std::thread([pending]() {
|
|
92
|
+
// The fetch is in core, shared with the AppKit loader: a URI means the same
|
|
93
|
+
// thing on both, and only the decode below differs.
|
|
94
|
+
fetchImageBytes(pending->uri, &pending->bytes, &pending->error);
|
|
95
|
+
g_idle_add_full(G_PRIORITY_DEFAULT, deliver, pending, nullptr);
|
|
96
|
+
}).detach();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
void GtkImageLoader::loadImage(const std::string &uri,
|
|
101
|
+
const facebook::react::IImageLoaderOnLoadCallback &&onLoad) {
|
|
102
|
+
// Copied because the callback outlives this call: `load` may answer on a
|
|
103
|
+
// later turn of the main loop, and the reference it was handed is gone by
|
|
104
|
+
// then.
|
|
105
|
+
auto callback = onLoad;
|
|
106
|
+
load(uri, [callback](GdkTexture *texture, const std::string &error) {
|
|
107
|
+
if (texture == nullptr) {
|
|
108
|
+
// The message rather than a size: `Image.getSize` rejects with it, and a
|
|
109
|
+
// zero-by-zero success would be indistinguishable from a 0x0 image.
|
|
110
|
+
callback(0.0, 0.0, error.empty() ? "could not load image" : error.c_str());
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
callback(static_cast<double>(gdk_texture_get_width(texture)),
|
|
114
|
+
static_cast<double>(gdk_texture_get_height(texture)),
|
|
115
|
+
nullptr);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
facebook::react::IImageLoader::CacheStatus GtkImageLoader::getCacheStatus(
|
|
120
|
+
const std::string &uri) {
|
|
121
|
+
// Memory or nothing: there is no disk cache here, and saying `Disk` would be
|
|
122
|
+
// claiming a persistence this has not got.
|
|
123
|
+
return cache_.find(uri) != cache_.end()
|
|
124
|
+
? facebook::react::IImageLoader::CacheStatus::Memory
|
|
125
|
+
: facebook::react::IImageLoader::CacheStatus::None;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
} // namespace basalt
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Loading the pixels behind <Image>.
|
|
2
|
+
//
|
|
3
|
+
// React Native's cxx platform does not do this for us. `ImageManager` has a
|
|
4
|
+
// platform variant under `imagemanager/platform/cxx`, and every method in it is
|
|
5
|
+
// a stub that returns an empty `ImageRequest` -- so no `ImageResponse` ever
|
|
6
|
+
// arrives, and `ImageState` never carries anything to render. The platform view
|
|
7
|
+
// is expected to load its own image, which is also what Android does (Fresco,
|
|
8
|
+
// from `ReactImageView`, not from the shadow node).
|
|
9
|
+
//
|
|
10
|
+
// So the mounting manager reads the URI straight off `ImageProps::sources` and
|
|
11
|
+
// asks this to produce a texture.
|
|
12
|
+
//
|
|
13
|
+
// Scheme support:
|
|
14
|
+
// file:// read from disk
|
|
15
|
+
// http, https fetched with libcurl, on a worker thread
|
|
16
|
+
// data: decoded inline
|
|
17
|
+
// bare paths treated as file paths, which is what a `require()`d asset
|
|
18
|
+
// looks like once Metro has resolved it in a release bundle
|
|
19
|
+
//
|
|
20
|
+
// Everything is decoded off the main thread and delivered back onto it, because
|
|
21
|
+
// GdkTexture is cheap to hand around but decoding a large PNG is not something
|
|
22
|
+
// to do in a mount transaction.
|
|
23
|
+
|
|
24
|
+
#pragma once
|
|
25
|
+
|
|
26
|
+
#include <gtk/gtk.h>
|
|
27
|
+
|
|
28
|
+
#include <functional>
|
|
29
|
+
#include <memory>
|
|
30
|
+
#include <string>
|
|
31
|
+
#include <unordered_map>
|
|
32
|
+
|
|
33
|
+
#include "ImageCache.h"
|
|
34
|
+
|
|
35
|
+
#include <react/io/IImageLoader.h>
|
|
36
|
+
|
|
37
|
+
namespace basalt {
|
|
38
|
+
|
|
39
|
+
// Also React Native's own `IImageLoader`, which is what `Image.getSize` and
|
|
40
|
+
// `Image.prefetch` reach. Upstream's `ImageLoaderModule` takes one and
|
|
41
|
+
// `ReactCxxTurboModuleProvider` never supplies it -- see
|
|
42
|
+
// docs/backlog/upstream.md -- so the host registers the module itself with
|
|
43
|
+
// this as the loader.
|
|
44
|
+
class GtkImageLoader : public facebook::react::IImageLoader {
|
|
45
|
+
public:
|
|
46
|
+
// `texture` is null when the load failed, and `error` says why. Always called
|
|
47
|
+
// on the GTK main thread, possibly synchronously if the image is cached.
|
|
48
|
+
using Callback = std::function<void(GdkTexture *texture, const std::string &error)>;
|
|
49
|
+
|
|
50
|
+
GtkImageLoader();
|
|
51
|
+
~GtkImageLoader();
|
|
52
|
+
|
|
53
|
+
GtkImageLoader(const GtkImageLoader &) = delete;
|
|
54
|
+
GtkImageLoader &operator=(const GtkImageLoader &) = delete;
|
|
55
|
+
GtkImageLoader(GtkImageLoader &&) = delete;
|
|
56
|
+
GtkImageLoader &operator=(GtkImageLoader &&) = delete;
|
|
57
|
+
|
|
58
|
+
void load(const std::string &uri, Callback &&callback);
|
|
59
|
+
|
|
60
|
+
// --- IImageLoader ----------------------------------------------------------
|
|
61
|
+
//
|
|
62
|
+
// The same decode as `load`, reporting the size rather than the pixels. It
|
|
63
|
+
// goes through the same cache, so `Image.getSize` on something already on
|
|
64
|
+
// screen answers without touching the disk or the network.
|
|
65
|
+
void loadImage(const std::string &uri,
|
|
66
|
+
const facebook::react::IImageLoaderOnLoadCallback &&onLoad) override;
|
|
67
|
+
|
|
68
|
+
facebook::react::IImageLoader::CacheStatus getCacheStatus(const std::string &uri) override;
|
|
69
|
+
|
|
70
|
+
private:
|
|
71
|
+
struct Pending;
|
|
72
|
+
|
|
73
|
+
static gboolean deliver(gpointer data);
|
|
74
|
+
|
|
75
|
+
// Decoded textures, keyed by URI. Textures are immutable and shareable, so
|
|
76
|
+
// two <Image>s with the same source paint the same object.
|
|
77
|
+
//
|
|
78
|
+
// Caches a texture and releases whatever the policy dropped.
|
|
79
|
+
void remember(const std::string &uri, GdkTexture *texture);
|
|
80
|
+
|
|
81
|
+
std::unordered_map<std::string, GdkTexture *> cache_;
|
|
82
|
+
// Which URI goes next, and when. The textures are this class's; the decision
|
|
83
|
+
// is shared with the other two hosts. See core/ImageCache.h.
|
|
84
|
+
basalt::ImageCachePolicy policy_;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
} // namespace basalt
|