basalt-appkit 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/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -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.
|
package/cli/runMacos.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `react-native run-macos`.
|
|
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-linux` and
|
|
6
|
+
* `run-windows`, because none of it is about a toolkit -- see the note at the
|
|
7
|
+
* top of that file.
|
|
8
|
+
*
|
|
9
|
+
* The command name is react-native-macos', deliberately, for the same reason
|
|
10
|
+
* the platform name is: an app that already knows `react-native run-macos`
|
|
11
|
+
* should not have to learn a second spelling to try this one.
|
|
12
|
+
*
|
|
13
|
+
* @format
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// @ts-check
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const {shared, sharedPackageDir} = require('./shared');
|
|
22
|
+
|
|
23
|
+
const {makeRunCommand} = shared('cli/desktop');
|
|
24
|
+
|
|
25
|
+
module.exports = makeRunCommand({
|
|
26
|
+
platform: 'macos',
|
|
27
|
+
label: 'macOS',
|
|
28
|
+
command: 'run-macos',
|
|
29
|
+
binary: 'basalt_appkit',
|
|
30
|
+
nativeDir: path.resolve(__dirname, '..', 'native'),
|
|
31
|
+
coreDir: path.join(sharedPackageDir(), 'native'),
|
|
32
|
+
toolchain: 'the Xcode command line tools',
|
|
33
|
+
});
|
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,298 @@
|
|
|
1
|
+
# basalt-appkit — the macOS view layer, over AppKit.
|
|
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.
|
|
5
|
+
#
|
|
6
|
+
# `appkit/` rather than `mac/`: the toolkit is what differs, and the name would
|
|
7
|
+
# be wrong the moment something else on macOS mattered.
|
|
8
|
+
|
|
9
|
+
cmake_minimum_required(VERSION 3.13)
|
|
10
|
+
project(basalt_appkit CXX)
|
|
11
|
+
|
|
12
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
13
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
14
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
15
|
+
|
|
16
|
+
# Executables at the top of the build tree whether this is configured directly
|
|
17
|
+
# or added by a wrapper, so `build/basalt_gtk` means the same thing either way.
|
|
18
|
+
# Every script and document here names that path.
|
|
19
|
+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Finding basalt-core, the shared half.
|
|
23
|
+
#
|
|
24
|
+
# The default works in both layouts this can be built from: a checkout of this
|
|
25
|
+
# repository, where the packages are siblings under packages/, and an app's
|
|
26
|
+
# node_modules, where npm hoists them side by side. `react-native run-linux --build`
|
|
27
|
+
# passes it explicitly anyway, because a pnpm store is neither.
|
|
28
|
+
#
|
|
29
|
+
# Guarded on the target rather than on the variable: a build that configures
|
|
30
|
+
# both platform packages would otherwise add the same directory twice, which
|
|
31
|
+
# CMake refuses.
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
if(NOT TARGET basalt_core)
|
|
34
|
+
if(NOT DEFINED BASALT_CORE_DIR)
|
|
35
|
+
set(BASALT_CORE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../basalt-core/native)
|
|
36
|
+
endif()
|
|
37
|
+
get_filename_component(BASALT_CORE_DIR "${BASALT_CORE_DIR}" ABSOLUTE)
|
|
38
|
+
if(NOT EXISTS ${BASALT_CORE_DIR}/CMakeLists.txt)
|
|
39
|
+
message(FATAL_ERROR
|
|
40
|
+
"basalt-core not found at ${BASALT_CORE_DIR}.\n"
|
|
41
|
+
"Pass -DBASALT_CORE_DIR=<path to basalt-core/native>.")
|
|
42
|
+
endif()
|
|
43
|
+
# Capability packages first, and only here, because they contribute include
|
|
44
|
+
# directories to basalt_core and CMake resolves those when the target is
|
|
45
|
+
# created rather than afterwards.
|
|
46
|
+
#
|
|
47
|
+
# This used to live only in the repository's root CMakeLists, which is the
|
|
48
|
+
# entry point for a checkout and is not the entry point for an app: the CLI
|
|
49
|
+
# configures -S <host package>/native, so the loop never ran and
|
|
50
|
+
# core/portability_probe.cpp failed on a Notifications.h that nothing had
|
|
51
|
+
# put on the path. A checkout could not see it, because there the root added
|
|
52
|
+
# the packages first and the header was always there.
|
|
53
|
+
#
|
|
54
|
+
# Guarded on the property rather than the variable for the same reason the
|
|
55
|
+
# block around it is guarded on the target: a build configuring two host
|
|
56
|
+
# packages must not add the same directory twice.
|
|
57
|
+
get_property(basalt_packages_added GLOBAL PROPERTY BASALT_PACKAGES_ADDED)
|
|
58
|
+
if(NOT basalt_packages_added)
|
|
59
|
+
set_property(GLOBAL PROPERTY BASALT_PACKAGES_ADDED TRUE)
|
|
60
|
+
foreach(package ${BASALT_PACKAGES})
|
|
61
|
+
if(EXISTS ${package}/native/CMakeLists.txt)
|
|
62
|
+
get_filename_component(basalt_package_name ${package} NAME)
|
|
63
|
+
message(STATUS "basalt: capability package ${basalt_package_name}")
|
|
64
|
+
add_subdirectory(${package}/native basalt-package-${basalt_package_name})
|
|
65
|
+
endif()
|
|
66
|
+
endforeach()
|
|
67
|
+
endif()
|
|
68
|
+
|
|
69
|
+
add_subdirectory(${BASALT_CORE_DIR} basalt_core)
|
|
70
|
+
endif()
|
|
71
|
+
|
|
72
|
+
# What capability packages contributed for this host; see
|
|
73
|
+
# packages/basalt-notifications for what a package sets.
|
|
74
|
+
#
|
|
75
|
+
# Read *after* the block above, because that block is where the packages are
|
|
76
|
+
# added. It used to be read at the top of this file, which worked for a
|
|
77
|
+
# checkout -- the root CMakeLists adds the packages before it adds this one --
|
|
78
|
+
# and silently contributed nothing for an app, where this file is the entry
|
|
79
|
+
# point. The symptom was a link error naming every notification seam.
|
|
80
|
+
get_property(BASALT_PACKAGE_APPKIT_SOURCES GLOBAL PROPERTY BASALT_PACKAGE_APPKIT_SOURCES)
|
|
81
|
+
# And what those sources need linked. A package contributing an implementation
|
|
82
|
+
# that reaches for a framework had no way to say so: this file named
|
|
83
|
+
# UserNotifications for the notifications package, which works only because that
|
|
84
|
+
# package is ours. An app's own module is not, and basalt naming its frameworks
|
|
85
|
+
# would be basalt knowing about it.
|
|
86
|
+
get_property(BASALT_PACKAGE_APPKIT_LINK_LIBRARIES GLOBAL PROPERTY BASALT_PACKAGE_APPKIT_LINK_LIBRARIES)
|
|
87
|
+
# And a package's own unit tests, which run in this host's suite. A capability
|
|
88
|
+
# with a platform half has nowhere else to be tested: it is compiled into the
|
|
89
|
+
# host binary, so the host's suite is the only binary that has it.
|
|
90
|
+
get_property(BASALT_PACKAGE_TESTS GLOBAL PROPERTY BASALT_PACKAGE_TESTS)
|
|
91
|
+
|
|
92
|
+
if(NOT APPLE)
|
|
93
|
+
# Not an error. A checkout builds every package it finds, and on anything but
|
|
94
|
+
# a Mac this one has nothing it could compile.
|
|
95
|
+
message(STATUS "basalt-appkit skipped: not a Mac")
|
|
96
|
+
return()
|
|
97
|
+
endif()
|
|
98
|
+
|
|
99
|
+
# Objective-C++ is enabled here rather than in the shared half, so that a Linux
|
|
100
|
+
# build never asks for a compiler it has no reason to have.
|
|
101
|
+
enable_language(OBJCXX)
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# basalt_appkit_view — the AppKit view layer.
|
|
105
|
+
#
|
|
106
|
+
# No React Native dependency, so it builds and is tested on its own, on a Mac
|
|
107
|
+
# with nothing else installed.
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
add_library(basalt_appkit_view STATIC
|
|
110
|
+
appkit/RnAppKitView.mm
|
|
111
|
+
# The <TextInput> peer seam. In the *view* target rather than the mounting
|
|
112
|
+
# one because RnAppKitView reads the peer's text for its tree dump, and this
|
|
113
|
+
# target has to keep linking on its own -- the same reason GtkTextPeer.cpp
|
|
114
|
+
# sits in basalt_gtk_view.
|
|
115
|
+
appkit/AppKitTextPeer.mm
|
|
116
|
+
# The paragraph object. Core Text only, no React Native, which is what keeps
|
|
117
|
+
# this target buildable on a Mac with nothing else installed.
|
|
118
|
+
appkit/RnTextLayout.mm
|
|
119
|
+
appkit/AppKitSnapshot.mm)
|
|
120
|
+
target_compile_options(basalt_appkit_view PRIVATE -Wall -Wextra -fobjc-arc)
|
|
121
|
+
target_include_directories(basalt_appkit_view PUBLIC appkit)
|
|
122
|
+
# core/FocusRing.h and core/Backface.h, and nothing else from there. Both are
|
|
123
|
+
# header-only and have no React Native in them, so taking them does not make
|
|
124
|
+
# this target depend on the half that does -- and the alternative is a focus
|
|
125
|
+
# ring that is a different blue on each desktop, and three toolkits each with
|
|
126
|
+
# their own idea of which way a rotated card is facing.
|
|
127
|
+
#
|
|
128
|
+
# Header-only is the constraint, not a preference: this library links no core,
|
|
129
|
+
# so anything it takes from there has to be `inline`.
|
|
130
|
+
target_include_directories(basalt_appkit_view PUBLIC
|
|
131
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../basalt-core/native/core)
|
|
132
|
+
target_link_libraries(basalt_appkit_view PUBLIC
|
|
133
|
+
"-framework Cocoa" "-framework QuartzCore" "-framework CoreText")
|
|
134
|
+
|
|
135
|
+
add_executable(demo_layout_appkit appkit/demo_layout_appkit.mm)
|
|
136
|
+
target_compile_options(demo_layout_appkit PRIVATE -Wall -Wextra -fobjc-arc)
|
|
137
|
+
target_link_libraries(demo_layout_appkit PRIVATE basalt_appkit_view)
|
|
138
|
+
|
|
139
|
+
# The runner comes from the shared half, which knows about no toolkit: the test
|
|
140
|
+
# infrastructure is shared even though the views are not.
|
|
141
|
+
set(BASALT_APPKIT_TEST_SOURCES
|
|
142
|
+
${BASALT_TEST_HARNESS}
|
|
143
|
+
tests/test_appkit_view.mm
|
|
144
|
+
# Core Text, against RnTextLayout alone. No React Native, so these run in the
|
|
145
|
+
# toolkit-only build too.
|
|
146
|
+
tests/test_appkit_text.mm
|
|
147
|
+
# The resize modes, against a real bitmap. No React Native, so these run in
|
|
148
|
+
# the toolkit-only build too.
|
|
149
|
+
tests/test_appkit_image.mm
|
|
150
|
+
# The overlay scrollbar's platform half: that it is a view above the content
|
|
151
|
+
# rather than paint behind it. No React Native, so these run in the
|
|
152
|
+
# toolkit-only build too.
|
|
153
|
+
tests/test_appkit_scrollbar.mm
|
|
154
|
+
# The NSAccessibility mapping. The React Native role name is what the
|
|
155
|
+
# cross-platform diff compares; this is where the platform half is checked.
|
|
156
|
+
tests/test_appkit_accessibility.mm
|
|
157
|
+
# macOS's half of the platform-services seam. The alert is the reason: a
|
|
158
|
+
# sheet is its own window and never appears in a snapshot.
|
|
159
|
+
tests/test_appkit_services.mm
|
|
160
|
+
tests/test_appkit_titlebar.mm)
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# basalt_appkit_mounting — the AppKit half of the Fabric mounting manager.
|
|
164
|
+
#
|
|
165
|
+
# The mutation walk is not here: it is in the shared half, in
|
|
166
|
+
# core/MountingWalk.h. What is here is what is genuinely AppKit about mounting.
|
|
167
|
+
#
|
|
168
|
+
# Skipped when the build was not pointed at a React Native, so the view layer
|
|
169
|
+
# above still builds and tests on its own.
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
if(TARGET basalt_core)
|
|
172
|
+
add_library(basalt_appkit_mounting STATIC
|
|
173
|
+
appkit/AppKitMountingManager.mm
|
|
174
|
+
# Which components this platform claims. See core/ComponentRegistry.h.
|
|
175
|
+
appkit/ComponentRegistryAppKit.mm
|
|
176
|
+
# macOS's implementation of core/FontRegistry.h -- the seam phase 17 named.
|
|
177
|
+
# Core links against whichever implementation the platform half provides,
|
|
178
|
+
# so without this nothing that reaches expo-font links.
|
|
179
|
+
appkit/FontRegistryCoreText.mm
|
|
180
|
+
# ...and of core/ColorScheme.h.
|
|
181
|
+
appkit/ColorSchemeAppKit.mm
|
|
182
|
+
# ...and of core/PlatformServices.h.
|
|
183
|
+
appkit/PlatformServicesAppKit.mm
|
|
184
|
+
# AttributedString -> Core Text. Shared by measurement and painting, so they
|
|
185
|
+
# agree, which is the whole reason it is one file.
|
|
186
|
+
appkit/CoreTextLayout.mm
|
|
187
|
+
# Replaces React Native's stub TextLayoutManager; see the shared half's
|
|
188
|
+
# cmake/ReactNativeCore.cmake for why the stub has to leave the build.
|
|
189
|
+
appkit/CoreTextLayoutManager.mm
|
|
190
|
+
# <ScrollView>: offset, clipping, onScroll, state and commands.
|
|
191
|
+
appkit/AppKitScrollView.mm
|
|
192
|
+
# <Image> pixels. RN's cxx ImageManager produces none; the fetch is in the
|
|
193
|
+
# shared half, and only the decode and the cache are here.
|
|
194
|
+
appkit/AppKitImageLoader.mm
|
|
195
|
+
# <TextInput>, over a real NSTextField.
|
|
196
|
+
appkit/AppKitTextInput.mm
|
|
197
|
+
# Accepting a file or text dragged onto the window; see
|
|
198
|
+
# core/DragAndDrop.h for the model the three hosts share.
|
|
199
|
+
appkit/AppKitDropTarget.mm
|
|
200
|
+
appkit/AppKitTitleBar.mm
|
|
201
|
+
# A window's size, position and full-screen state; see core/WindowControl.h.
|
|
202
|
+
appkit/AppKitWindowControl.mm
|
|
203
|
+
# The application menu; see core/MenuModel.h for why the three differ.
|
|
204
|
+
appkit/AppKitMenuBar.mm
|
|
205
|
+
appkit/AppKitWindowModule.mm)
|
|
206
|
+
target_compile_options(basalt_appkit_mounting PRIVATE -Wall -Wextra -fobjc-arc)
|
|
207
|
+
target_include_directories(basalt_appkit_mounting PUBLIC appkit)
|
|
208
|
+
target_link_libraries(basalt_appkit_mounting PUBLIC basalt_appkit_view basalt_core)
|
|
209
|
+
target_link_libraries(basalt_appkit_mounting PUBLIC "-framework CoreText" "-framework ImageIO")
|
|
210
|
+
|
|
211
|
+
# @shopify/react-native-skia, when the build was pointed at an app that has it.
|
|
212
|
+
# See ../../basalt-core/native/cmake/Skia.cmake.
|
|
213
|
+
#
|
|
214
|
+
# Here and not on basalt_core, which is where it went first. Skia's Apple half
|
|
215
|
+
# reaches NSScreen, NSColorSpace and NSApplication, and basalt_core_probe links
|
|
216
|
+
# the core with no toolkit on purpose -- it exists so that what a new desktop
|
|
217
|
+
# has to provide is a link error rather than a discovery. It failed on exactly
|
|
218
|
+
# those three symbols, which is the probe doing its job: this is a host's
|
|
219
|
+
# dependency, not a portable one.
|
|
220
|
+
if(TARGET skia_core)
|
|
221
|
+
target_link_libraries(basalt_appkit_mounting PUBLIC skia_core "-framework AppKit")
|
|
222
|
+
target_sources(basalt_appkit_mounting PUBLIC $<TARGET_OBJECTS:skia_core>)
|
|
223
|
+
target_compile_definitions(basalt_appkit_mounting PUBLIC BASALT_HAS_SKIA=1)
|
|
224
|
+
# This host's half: the TurboModule the package asks for by name, and the
|
|
225
|
+
# platform context it hands to Skia. See appkit/AppKitSkiaModule.h.
|
|
226
|
+
target_sources(basalt_appkit_mounting PRIVATE
|
|
227
|
+
appkit/AppKitSkiaModule.mm
|
|
228
|
+
appkit/AppKitSkiaContext.mm
|
|
229
|
+
appkit/AppKitSkiaPeer.mm)
|
|
230
|
+
# That Skia renders rather than merely links; see the file. Appended here so
|
|
231
|
+
# it is compiled only when there is a Skia to render with.
|
|
232
|
+
list(APPEND BASALT_APPKIT_TEST_SOURCES tests/test_appkit_skia.mm)
|
|
233
|
+
endif()
|
|
234
|
+
# UniformTypeIdentifiers, for the file panel's filters: `allowedFileTypes`
|
|
235
|
+
# took bare extensions and is deprecated, and its replacement takes UTTypes.
|
|
236
|
+
target_link_libraries(basalt_appkit_mounting PUBLIC "-framework UniformTypeIdentifiers")
|
|
237
|
+
|
|
238
|
+
# -------------------------------------------------------------------------
|
|
239
|
+
# basalt_appkit — a ReactHost, Hermes, and one surface in an NSWindow.
|
|
240
|
+
# -------------------------------------------------------------------------
|
|
241
|
+
add_executable(basalt_appkit
|
|
242
|
+
appkit/main_appkit.mm
|
|
243
|
+
appkit/AppKitRunLoopObserver.mm
|
|
244
|
+
appkit/AppKitAnimationChoreographer.mm
|
|
245
|
+
appkit/AppKitFocus.mm
|
|
246
|
+
appkit/AppKitKeyEvents.mm
|
|
247
|
+
# Why macOS cannot show a notification; see the file.
|
|
248
|
+
${BASALT_PACKAGE_APPKIT_SOURCES}
|
|
249
|
+
appkit/AppKitTouchDispatcher.mm)
|
|
250
|
+
target_compile_options(basalt_appkit PRIVATE -Wall -Wextra -fobjc-arc)
|
|
251
|
+
target_link_libraries(basalt_appkit PRIVATE basalt_appkit_mounting)
|
|
252
|
+
if(BASALT_PACKAGE_APPKIT_LINK_LIBRARIES)
|
|
253
|
+
target_link_libraries(basalt_appkit PRIVATE ${BASALT_PACKAGE_APPKIT_LINK_LIBRARIES})
|
|
254
|
+
endif()
|
|
255
|
+
|
|
256
|
+
# Real mutations into real views, with no JavaScript anywhere. The checkpoint
|
|
257
|
+
# between "the mounting manager compiles" and "React Native runs": a mistake
|
|
258
|
+
# in the Create/Insert/Update/Remove/Delete handling shows up in a picture
|
|
259
|
+
# rather than in review.
|
|
260
|
+
add_executable(mount_harness_appkit appkit/mount_harness_appkit.mm)
|
|
261
|
+
target_compile_options(mount_harness_appkit PRIVATE -Wall -Wextra -fobjc-arc)
|
|
262
|
+
target_link_libraries(mount_harness_appkit PRIVATE basalt_appkit_mounting)
|
|
263
|
+
|
|
264
|
+
list(APPEND BASALT_APPKIT_TEST_SOURCES
|
|
265
|
+
${BASALT_CORE_TESTS}
|
|
266
|
+
# A package's own tests, beside core's and for the same reason: both need
|
|
267
|
+
# basalt_core, which only this branch of the build has. A view-only build --
|
|
268
|
+
# no React Native, so no core -- compiles neither, rather than failing on a
|
|
269
|
+
# package header it was never given the include path for.
|
|
270
|
+
${BASALT_PACKAGE_TESTS}
|
|
271
|
+
tests/test_appkit_mounting.mm
|
|
272
|
+
# Hit testing lives with the touch dispatcher, which the host builds; the
|
|
273
|
+
# tests need it too, and it pulls in nothing else.
|
|
274
|
+
appkit/AppKitFocus.mm
|
|
275
|
+
appkit/AppKitKeyEvents.mm
|
|
276
|
+
# Why macOS cannot show a notification; see the file.
|
|
277
|
+
${BASALT_PACKAGE_APPKIT_SOURCES}
|
|
278
|
+
appkit/AppKitTouchDispatcher.mm
|
|
279
|
+
tests/test_appkit_input.mm
|
|
280
|
+
tests/test_appkit_key_events.mm
|
|
281
|
+
# <TextInput>'s controlled-value loop, through real mutations.
|
|
282
|
+
tests/test_appkit_textinput.mm)
|
|
283
|
+
endif()
|
|
284
|
+
|
|
285
|
+
add_executable(basalt_appkit_tests ${BASALT_APPKIT_TEST_SOURCES})
|
|
286
|
+
target_compile_options(basalt_appkit_tests PRIVATE -Wall -Wextra -fobjc-arc)
|
|
287
|
+
target_include_directories(basalt_appkit_tests PRIVATE ${BASALT_TEST_INCLUDE})
|
|
288
|
+
# basalt_appkit_mounting already carries basalt_appkit_view; naming both makes
|
|
289
|
+
# the linker warn about a duplicate library rather than doing anything useful.
|
|
290
|
+
if(TARGET basalt_appkit_mounting)
|
|
291
|
+
target_link_libraries(basalt_appkit_tests PRIVATE basalt_appkit_mounting)
|
|
292
|
+
else()
|
|
293
|
+
target_link_libraries(basalt_appkit_tests PRIVATE basalt_appkit_view)
|
|
294
|
+
endif()
|
|
295
|
+
# The tests compile the package sources too, so they need the same libraries.
|
|
296
|
+
if(BASALT_PACKAGE_APPKIT_LINK_LIBRARIES)
|
|
297
|
+
target_link_libraries(basalt_appkit_tests PRIVATE ${BASALT_PACKAGE_APPKIT_LINK_LIBRARIES})
|
|
298
|
+
endif()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Drives React Native's animation frame callbacks from the display's refresh.
|
|
2
|
+
//
|
|
3
|
+
// AnimationChoreographer is the frame-callback seam in ReactCxxPlatform. It is
|
|
4
|
+
// NOT what drives mounting -- mounts arrive on the JS thread via the
|
|
5
|
+
// RuntimeScheduler and are marshalled by AppKitMountingManager. This is purely the
|
|
6
|
+
// "there is a new frame" signal the animation backend needs, and the counterpart
|
|
7
|
+
// of gtk/GtkAnimationChoreographer, which rides GTK's frame clock.
|
|
8
|
+
|
|
9
|
+
#pragma once
|
|
10
|
+
|
|
11
|
+
#ifdef __OBJC__
|
|
12
|
+
#import <Cocoa/Cocoa.h>
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
#include <react/renderer/animationbackend/AnimationChoreographer.h>
|
|
16
|
+
|
|
17
|
+
#include <atomic>
|
|
18
|
+
|
|
19
|
+
namespace basalt {
|
|
20
|
+
|
|
21
|
+
class AppKitAnimationChoreographer final : public facebook::react::AnimationChoreographer {
|
|
22
|
+
public:
|
|
23
|
+
AppKitAnimationChoreographer() = default;
|
|
24
|
+
~AppKitAnimationChoreographer() override;
|
|
25
|
+
|
|
26
|
+
// Called from the JS thread. Both marshal to the main thread before touching
|
|
27
|
+
// the display link.
|
|
28
|
+
void resume() override;
|
|
29
|
+
void pause() override;
|
|
30
|
+
|
|
31
|
+
#ifdef __OBJC__
|
|
32
|
+
// Called on the main thread once the view is in a window: a display link is
|
|
33
|
+
// tied to the screen the view is on, and a view with no window has no screen.
|
|
34
|
+
// The same reason the GTK side waits for realisation.
|
|
35
|
+
void attachToView(NSView *view);
|
|
36
|
+
#endif
|
|
37
|
+
void detach();
|
|
38
|
+
|
|
39
|
+
// Not private: the trampoline object that receives the display link callback
|
|
40
|
+
// has to reach it, and it is not something a caller would ever want.
|
|
41
|
+
void onFrame(double timestampSeconds);
|
|
42
|
+
|
|
43
|
+
private:
|
|
44
|
+
void syncRunningState();
|
|
45
|
+
|
|
46
|
+
// Opaque to C++ callers. Holds the CADisplayLink and its target.
|
|
47
|
+
void *link_{nullptr};
|
|
48
|
+
void *target_{nullptr};
|
|
49
|
+
|
|
50
|
+
// Written from the JS thread, read on the main thread.
|
|
51
|
+
std::atomic<bool> running_{false};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
} // namespace basalt
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#import "AppKitAnimationChoreographer.h"
|
|
2
|
+
|
|
3
|
+
#import <QuartzCore/QuartzCore.h>
|
|
4
|
+
|
|
5
|
+
#include <chrono>
|
|
6
|
+
|
|
7
|
+
// A display link needs an Objective-C target, and the choreographer is a C++
|
|
8
|
+
// object, so this carries the callback across.
|
|
9
|
+
@interface RnAppKitDisplayLinkTarget : NSObject
|
|
10
|
+
@property(nonatomic, assign) basalt::AppKitAnimationChoreographer *owner;
|
|
11
|
+
- (void)step:(CADisplayLink *)sender;
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
@implementation RnAppKitDisplayLinkTarget
|
|
15
|
+
- (void)step:(CADisplayLink *)sender {
|
|
16
|
+
if (_owner != nullptr) {
|
|
17
|
+
// targetTimestamp rather than timestamp: React Native's animation backend
|
|
18
|
+
// wants the time the frame will be shown, not the time the callback ran.
|
|
19
|
+
_owner->onFrame(sender.targetTimestamp);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
namespace basalt {
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
void onMainThread(dispatch_block_t block) {
|
|
29
|
+
if ([NSThread isMainThread]) {
|
|
30
|
+
block();
|
|
31
|
+
} else {
|
|
32
|
+
dispatch_async(dispatch_get_main_queue(), block);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
} // namespace
|
|
37
|
+
|
|
38
|
+
AppKitAnimationChoreographer::~AppKitAnimationChoreographer() {
|
|
39
|
+
detach();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void AppKitAnimationChoreographer::attachToView(NSView *view) {
|
|
43
|
+
detach();
|
|
44
|
+
|
|
45
|
+
RnAppKitDisplayLinkTarget *target = [[RnAppKitDisplayLinkTarget alloc] init];
|
|
46
|
+
target.owner = this;
|
|
47
|
+
|
|
48
|
+
// -[NSView displayLinkWithTarget:selector:] is macOS 14 and later. Below
|
|
49
|
+
// that the only alternatives are CVDisplayLink, which is itself deprecated
|
|
50
|
+
// and calls back on its own thread, and a timer, which is not aligned to
|
|
51
|
+
// anything. Neither is worth carrying: this platform targets the current
|
|
52
|
+
// Expo, which is well inside 14.
|
|
53
|
+
if (@available(macOS 14.0, *)) {
|
|
54
|
+
CADisplayLink *link = [view displayLinkWithTarget:target selector:@selector(step:)];
|
|
55
|
+
// Added paused. resume() is what starts it, and starting here would wake
|
|
56
|
+
// the process at display rate for an app that never animates -- the same
|
|
57
|
+
// trade the GTK side makes by not holding the frame clock open.
|
|
58
|
+
link.paused = YES;
|
|
59
|
+
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
60
|
+
|
|
61
|
+
link_ = (__bridge_retained void *)link;
|
|
62
|
+
target_ = (__bridge_retained void *)target;
|
|
63
|
+
syncRunningState();
|
|
64
|
+
} else {
|
|
65
|
+
NSLog(@"react-native: animation frames need macOS 14 or later; Animated and "
|
|
66
|
+
@"requestAnimationFrame will not tick");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
void AppKitAnimationChoreographer::detach() {
|
|
71
|
+
if (link_ == nullptr && target_ == nullptr) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void *link = link_;
|
|
76
|
+
void *target = target_;
|
|
77
|
+
link_ = nullptr;
|
|
78
|
+
target_ = nullptr;
|
|
79
|
+
|
|
80
|
+
onMainThread(^{
|
|
81
|
+
if (link != nullptr) {
|
|
82
|
+
CADisplayLink *displayLink = (__bridge_transfer CADisplayLink *)link;
|
|
83
|
+
[displayLink invalidate];
|
|
84
|
+
}
|
|
85
|
+
if (target != nullptr) {
|
|
86
|
+
RnAppKitDisplayLinkTarget *owner = (__bridge_transfer RnAppKitDisplayLinkTarget *)target;
|
|
87
|
+
owner.owner = nullptr;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
void AppKitAnimationChoreographer::resume() {
|
|
93
|
+
running_ = true;
|
|
94
|
+
onMainThread(^{
|
|
95
|
+
syncRunningState();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void AppKitAnimationChoreographer::pause() {
|
|
100
|
+
running_ = false;
|
|
101
|
+
onMainThread(^{
|
|
102
|
+
syncRunningState();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
void AppKitAnimationChoreographer::syncRunningState() {
|
|
107
|
+
if (link_ == nullptr) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
CADisplayLink *link = (__bridge CADisplayLink *)link_;
|
|
111
|
+
link.paused = running_ ? NO : YES;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
void AppKitAnimationChoreographer::onFrame(double timestampSeconds) {
|
|
115
|
+
// AnimationTimestamp is a std::chrono duration against a steady clock, and
|
|
116
|
+
// CADisplayLink's timestamps are mach absolute time in seconds -- the same
|
|
117
|
+
// clock, in different units. Converting rather than calling now() keeps the
|
|
118
|
+
// timestamp the frame's rather than the callback's.
|
|
119
|
+
using namespace std::chrono;
|
|
120
|
+
const auto timestamp = duration_cast<facebook::react::AnimationTimestamp>(
|
|
121
|
+
duration<double>(timestampSeconds));
|
|
122
|
+
onAnimationFrame(timestamp);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
} // namespace basalt
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The AppKit half of drag and drop: what happens when something is dragged
|
|
2
|
+
// onto the window.
|
|
3
|
+
//
|
|
4
|
+
// See core/DragAndDrop.h for the model, which is shared, and for why a view
|
|
5
|
+
// declares itself a drop target by its nativeID.
|
|
6
|
+
|
|
7
|
+
#pragma once
|
|
8
|
+
|
|
9
|
+
#import "RnAppKitView.h"
|
|
10
|
+
|
|
11
|
+
#include "DragAndDrop.h"
|
|
12
|
+
|
|
13
|
+
#include <cstdint>
|
|
14
|
+
|
|
15
|
+
#include <react/renderer/core/ReactPrimitives.h>
|
|
16
|
+
|
|
17
|
+
namespace basalt {
|
|
18
|
+
|
|
19
|
+
// The deepest view under this point that would take what is being dragged, or
|
|
20
|
+
// 0 for none. The point is in the root view's coordinates.
|
|
21
|
+
facebook::react::Tag dropTargetAt(RnAppKitView *root, double x, double y, std::uint16_t accepts);
|
|
22
|
+
|
|
23
|
+
// Makes the surface root accept files and text dragged onto it.
|
|
24
|
+
void attachDropTarget(RnAppKitView *root);
|
|
25
|
+
|
|
26
|
+
// The payload a marked view under this point represents, or an empty one.
|
|
27
|
+
DragPayload dragPayloadAt(RnAppKitView *root, double x, double y);
|
|
28
|
+
|
|
29
|
+
// Begins a drag out of the window, if the press at this point started on a
|
|
30
|
+
// marked view. Answers whether it did -- a caller that hears yes should stop
|
|
31
|
+
// treating the gesture as a press, because AppKit now owns it.
|
|
32
|
+
bool beginDragIfMarked(RnAppKitView *root, double x, double y);
|
|
33
|
+
|
|
34
|
+
} // namespace basalt
|