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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. 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