basalt-gtk 0.0.0-rc5 → 0.1.0

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 (41) hide show
  1. package/native/CMakeLists.txt +16 -0
  2. package/native/gtk/ComponentRegistryGtk.cpp +20 -0
  3. package/native/gtk/FontRegistryFontconfig.cpp +10 -2
  4. package/native/gtk/GtkFocus.cpp +22 -1
  5. package/native/gtk/GtkFocus.h +3 -0
  6. package/native/gtk/GtkImageLoader.cpp +75 -4
  7. package/native/gtk/GtkImageLoader.h +38 -1
  8. package/native/gtk/GtkMountingManager.cpp +622 -18
  9. package/native/gtk/GtkMountingManager.h +34 -0
  10. package/native/gtk/GtkScrollView.cpp +212 -2
  11. package/native/gtk/GtkScrollView.h +48 -0
  12. package/native/gtk/GtkTextInput.cpp +347 -21
  13. package/native/gtk/GtkTextInput.h +67 -0
  14. package/native/gtk/GtkTextPeer.cpp +385 -11
  15. package/native/gtk/GtkTextPeer.h +88 -0
  16. package/native/gtk/GtkTouchDispatcher.cpp +207 -1
  17. package/native/gtk/GtkTouchDispatcher.h +47 -0
  18. package/native/gtk/PangoTextLayout.cpp +611 -82
  19. package/native/gtk/PangoTextLayout.h +54 -1
  20. package/native/gtk/PangoTextLayoutManager.cpp +35 -13
  21. package/native/gtk/PlatformServicesGtk.cpp +416 -51
  22. package/native/gtk/RnTextDashes.h +65 -0
  23. package/native/gtk/RnView.cpp +2583 -22
  24. package/native/gtk/RnView.h +510 -0
  25. package/native/gtk/main_gtk.cpp +276 -3
  26. package/native/tests/GtkPixels.h +192 -0
  27. package/native/tests/TestMainGtk.cpp +13 -0
  28. package/native/tests/test_accessibility.cpp +519 -0
  29. package/native/tests/test_animated_image.cpp +330 -0
  30. package/native/tests/test_focus.cpp +101 -0
  31. package/native/tests/test_fonts.cpp +247 -0
  32. package/native/tests/test_gtk_clipboard.cpp +117 -0
  33. package/native/tests/test_gtk_paint.cpp +1018 -0
  34. package/native/tests/test_hittest.cpp +107 -0
  35. package/native/tests/test_mounting.cpp +79 -0
  36. package/native/tests/test_scrollview.cpp +485 -7
  37. package/native/tests/test_selection.cpp +476 -0
  38. package/native/tests/test_text.cpp +1532 -0
  39. package/native/tests/test_textinput.cpp +725 -0
  40. package/native/tests/test_viewprops.cpp +1498 -0
  41. package/package.json +4 -3
@@ -323,19 +323,35 @@ if(TARGET basalt_core)
323
323
  tests/test_mounting.cpp
324
324
  tests/test_view.cpp
325
325
  tests/test_text.cpp
326
+ tests/test_selection.cpp
327
+ # Fonts loaded at runtime: core/FontRegistry.h's GTK half, the Pango
328
+ # context a late registration has to reload, and the measurement cache it
329
+ # has to empty.
330
+ tests/test_fonts.cpp
326
331
  tests/test_focus.cpp
327
332
  tests/test_key_events_gtk.cpp
328
333
  tests/test_hittest.cpp
329
334
  tests/test_scrollview.cpp
330
335
  tests/test_image.cpp
336
+ # Animated images: gdk-pixbuf's frames, paced by core's clamp. Real pixels,
337
+ # because a view that advanced its iterator and kept painting the first
338
+ # texture would pass on the frame index alone.
339
+ tests/test_animated_image.cpp
331
340
  tests/test_accessibility.cpp
332
341
  tests/test_viewprops.cpp
342
+ # What reached the pixels, asking what tests/test_win32_paint.cpp asks of
343
+ # Direct2D. No display and no window: see tests/GtkPixels.h.
344
+ tests/test_gtk_paint.cpp
333
345
  tests/test_textinput.cpp
334
346
  tests/test_titlebar.cpp
335
347
  # The drag regions, asking what tests/test_win32_titlebar.cpp asks of
336
348
  # Windows. Needs a shown window: it walks up from gtk_widget_pick.
337
349
  tests/test_titlebar_drag.cpp
338
350
  tests/test_version.cpp
351
+ # The clipboard seam and which thread it runs on, asking what
352
+ # tests/test_win32_clipboard.cpp asks of Windows. Uses the real clipboard and
353
+ # puts back what was there.
354
+ tests/test_gtk_clipboard.cpp
339
355
  # Hit testing lives with the touch dispatcher, which the host builds; the
340
356
  # tests need it too, and it pulls in nothing else.
341
357
  gtk/GtkFocus.cpp
@@ -3,6 +3,8 @@
3
3
 
4
4
  #include "ComponentRegistry.h"
5
5
  #include "ExpoImageComponent.h"
6
+ #include "SafeAreaComponent.h"
7
+ #include "ScreensComponent.h"
6
8
 
7
9
  #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
8
10
  #include <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
@@ -24,6 +26,24 @@ ComponentRegistryFactory getDefaultComponentRegistryFactory() {
24
26
  static auto providerRegistry = []() {
25
27
  auto registry = std::make_shared<ComponentDescriptorProviderRegistry>();
26
28
  registry->add(concreteComponentDescriptorProvider<ViewComponentDescriptor>());
29
+ // react-native-safe-area-context's provider. Registered on every
30
+ // platform, like expo-image's above: the descriptor is ordinary Fabric
31
+ // C++ and an app that never mounts one pays a registry entry. Without
32
+ // it the provider renders its children never, not badly. See
33
+ // core/SafeAreaComponent.h.
34
+ registry->add(concreteComponentDescriptorProvider<SafeAreaProviderComponentDescriptor>());
35
+ // react-native-screens. RNSScreen carries activityState, which is what
36
+ // hides the screen behind the one in front; the rest are containers that
37
+ // only need their names known, since an unregistered name becomes
38
+ // UnimplementedNativeView and carries none of the layout props. See
39
+ // core/ScreensComponent.h.
40
+ registry->add(concreteComponentDescriptorProvider<RNSScreenComponentDescriptor>());
41
+ registry->add(concreteComponentDescriptorProvider<RNSScreenStackComponentDescriptor>());
42
+ registry->add(concreteComponentDescriptorProvider<RNSScreenContainerComponentDescriptor>());
43
+ registry->add(concreteComponentDescriptorProvider<RNSScreenNavigationContainerComponentDescriptor>());
44
+ registry->add(concreteComponentDescriptorProvider<RNSScreenContentWrapperComponentDescriptor>());
45
+ registry->add(concreteComponentDescriptorProvider<RNSScreenStackHeaderConfigComponentDescriptor>());
46
+ registry->add(concreteComponentDescriptorProvider<RNSScreenStackHeaderSubviewComponentDescriptor>());
27
47
  // Text is three descriptors, and only one of them mounts. <Text> becomes
28
48
  // a Text node, its string a RawText node, and the outermost <Text>
29
49
  // becomes a Paragraph that folds the whole subtree into a single
@@ -82,8 +82,16 @@ bool registerFont(const std::string &name, const std::string &path) {
82
82
 
83
83
  // Pango caches which fonts exist. Adding one to fontconfig behind its back
84
84
  // leaves it looking at the old list, so the family would not be found until
85
- // something else happened to invalidate it. Only meaningful for a fontconfig
86
- // backed map, which is not the only kind Pango has.
85
+ // something else happened to invalidate it.
86
+ //
87
+ // Only meaningful for a fontconfig backed map, which is not the only kind
88
+ // Pango has, and the other kind is reachable: on a Mac, Homebrew's Pango
89
+ // answers this with a `PangoCairoCoreTextFontMap`, which does not consult
90
+ // fontconfig at all. A font loaded at runtime is then registered here and
91
+ // invisible to the text engine. That is a property of running the GTK host on
92
+ // macOS, which is a comparison arrangement rather than a target, and
93
+ // tests/test_fonts.cpp stops at the mapping when it sees that map. docs/
94
+ // TESTING.md records it.
87
95
  PangoFontMap *fontMap = pango_cairo_font_map_get_default();
88
96
  if (PANGO_IS_FC_FONT_MAP(fontMap)) {
89
97
  pango_fc_font_map_config_changed(PANGO_FC_FONT_MAP(fontMap));
@@ -42,6 +42,13 @@ GtkFocusManager::GtkFocusManager(GtkMountingManager *mountingManager, RnView *su
42
42
  g_object_add_weak_pointer(G_OBJECT(keyController_),
43
43
  reinterpret_cast<gpointer *>(&keyController_));
44
44
 
45
+ // Weak for the reason the controller above is: the root can be destroyed
46
+ // before this manager is, and then every line of the destructor is reading
47
+ // freed memory. GObject nulls the pointer on finalise, which is the only way
48
+ // to know -- see the destructor.
49
+ g_object_add_weak_pointer(G_OBJECT(surfaceRoot_),
50
+ reinterpret_cast<gpointer *>(&surfaceRoot_));
51
+
45
52
  // The root may or may not be in a window yet, depending on the order the host
46
53
  // built things in, so both are handled rather than one being assumed.
47
54
  attachToWindow();
@@ -53,8 +60,19 @@ GtkFocusManager::~GtkFocusManager() {
53
60
  g_signal_handlers_disconnect_by_data(window_, this);
54
61
  window_ = nullptr;
55
62
  }
56
- if (surfaceRoot_ != nullptr && RN_IS_VIEW(surfaceRoot_)) {
63
+ // Only if the root is still there, which the weak pointer is what says.
64
+ //
65
+ // This used to ask `RN_IS_VIEW(surfaceRoot_)`, which cannot work and is worth
66
+ // recording as a trap: a type check reads the instance's type pointer out of
67
+ // the object, so on a freed GObject it reads GLib's 0xaa poison and
68
+ // dereferences it. The crash was `SIGSEGV at 0xaaaaaaaaaaaaaaaa` inside
69
+ // RN_IS_VIEW, from this destructor, and it was latent for as long as the
70
+ // freed bytes at that offset happened not to fault.
71
+ if (surfaceRoot_ != nullptr) {
57
72
  g_signal_handlers_disconnect_by_data(surfaceRoot_, this);
73
+ g_object_remove_weak_pointer(G_OBJECT(surfaceRoot_),
74
+ reinterpret_cast<gpointer *>(&surfaceRoot_));
75
+ surfaceRoot_ = nullptr;
58
76
  }
59
77
  // The key controller belongs to the widget, and that is exactly why it has to
60
78
  // be disconnected: it outlives *this*, because both teardown paths reset the
@@ -77,6 +95,9 @@ void GtkFocusManager::onRootChanged(GObject * /*widget*/, GParamSpec * /*spec*/,
77
95
  }
78
96
 
79
97
  void GtkFocusManager::attachToWindow() {
98
+ if (surfaceRoot_ == nullptr) {
99
+ return;
100
+ }
80
101
  GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(surfaceRoot_));
81
102
  GtkWindow *window = GTK_IS_WINDOW(root) ? GTK_WINDOW(root) : nullptr;
82
103
  if (window == window_) {
@@ -102,6 +102,9 @@ class GtkFocusManager {
102
102
  void emitFocus(facebook::react::Tag tag, bool focused);
103
103
 
104
104
  GtkMountingManager *mountingManager_;
105
+ // The surface root, held weakly: GObject nulls this when the view is
106
+ // finalised, which is what makes the destructor safe to run after the root
107
+ // has gone. A type check cannot do that job -- see the destructor.
105
108
  RnView *surfaceRoot_;
106
109
  GtkEventController *keyController_{nullptr};
107
110
  // The window this is listening to, held weakly: it outlives the manager in
@@ -15,6 +15,9 @@ struct GtkImageLoader::Pending {
15
15
  std::string bytes;
16
16
  std::string error;
17
17
  Callback callback;
18
+ // Called from the worker thread as the bytes arrive, rather than from
19
+ // `deliver` below: see the header.
20
+ basalt::ImageProgress onProgress;
18
21
  };
19
22
 
20
23
  GtkImageLoader::GtkImageLoader() = default;
@@ -24,6 +27,10 @@ GtkImageLoader::~GtkImageLoader() {
24
27
  g_clear_object(&texture);
25
28
  }
26
29
  cache_.clear();
30
+ for (auto &[uri, animation] : animations_) {
31
+ g_clear_object(&animation);
32
+ }
33
+ animations_.clear();
27
34
  }
28
35
 
29
36
  // Caches the texture and drops whatever that pushed out. See core/ImageCache.h
@@ -44,6 +51,11 @@ void GtkImageLoader::remember(const std::string &uri, GdkTexture *texture) {
44
51
  g_clear_object(&it->second);
45
52
  cache_.erase(it);
46
53
  }
54
+ const auto animated = animations_.find(evicted);
55
+ if (animated != animations_.end()) {
56
+ g_clear_object(&animated->second);
57
+ animations_.erase(animated);
58
+ }
47
59
  }
48
60
  }
49
61
 
@@ -60,13 +72,23 @@ gboolean GtkImageLoader::deliver(gpointer data) {
60
72
  GBytes *bytes = g_bytes_new(pending->bytes.data(), pending->bytes.size());
61
73
  GError *decodeError = nullptr;
62
74
  texture = gdk_texture_new_from_bytes(bytes, &decodeError);
63
- g_bytes_unref(bytes);
64
75
  if (texture == nullptr) {
65
76
  error = decodeError != nullptr ? decodeError->message : "could not decode image";
66
77
  g_clear_error(&decodeError);
67
78
  } else {
79
+ // The same bytes again, as an animation. A second decode rather than one
80
+ // path for both, because `gdk_texture_new_from_bytes` is what every
81
+ // still image goes through and this must not change what it produces:
82
+ // gdk-pixbuf's loaders and GDK's are not the same set, and a PNG that
83
+ // decodes one way may not decode the other.
84
+ //
85
+ // Only one frame's worth of extra work for a still image, which is what
86
+ // nearly every <Image> is: `is_static_image` is the first thing asked,
87
+ // and gdk-pixbuf has already stopped at the first frame to answer it.
88
+ pending->loader->rememberAnimation(pending->uri, bytes);
68
89
  pending->loader->remember(pending->uri, texture);
69
90
  }
91
+ g_bytes_unref(bytes);
70
92
  }
71
93
 
72
94
  pending->callback(texture, error);
@@ -74,7 +96,9 @@ gboolean GtkImageLoader::deliver(gpointer data) {
74
96
  return G_SOURCE_REMOVE;
75
97
  }
76
98
 
77
- void GtkImageLoader::load(const std::string &uri, Callback &&callback) {
99
+ void GtkImageLoader::load(const std::string &uri,
100
+ Callback &&callback,
101
+ basalt::ImageProgress onProgress) {
78
102
  if (uri.empty()) {
79
103
  callback(nullptr, "empty source uri");
80
104
  return;
@@ -86,12 +110,12 @@ void GtkImageLoader::load(const std::string &uri, Callback &&callback) {
86
110
  return;
87
111
  }
88
112
 
89
- auto *pending = new Pending{this, uri, {}, {}, std::move(callback)};
113
+ auto *pending = new Pending{this, uri, {}, {}, std::move(callback), std::move(onProgress)};
90
114
 
91
115
  std::thread([pending]() {
92
116
  // The fetch is in core, shared with the AppKit loader: a URI means the same
93
117
  // thing on both, and only the decode below differs.
94
- fetchImageBytes(pending->uri, &pending->bytes, &pending->error);
118
+ fetchImageBytes(pending->uri, &pending->bytes, &pending->error, pending->onProgress);
95
119
  g_idle_add_full(G_PRIORITY_DEFAULT, deliver, pending, nullptr);
96
120
  }).detach();
97
121
  }
@@ -116,6 +140,53 @@ void GtkImageLoader::loadImage(const std::string &uri,
116
140
  });
117
141
  }
118
142
 
143
+ // Decodes `bytes` as an animation and keeps it when it turns out to be one.
144
+ //
145
+ // Through a memory input stream, which is the only entry point gdk-pixbuf
146
+ // offers for animation-from-bytes. It reads synchronously, and on the main
147
+ // thread, for the same reason the texture above is decoded there: what comes
148
+ // out is a GObject whose frames become GdkTextures, and both are only safe on
149
+ // the thread that will use them. The fetch, which is the slow part, already
150
+ // happened on the worker.
151
+ void GtkImageLoader::rememberAnimation(const std::string &uri, GBytes *bytes) {
152
+ if (animations_.find(uri) != animations_.end()) {
153
+ return;
154
+ }
155
+
156
+ // gdk-pixbuf 2.44 deprecated its whole animation API, with nothing in
157
+ // gdk-pixbuf to replace it: GNOME's direction is glycin, a separate library
158
+ // and a new dependency, and GTK4 itself has no frame source for a GIF at all
159
+ // -- `gtk_image_set_from_file` on one shows a still. So this uses the
160
+ // deprecated API deliberately rather than by accident, and
161
+ // docs/backlog/image.md records what replacing it would take.
162
+ G_GNUC_BEGIN_IGNORE_DEPRECATIONS
163
+
164
+ GInputStream *stream = g_memory_input_stream_new_from_bytes(bytes);
165
+ GError *error = nullptr;
166
+ GdkPixbufAnimation *animation =
167
+ gdk_pixbuf_animation_new_from_stream(stream, nullptr, &error);
168
+ g_object_unref(stream);
169
+ if (animation == nullptr) {
170
+ // Not an error worth reporting: the still decode above already succeeded,
171
+ // and a format gdk-pixbuf has no loader for is simply not animated.
172
+ g_clear_error(&error);
173
+ return;
174
+ }
175
+
176
+ if (gdk_pixbuf_animation_is_static_image(animation) == TRUE) {
177
+ g_object_unref(animation);
178
+ return;
179
+ }
180
+
181
+ animations_[uri] = animation;
182
+ G_GNUC_END_IGNORE_DEPRECATIONS
183
+ }
184
+
185
+ GdkPixbufAnimation *GtkImageLoader::animation(const std::string &uri) {
186
+ const auto found = animations_.find(uri);
187
+ return found == animations_.end() ? nullptr : found->second;
188
+ }
189
+
119
190
  facebook::react::IImageLoader::CacheStatus GtkImageLoader::getCacheStatus(
120
191
  const std::string &uri) {
121
192
  // Memory or nothing: there is no disk cache here, and saying `Disk` would be
@@ -30,6 +30,7 @@
30
30
  #include <string>
31
31
  #include <unordered_map>
32
32
 
33
+ #include "ImageBytes.h"
33
34
  #include "ImageCache.h"
34
35
 
35
36
  #include <react/io/IImageLoader.h>
@@ -55,7 +56,32 @@ class GtkImageLoader : public facebook::react::IImageLoader {
55
56
  GtkImageLoader(GtkImageLoader &&) = delete;
56
57
  GtkImageLoader &operator=(GtkImageLoader &&) = delete;
57
58
 
58
- void load(const std::string &uri, Callback &&callback);
59
+ // `onProgress` is optional and is `basalt::ImageProgress`: how much of an
60
+ // http fetch has arrived, called from the fetching thread. Only an http URI
61
+ // reports anything; see core/ImageBytes.h.
62
+ //
63
+ // Not marshalled to the main thread, unlike the callback above, and that is
64
+ // the point of it: a Fabric event emitter may be dispatched from any thread,
65
+ // and a progress tick that had to wait for the main loop would arrive after
66
+ // the image it is about.
67
+ void load(const std::string &uri,
68
+ Callback &&callback,
69
+ basalt::ImageProgress onProgress = {});
70
+
71
+ // The animation behind an animated image, or null.
72
+ //
73
+ // Separate from `load` because being animated is a property of the file
74
+ // rather than of the request: `load` answers with the first frame, which is
75
+ // what a still <Image> paints and what an animated one starts on, and this
76
+ // says whether there is more. Both come out of the same decode.
77
+ //
78
+ // gdk-pixbuf owns the frames, and offers an iterator with a clock rather than
79
+ // indexed access, which is why this hands over the animation itself where the
80
+ // AppKit loader hands over an array of frames. The object is the loader's and
81
+ // is the same object each time, which is what lets a view tell a re-mount
82
+ // from a new animation. Good until the next load, since an eviction can take
83
+ // it away.
84
+ GdkPixbufAnimation *animation(const std::string &uri);
59
85
 
60
86
  // --- IImageLoader ----------------------------------------------------------
61
87
  //
@@ -77,8 +103,19 @@ class GtkImageLoader : public facebook::react::IImageLoader {
77
103
  //
78
104
  // Caches a texture and releases whatever the policy dropped.
79
105
  void remember(const std::string &uri, GdkTexture *texture);
106
+ void rememberAnimation(const std::string &uri, GBytes *bytes);
80
107
 
81
108
  std::unordered_map<std::string, GdkTexture *> cache_;
109
+ // Only the files that turned out to have more than one frame, so a still
110
+ // image costs nothing. Keyed by the same URI and dropped by the same
111
+ // eviction.
112
+ //
113
+ // The frames inside are gdk-pixbuf's and are not measured by the cache
114
+ // policy: there is no way to ask a GdkPixbufAnimation how much it is holding,
115
+ // and the AppKit side can because it decoded the frames itself. So an
116
+ // animated image is accounted as its first frame on this host. Recorded in
117
+ // docs/backlog/image.md rather than guessed at.
118
+ std::unordered_map<std::string, GdkPixbufAnimation *> animations_;
82
119
  // Which URI goes next, and when. The textures are this class's; the decision
83
120
  // is shared with the other two hosts. See core/ImageCache.h.
84
121
  basalt::ImageCachePolicy policy_;