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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vince Vella
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `react-native run-linux`.
3
+ *
4
+ * Four strings and a path. Everything a desktop run actually does is in
5
+ * basalt-core's cli/desktop.js, shared with `run-macos` and
6
+ * `run-windows`, because none of it is about a toolkit -- see the note at the
7
+ * top of that file.
8
+ *
9
+ * @format
10
+ */
11
+
12
+ // @ts-check
13
+ 'use strict';
14
+
15
+ const path = require('path');
16
+
17
+ const {shared, sharedPackageDir} = require('./shared');
18
+
19
+ const {makeRunCommand} = shared('cli/desktop');
20
+
21
+ module.exports = makeRunCommand({
22
+ platform: 'linux',
23
+ label: 'Linux',
24
+ command: 'run-linux',
25
+ binary: 'basalt_gtk',
26
+ nativeDir: path.resolve(__dirname, '..', 'native'),
27
+ coreDir: path.join(sharedPackageDir(), 'native'),
28
+ toolchain: 'a C++20 compiler and the GTK4 and Pango development packages',
29
+ });
package/cli/shared.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Reaching basalt-core, the shared half, from this package.
3
+ *
4
+ * The one file in a platform package's CLI that cannot itself be shared,
5
+ * because it is what finds the thing everything else is shared through. Two
6
+ * layouts matter and only one of them resolves by name.
7
+ *
8
+ * Installed into an app, basalt-core is a sibling in node_modules and
9
+ * `require('basalt-core/...')` finds it. In a checkout of this
10
+ * repository the two packages are sibling directories with nothing installed
11
+ * between them -- deliberately, since this repo has no node_modules of its own
12
+ * -- and the same require fails. So: by name first, because that is the layout
13
+ * an app is in, and beside us second, because that is the layout it is
14
+ * developed in.
15
+ *
16
+ * Both reach the *built* package. basalt-core is TypeScript: by name
17
+ * resolves through its `exports` map, which points into `dist/`, and the
18
+ * sibling path has to say so itself. A checkout that has not run
19
+ * scripts/build_ts.sh has nothing here to find, which is what the error says.
20
+ *
21
+ * @format
22
+ */
23
+
24
+ // @ts-check
25
+ 'use strict';
26
+
27
+ const path = require('path');
28
+
29
+ const SIBLING = path.resolve(__dirname, '..', '..', 'basalt-core');
30
+
31
+ /**
32
+ * Where basalt-core is on disk. Same two layouts, same order.
33
+ * @returns {string}
34
+ */
35
+ function sharedPackageDir() {
36
+ try {
37
+ return path.dirname(require.resolve('basalt-core/package.json'));
38
+ } catch (error) {
39
+ if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'MODULE_NOT_FOUND') {
40
+ throw error;
41
+ }
42
+ return SIBLING;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * One of basalt-core's modules, by subpath.
48
+ * @param {string} subpath
49
+ * @returns {any}
50
+ */
51
+ function shared(subpath) {
52
+ const name = `basalt-core/${subpath}`;
53
+ try {
54
+ return require(name);
55
+ } catch (error) {
56
+ if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'MODULE_NOT_FOUND') {
57
+ // The module was found and threw on its way up. Passing it through keeps
58
+ // a real error in that file from being reported as a missing package.
59
+ throw error;
60
+ }
61
+ try {
62
+ return require(path.join(SIBLING, 'dist', subpath));
63
+ } catch (siblingError) {
64
+ if (/** @type {NodeJS.ErrnoException} */ (siblingError).code === 'MODULE_NOT_FOUND') {
65
+ throw new Error(
66
+ `could not load ${name}. In a checkout, basalt-core has to be ` +
67
+ 'built first: run scripts/build_ts.sh.',
68
+ );
69
+ }
70
+ throw siblingError;
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = {shared, sharedPackageDir};
@@ -0,0 +1,362 @@
1
+ # basalt-gtk — the Linux view layer, over GTK4.
2
+ #
3
+ # A view layer, a mounting manager and a host. Everything behind them is in
4
+ # basalt-core, which this adds as a subdirectory; nothing here is
5
+ # duplicated from it.
6
+
7
+ cmake_minimum_required(VERSION 3.13)
8
+ project(basalt_gtk CXX)
9
+
10
+ set(CMAKE_CXX_STANDARD 20)
11
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
12
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
13
+
14
+ # Executables at the top of the build tree whether this is configured directly
15
+ # or added by a wrapper, so `build/basalt_gtk` means the same thing either way.
16
+ # Every script and document here names that path.
17
+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Finding basalt-core, the shared half.
21
+ #
22
+ # The default works in both layouts this can be built from: a checkout of this
23
+ # repository, where the packages are siblings under packages/, and an app's
24
+ # node_modules, where npm hoists them side by side. `react-native run-linux --build`
25
+ # passes it explicitly anyway, because a pnpm store is neither.
26
+ #
27
+ # Guarded on the target rather than on the variable: a build that configures
28
+ # both platform packages would otherwise add the same directory twice, which
29
+ # CMake refuses.
30
+ # ---------------------------------------------------------------------------
31
+ if(NOT TARGET basalt_core)
32
+ if(NOT DEFINED BASALT_CORE_DIR)
33
+ set(BASALT_CORE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../basalt-core/native)
34
+ endif()
35
+ get_filename_component(BASALT_CORE_DIR "${BASALT_CORE_DIR}" ABSOLUTE)
36
+ if(NOT EXISTS ${BASALT_CORE_DIR}/CMakeLists.txt)
37
+ message(FATAL_ERROR
38
+ "basalt-core not found at ${BASALT_CORE_DIR}.\n"
39
+ "Pass -DBASALT_CORE_DIR=<path to basalt-core/native>.")
40
+ endif()
41
+ # Capability packages first, and only here, because they contribute include
42
+ # directories to basalt_core and CMake resolves those when the target is
43
+ # created rather than afterwards.
44
+ #
45
+ # This used to live only in the repository's root CMakeLists, which is the
46
+ # entry point for a checkout and is not the entry point for an app: the CLI
47
+ # configures -S <host package>/native, so the loop never ran and
48
+ # core/portability_probe.cpp failed on a Notifications.h that nothing had
49
+ # put on the path. A checkout could not see it, because there the root added
50
+ # the packages first and the header was always there.
51
+ #
52
+ # Guarded on the property rather than the variable for the same reason the
53
+ # block around it is guarded on the target: a build configuring two host
54
+ # packages must not add the same directory twice.
55
+ get_property(basalt_packages_added GLOBAL PROPERTY BASALT_PACKAGES_ADDED)
56
+ if(NOT basalt_packages_added)
57
+ set_property(GLOBAL PROPERTY BASALT_PACKAGES_ADDED TRUE)
58
+ foreach(package ${BASALT_PACKAGES})
59
+ if(EXISTS ${package}/native/CMakeLists.txt)
60
+ get_filename_component(basalt_package_name ${package} NAME)
61
+ message(STATUS "basalt: capability package ${basalt_package_name}")
62
+ add_subdirectory(${package}/native basalt-package-${basalt_package_name})
63
+ endif()
64
+ endforeach()
65
+ endif()
66
+
67
+ add_subdirectory(${BASALT_CORE_DIR} basalt_core)
68
+ endif()
69
+
70
+ # What capability packages contributed for this host; see
71
+ # packages/basalt-notifications for what a package sets.
72
+ #
73
+ # Read *after* the block above, because that block is where the packages are
74
+ # added. It used to be read at the top of this file, which worked for a
75
+ # checkout -- the root CMakeLists adds the packages before it adds this one --
76
+ # and silently contributed nothing for an app, where this file is the entry
77
+ # point. The symptom was a link error naming every notification seam.
78
+ get_property(BASALT_PACKAGE_GTK_SOURCES GLOBAL PROPERTY BASALT_PACKAGE_GTK_SOURCES)
79
+ # And what those sources need linked. A package contributing an implementation
80
+ # that reaches for a library had no way to say so; see the same property in the
81
+ # AppKit package for what went missing.
82
+ get_property(BASALT_PACKAGE_GTK_LINK_LIBRARIES GLOBAL PROPERTY BASALT_PACKAGE_GTK_LINK_LIBRARIES)
83
+ # And a package's own unit tests, which run in this host's suite. A capability
84
+ # with a platform half has nowhere else to be tested: it is compiled into the
85
+ # host binary, so the host's suite is the only binary that has it.
86
+ get_property(BASALT_PACKAGE_TESTS GLOBAL PROPERTY BASALT_PACKAGE_TESTS)
87
+
88
+ # Found here rather than inherited from the shared half. find_package sets
89
+ # PKG_CONFIG_VERSION as an ordinary variable, which is scoped to the directory
90
+ # that ran it, so a package that only inherits it gets pkg_check_modules failing
91
+ # on a malformed `if` deep inside FindPkgConfig rather than on anything legible.
92
+ #
93
+ # Neither of the first two is REQUIRED, and that is the same courtesy the AppKit
94
+ # and Win32 packages extend: a checkout builds every package it finds, and on a
95
+ # machine with no GTK -- Windows, or a Mac without brew's gtk4 -- this one has
96
+ # nothing it could compile and should say so rather than fail the configure for
97
+ # the platform that machine *can* build. Everything after these two stays
98
+ # REQUIRED: a box with gtk4 and no pangoft2 is a broken installation, not an
99
+ # absent one.
100
+ find_package(PkgConfig QUIET)
101
+ if(NOT PKG_CONFIG_FOUND)
102
+ message(STATUS "basalt-gtk skipped: no pkg-config")
103
+ return()
104
+ endif()
105
+
106
+ pkg_check_modules(GTK4 IMPORTED_TARGET gtk4)
107
+ if(NOT GTK4_FOUND)
108
+ message(STATUS "basalt-gtk skipped: no gtk4")
109
+ return()
110
+ endif()
111
+ # For fonts an app loads at runtime. Pango finds fonts through fontconfig, so
112
+ # adding one to fontconfig is what makes it available to text this platform
113
+ # renders. See gtk/FontRegistryFontconfig.cpp.
114
+ pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
115
+ # pango_fc_font_map_config_changed lives here rather than in pango proper. It is
116
+ # how Pango is told that the set of fonts changed underneath it.
117
+ pkg_check_modules(PANGOFT2 REQUIRED IMPORTED_TARGET pangoft2)
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # basalt_gtk_view — the GTK4 widget layer.
121
+ #
122
+ # No React Native dependency, so it builds and runs on its own.
123
+ # ---------------------------------------------------------------------------
124
+
125
+ add_library(basalt_gtk_view STATIC gtk/RnView.cpp gtk/GtkTextPeer.cpp)
126
+ # This project's own sources are held to -Wall -Wextra. React Native's are not:
127
+ # its targets set their own flags, and these are PRIVATE so they never reach it.
128
+ target_compile_options(basalt_gtk_view PRIVATE -Wall -Wextra)
129
+ target_include_directories(basalt_gtk_view PUBLIC gtk)
130
+ # core/FocusRing.h and core/Backface.h, and nothing else from there. Both are
131
+ # header-only and have no React Native in them, so taking them does not make
132
+ # this target depend on the half that does -- and the alternative is a focus
133
+ # ring that is a different blue on each desktop, and three toolkits each with
134
+ # their own idea of which way a rotated card is facing.
135
+ #
136
+ # Header-only is the constraint, not a preference: this library links no core,
137
+ # so anything it takes from there has to be `inline`.
138
+ target_include_directories(basalt_gtk_view PUBLIC
139
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../basalt-core/native/core)
140
+ target_link_libraries(basalt_gtk_view PUBLIC PkgConfig::GTK4 PkgConfig::FONTCONFIG PkgConfig::PANGOFT2)
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # basalt_notification_stub -- a stand-in for a desktop's notification daemon.
144
+ #
145
+ # Test infrastructure rather than a host: the end-to-end suite starts a session
146
+ # bus and this on it, so that the one thing about notifications that needs a
147
+ # service to be running can actually be asserted. Needs GIO and nothing else,
148
+ # which basalt_gtk_view already brings.
149
+ # ---------------------------------------------------------------------------
150
+
151
+ add_executable(basalt_notification_stub tests/notification_stub.cpp)
152
+ target_link_libraries(basalt_notification_stub PRIVATE PkgConfig::GTK4)
153
+ target_compile_options(basalt_notification_stub PRIVATE -Wall -Wextra)
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # demo_layout_gtk — exercises the widget layer with hand-written frames.
157
+ # ---------------------------------------------------------------------------
158
+
159
+ add_executable(demo_layout_gtk gtk/demo_layout_gtk.cpp)
160
+ target_link_libraries(demo_layout_gtk PRIVATE basalt_gtk_view)
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # basalt_gtk_mounting — the GTK half of the Fabric mounting manager.
164
+ #
165
+ # The mutation walk is not here: it is in core/MountingWalk.h, shared with every
166
+ # other desktop platform. What is here is what is genuinely GTK about mounting.
167
+ # ---------------------------------------------------------------------------
168
+
169
+ if(TARGET basalt_core)
170
+ add_library(basalt_gtk_mounting STATIC
171
+ gtk/GtkMountingManager.cpp
172
+ # Which components this platform claims. See core/ComponentRegistry.h.
173
+ gtk/ComponentRegistryGtk.cpp
174
+ # The mounting manager loads <Image> pixels itself, so the loader lives
175
+ # here rather than with the host: mount_harness_gtk links this too.
176
+ gtk/GtkImageLoader.cpp
177
+ gtk/GtkScrollView.cpp
178
+ gtk/GtkTextInput.cpp
179
+ gtk/GtkTitleBar.cpp
180
+ # Whether a point drags the window: the geometry half, kept apart from the
181
+ # title bar so it needs no toplevel. See the header.
182
+ gtk/GtkTitleBarLayout.cpp
183
+ # Accepting a file or text dragged onto the window; see
184
+ # core/DragAndDrop.h for the model the three hosts share.
185
+ gtk/GtkDropTarget.cpp
186
+ # A window's size and full-screen state; see core/WindowControl.h.
187
+ gtk/GtkWindowControl.cpp
188
+ # The application menu; see core/MenuModel.h for why the three differ.
189
+ gtk/GtkMenuBar.cpp
190
+ gtk/GtkWindowModule.cpp
191
+ # Linux's implementation of core/FontRegistry.h.
192
+ gtk/FontRegistryFontconfig.cpp
193
+ # ...and of core/ColorScheme.h.
194
+ gtk/ColorSchemeGtk.cpp
195
+ # ...and of core/PlatformServices.h.
196
+ gtk/PlatformServicesGtk.cpp
197
+ gtk/PangoTextLayout.cpp
198
+ # Replaces React Native's stub; see cmake/ReactNativeCore.cmake.
199
+ gtk/PangoTextLayoutManager.cpp)
200
+
201
+ target_include_directories(basalt_gtk_mounting PUBLIC gtk)
202
+ target_link_libraries(basalt_gtk_mounting PUBLIC basalt_core)
203
+
204
+ # SYSTEM, so -Wall -Wextra on this project's own sources does not drown in
205
+ # diagnostics from React Native's headers, which are not -Wextra clean (its
206
+ # own targets build with a narrower warning set).
207
+ target_include_directories(basalt_gtk_mounting SYSTEM PUBLIC
208
+ ${RN_DIR}/ReactCommon
209
+ ${RN_DIR}/ReactCommon/jsi
210
+ # Not ${RN_DIR}/ReactCxxPlatform: when React Native is an installed package
211
+ # that directory does not exist and this one is the fetched copy. Latent
212
+ # until a version whose headers are not -Wextra clean, which 0.81 is.
213
+ ${REACT_CXX_PLATFORM_DIR}
214
+ ${FOLLY_DIR}
215
+ # Out-of-tree platforms pick a variant for each seam that has one. The cxx
216
+ # variants are the generic implementations; textlayoutmanager/cxx is a stub
217
+ # that measures nothing and is the first thing a Linux platform replaces.
218
+ ${RN_DIR}/ReactCommon/react/renderer/graphics/platform/cxx
219
+ ${RN_DIR}/ReactCommon/react/renderer/components/view/platform/cxx
220
+ ${RN_DIR}/ReactCommon/react/renderer/textlayoutmanager/platform/cxx
221
+ ${RN_DIR}/ReactCommon/react/renderer/imagemanager/platform/cxx
222
+ )
223
+
224
+ target_compile_definitions(basalt_gtk_mounting PUBLIC
225
+ FOLLY_NO_CONFIG=1
226
+ FOLLY_CFG_NO_COROUTINES=1
227
+ FOLLY_MOBILE=0
228
+ FOLLY_HAVE_PTHREAD=1
229
+ FOLLY_HAVE_CLOCK_GETTIME=1
230
+ GLOG_USE_GLOG_EXPORT
231
+ )
232
+
233
+ # RN headers carry Xcode's #pragma mark, which GCC does not recognise.
234
+ target_compile_options(basalt_gtk_mounting PRIVATE -Wall -Wextra -Wno-unknown-pragmas)
235
+
236
+ target_link_libraries(basalt_gtk_mounting PUBLIC basalt_gtk_view rn_core)
237
+ target_link_libraries(basalt_gtk_mounting PUBLIC hermes-engine::hermesvm)
238
+
239
+ # @shopify/react-native-skia, when the build was pointed at an app that has
240
+ # it. See ../../basalt-core/native/cmake/Skia.cmake.
241
+ #
242
+ # Here rather than on basalt_core, for the reason the other two packages
243
+ # give: a platform context is a host's dependency. This one reaches
244
+ # fontconfig and GDK, and core/portability_probe.cpp links the core with no
245
+ # toolkit on purpose.
246
+ if(TARGET skia_core)
247
+ target_link_libraries(basalt_gtk_mounting PUBLIC skia_core)
248
+ target_sources(basalt_gtk_mounting PUBLIC $<TARGET_OBJECTS:skia_core>)
249
+ target_compile_definitions(basalt_gtk_mounting PUBLIC BASALT_HAS_SKIA=1)
250
+ # This host's half: the TurboModule the package asks for by name, and the
251
+ # platform context it hands to Skia. See gtk/GtkSkiaModule.h.
252
+ #
253
+ # No unicode adapter beside them, where the win32 package has one: that
254
+ # host links the libgrapheme backend and has to answer for
255
+ # `SkUnicodes::ICU::Make` itself. This one links skunicode_icu, which
256
+ # defines it.
257
+ target_sources(basalt_gtk_mounting PRIVATE
258
+ gtk/GtkSkiaModule.cpp
259
+ gtk/GtkSkiaContext.cpp
260
+ # The `<Canvas>` peer. The shortest of the three, because an RnView
261
+ # already draws a GdkTexture -- see gtk/GtkSkiaPeer.h.
262
+ gtk/GtkSkiaPeer.cpp)
263
+ endif()
264
+
265
+ # The seams point both ways, and GNU ld has to be told.
266
+ #
267
+ # `basalt_core` declares `core/PlatformServices.h`, `core/ColorScheme.h` and
268
+ # `core/FontRegistry.h`; this library defines them. So `CoreModules.o`, inside
269
+ # libbasalt_core.a, has undefined references that only libbasalt_gtk_mounting.a
270
+ # can satisfy -- while this library's own objects call into core. That is a
271
+ # genuine cycle between two static archives.
272
+ #
273
+ # It links anywhere the linker resolves across the whole command line, which
274
+ # is why Windows and macOS never noticed. GNU ld scans left to right exactly
275
+ # once, and CMake orders a dependency *after* its dependent, so libbasalt_core.a
276
+ # arrives with libbasalt_gtk_mounting.a already behind it and the seams come
277
+ # out undefined.
278
+ #
279
+ # Declaring the cycle is CMake's own answer: for mutually dependent STATIC
280
+ # libraries it repeats the whole connected component on the link line. A
281
+ # `--start-group` would work too and would say less about why.
282
+ target_link_libraries(basalt_core PUBLIC basalt_gtk_mounting)
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # mount_harness_gtk — drives real ShadowViewMutations through the real mounting
286
+ # manager into real GTK widgets, with no JS runtime involved.
287
+ # ---------------------------------------------------------------------------
288
+ add_executable(mount_harness_gtk gtk/mount_harness_gtk.cpp)
289
+ target_link_libraries(mount_harness_gtk PRIVATE basalt_gtk_mounting)
290
+ target_compile_options(mount_harness_gtk PRIVATE -Wno-unknown-pragmas)
291
+
292
+ # ---------------------------------------------------------------------------
293
+ # basalt_gtk -- the real thing: ReactHost, Hermes and a live surface, with
294
+ # the mutation stream coming from Fabric rather than from a harness.
295
+ # ---------------------------------------------------------------------------
296
+ add_executable(basalt_gtk
297
+ gtk/main_gtk.cpp
298
+ gtk/GtkAnimationChoreographer.cpp
299
+ gtk/GtkRunLoopObserver.cpp
300
+ gtk/GtkFocus.cpp
301
+ gtk/GtkKeyEvents.cpp
302
+ # org.freedesktop.Notifications, over the session bus.
303
+ ${BASALT_PACKAGE_GTK_SOURCES}
304
+ gtk/GtkTouchDispatcher.cpp)
305
+ target_link_libraries(basalt_gtk PRIVATE basalt_gtk_mounting)
306
+ if(BASALT_PACKAGE_GTK_LINK_LIBRARIES)
307
+ target_link_libraries(basalt_gtk PRIVATE ${BASALT_PACKAGE_GTK_LINK_LIBRARIES})
308
+ endif()
309
+ target_compile_options(basalt_gtk PRIVATE -Wall -Wextra -Wno-unknown-pragmas)
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # basalt_gtk_tests -- the test suite.
313
+ #
314
+ # Needs a display: every test touches widgets, and GTK cannot create one
315
+ # before gtk_init succeeds. It presents no window. On a headless machine run
316
+ # it under Xvfb; see docs/TESTING.md.
317
+ # ---------------------------------------------------------------------------
318
+ add_executable(basalt_gtk_tests
319
+ ${BASALT_TEST_HARNESS}
320
+ ${BASALT_CORE_TESTS}
321
+ ${BASALT_PACKAGE_TESTS}
322
+ tests/TestMainGtk.cpp
323
+ tests/test_mounting.cpp
324
+ tests/test_view.cpp
325
+ tests/test_text.cpp
326
+ tests/test_focus.cpp
327
+ tests/test_key_events_gtk.cpp
328
+ tests/test_hittest.cpp
329
+ tests/test_scrollview.cpp
330
+ tests/test_image.cpp
331
+ tests/test_accessibility.cpp
332
+ tests/test_viewprops.cpp
333
+ tests/test_textinput.cpp
334
+ tests/test_titlebar.cpp
335
+ # The drag regions, asking what tests/test_win32_titlebar.cpp asks of
336
+ # Windows. Needs a shown window: it walks up from gtk_widget_pick.
337
+ tests/test_titlebar_drag.cpp
338
+ tests/test_version.cpp
339
+ # Hit testing lives with the touch dispatcher, which the host builds; the
340
+ # tests need it too, and it pulls in nothing else.
341
+ gtk/GtkFocus.cpp
342
+ gtk/GtkKeyEvents.cpp
343
+ # org.freedesktop.Notifications, over the session bus.
344
+ ${BASALT_PACKAGE_GTK_SOURCES}
345
+ gtk/GtkTouchDispatcher.cpp)
346
+ # The Skia/GDK pixel-format agreement, which only exists to test when Skia
347
+ # is in the build. See tests/test_skia_texture.cpp for why a comment was
348
+ # not enough.
349
+ if(TARGET skia_core)
350
+ target_sources(basalt_gtk_tests PRIVATE tests/test_skia_texture.cpp)
351
+ endif()
352
+ target_link_libraries(basalt_gtk_tests PRIVATE basalt_gtk_mounting)
353
+ if(BASALT_PACKAGE_GTK_LINK_LIBRARIES)
354
+ target_link_libraries(basalt_gtk_tests PRIVATE ${BASALT_PACKAGE_GTK_LINK_LIBRARIES})
355
+ endif()
356
+ target_include_directories(basalt_gtk_tests PRIVATE tests ${BASALT_TEST_INCLUDE})
357
+ target_compile_options(basalt_gtk_tests PRIVATE -Wall -Wextra -Wno-unknown-pragmas)
358
+
359
+ message(STATUS "basalt_gtk_mounting enabled (RN_DIR=${RN_DIR})")
360
+ else()
361
+ message(STATUS "basalt_gtk_mounting skipped (pass -DRN_DIR=... to enable)")
362
+ endif()
@@ -0,0 +1,58 @@
1
+ // Linux's half of the colour-scheme seam.
2
+ //
3
+ // `GtkSettings:gtk-application-prefer-dark-theme` is what a GTK application
4
+ // reads, and what a desktop environment sets when the user asks for dark mode.
5
+ //
6
+ // The more modern answer is the `org.freedesktop.appearance color-scheme`
7
+ // setting from the XDG desktop portal, which works across toolkits and inside a
8
+ // Flatpak sandbox. It needs a D-Bus round trip and a subscription, and GTK
9
+ // already reflects the portal's value into this setting on a desktop that has
10
+ // one -- so this is the same answer with none of the plumbing. Worth revisiting
11
+ // if a sandboxed build ever disagrees with the desktop around it.
12
+
13
+ #include "ColorScheme.h"
14
+
15
+ #include <gtk/gtk.h>
16
+
17
+ namespace basalt {
18
+
19
+ namespace {
20
+
21
+ void onPreferDarkChanged(GObject * /*settings*/, GParamSpec * /*spec*/, gpointer /*userData*/) {
22
+ notifyColorSchemeChanged();
23
+ }
24
+
25
+ bool gObserving = false;
26
+
27
+ } // namespace
28
+
29
+ ColorScheme systemColorScheme() {
30
+ // gtk_settings_get_default returns null before gtk_init, which is where a
31
+ // unit test reads from. Light is React Native's default when a platform has
32
+ // no answer.
33
+ GtkSettings *settings = gtk_settings_get_default();
34
+ if (settings == nullptr) {
35
+ return ColorScheme::Light;
36
+ }
37
+
38
+ gboolean prefersDark = FALSE;
39
+ g_object_get(settings, "gtk-application-prefer-dark-theme", &prefersDark, nullptr);
40
+ return prefersDark == TRUE ? ColorScheme::Dark : ColorScheme::Light;
41
+ }
42
+
43
+ void startObservingColorScheme() {
44
+ if (gObserving) {
45
+ return;
46
+ }
47
+ GtkSettings *settings = gtk_settings_get_default();
48
+ if (settings == nullptr) {
49
+ return;
50
+ }
51
+ g_signal_connect(settings,
52
+ "notify::gtk-application-prefer-dark-theme",
53
+ G_CALLBACK(onPreferDarkChanged),
54
+ nullptr);
55
+ gObserving = true;
56
+ }
57
+
58
+ } // namespace basalt
@@ -0,0 +1,101 @@
1
+ // The component registry for the GTK platform. See core/ComponentRegistry.h for
2
+ // why this is per-platform rather than shared.
3
+
4
+ #include "ComponentRegistry.h"
5
+ #include "ExpoImageComponent.h"
6
+
7
+ #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
8
+ #include <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
9
+ #include <react/renderer/components/image/ImageComponentDescriptor.h>
10
+ #include <react/renderer/components/iostextinput/TextInputComponentDescriptor.h>
11
+ #include <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
12
+ #include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
13
+ #include <react/renderer/components/switch/AppleSwitchComponentDescriptor.h>
14
+ #include <react/renderer/components/text/ParagraphComponentDescriptor.h>
15
+ #include <react/renderer/components/text/RawTextComponentDescriptor.h>
16
+ #include <react/renderer/components/text/TextComponentDescriptor.h>
17
+ #include <react/renderer/components/view/ViewComponentDescriptor.h>
18
+
19
+ namespace facebook::react {
20
+
21
+ ComponentRegistryFactory getDefaultComponentRegistryFactory() {
22
+ return [](const EventDispatcher::Weak &eventDispatcher,
23
+ const std::shared_ptr<const ContextContainer> &contextContainer) {
24
+ static auto providerRegistry = []() {
25
+ auto registry = std::make_shared<ComponentDescriptorProviderRegistry>();
26
+ registry->add(concreteComponentDescriptorProvider<ViewComponentDescriptor>());
27
+ // Text is three descriptors, and only one of them mounts. <Text> becomes
28
+ // a Text node, its string a RawText node, and the outermost <Text>
29
+ // becomes a Paragraph that folds the whole subtree into a single
30
+ // AttributedString. Only Paragraph has a widget.
31
+ registry->add(concreteComponentDescriptorProvider<ParagraphComponentDescriptor>());
32
+ registry->add(concreteComponentDescriptorProvider<TextComponentDescriptor>());
33
+ registry->add(concreteComponentDescriptorProvider<RawTextComponentDescriptor>());
34
+ // Image's descriptor pulls an ImageManager out of the ContextContainer,
35
+ // creating one if absent. React Native's cxx ImageManager is a stub, so
36
+ // it produces no pixels; GtkImageLoader does that from the props instead.
37
+ registry->add(concreteComponentDescriptorProvider<ImageComponentDescriptor>());
38
+ // Unlike Image's, ScrollView's descriptor is a bare alias for
39
+ // ConcreteComponentDescriptor -- no manager, no ContextContainer entry.
40
+ // Its content child is a plain View, so nothing else is needed. Leaving
41
+ // it out does not fail loudly: the registry silently substitutes
42
+ // UnimplementedNativeView, which has no ScrollViewState, and the symptom
43
+ // is a ScrollView that renders but never scrolls.
44
+ registry->add(concreteComponentDescriptorProvider<ScrollViewComponentDescriptor>());
45
+ // React Native's *iOS* TextInput, whose C++ is portable: it measures
46
+ // through a TextLayoutManager, which here is the Pango one. Android's
47
+ // needs fbjni. Its component name is "TextInput".
48
+ registry->add(concreteComponentDescriptorProvider<TextInputComponentDescriptor>());
49
+ // expo-image's view. Registered whether or not the build has Expo in it:
50
+ // the descriptor is ordinary Fabric C++, and an app that never renders
51
+ // one pays a registry entry. See core/ExpoImageComponent.h.
52
+ registry->add(concreteComponentDescriptorProvider<ExpoImageComponentDescriptor>());
53
+
54
+ // --- The controls -----------------------------------------------------
55
+ //
56
+ // Everything above is a box with something drawn in it. These are not:
57
+ // each one is a toolkit control, or a window, and each one needed the
58
+ // view layer to grow a peer before registering it here would have meant
59
+ // anything. Registering a descriptor with nothing to mount it does not
60
+ // error -- Fabric builds the shadow nodes, lays them out, and the app
61
+ // renders blank rectangles -- so this list and the mounting manager's
62
+ // `hasComponent` are two statements of the same fact and have to agree.
63
+
64
+ // <ActivityIndicator>. React Native's JavaScript writes the 20pt or 36pt
65
+ // box into the view's style, so the shadow node needs no measurement of
66
+ // its own; what the host draws inside it is a spinner.
67
+ registry->add(concreteComponentDescriptorProvider<ActivityIndicatorViewComponentDescriptor>());
68
+ // <Switch>. The descriptor is React Native's iOS one -- portable C++
69
+ // apart from a `measureContent` that instantiates a UISwitch, which
70
+ // core/DesktopControls.cpp replaces with a constant so a switch is the
71
+ // same size on all three desktops.
72
+ registry->add(concreteComponentDescriptorProvider<SwitchComponentDescriptor>());
73
+ // <Modal>. Not a second window: the shadow node is a root-kind node sized
74
+ // from its state and positioned absolutely, so a modal is an overlay
75
+ // filling the surface -- which is also what it is on the web. The host
76
+ // commits the window's size into that state; see
77
+ // core/DesktopControls.h's modalScreenSizeNeedsUpdate for why it must.
78
+ registry->add(concreteComponentDescriptorProvider<ModalHostViewComponentDescriptor>());
79
+ // <RefreshControl>. The spinner a scroll view shows while it refreshes.
80
+ // The gesture that starts it is in core/PullToRefresh.h, because a
81
+ // desktop has no rubber band to release.
82
+ registry->add(concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>());
83
+ // What React Native substitutes for a component no platform registered.
84
+ // Registering it is what turns "nothing happens" into a mounted view the
85
+ // tree dump can show, and it is what the comments above mean when they
86
+ // say the substitution is silent.
87
+ registry->add(concreteComponentDescriptorProvider<UnimplementedNativeViewComponentDescriptor>());
88
+ // React DevTools' element highlighter. It draws nothing here yet -- the
89
+ // highlight arrives as a command rather than as props -- but a DevTools
90
+ // session mounts one, and an unregistered component in the tree is a
91
+ // blank rectangle in the middle of the app.
92
+ registry->add(concreteComponentDescriptorProvider<DebuggingOverlayComponentDescriptor>());
93
+
94
+ return registry;
95
+ }();
96
+ return providerRegistry->createComponentDescriptorRegistry(
97
+ {eventDispatcher, contextContainer, nullptr});
98
+ };
99
+ }
100
+
101
+ } // namespace facebook::react