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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runMacos.js +33 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +298 -0
  5. package/native/appkit/AppKitAnimationChoreographer.h +54 -0
  6. package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
  7. package/native/appkit/AppKitDropTarget.h +34 -0
  8. package/native/appkit/AppKitDropTarget.mm +292 -0
  9. package/native/appkit/AppKitFocus.h +120 -0
  10. package/native/appkit/AppKitFocus.mm +293 -0
  11. package/native/appkit/AppKitImageLoader.h +80 -0
  12. package/native/appkit/AppKitImageLoader.mm +156 -0
  13. package/native/appkit/AppKitKeyEvents.h +26 -0
  14. package/native/appkit/AppKitKeyEvents.mm +97 -0
  15. package/native/appkit/AppKitMenuBar.mm +424 -0
  16. package/native/appkit/AppKitMountingManager.h +135 -0
  17. package/native/appkit/AppKitMountingManager.mm +946 -0
  18. package/native/appkit/AppKitRunLoopObserver.h +34 -0
  19. package/native/appkit/AppKitRunLoopObserver.mm +42 -0
  20. package/native/appkit/AppKitScrollView.h +168 -0
  21. package/native/appkit/AppKitScrollView.mm +550 -0
  22. package/native/appkit/AppKitSkiaContext.h +33 -0
  23. package/native/appkit/AppKitSkiaContext.mm +255 -0
  24. package/native/appkit/AppKitSkiaModule.h +53 -0
  25. package/native/appkit/AppKitSkiaModule.mm +81 -0
  26. package/native/appkit/AppKitSkiaPeer.h +37 -0
  27. package/native/appkit/AppKitSkiaPeer.mm +123 -0
  28. package/native/appkit/AppKitSkiaView.h +62 -0
  29. package/native/appkit/AppKitSnapshot.h +21 -0
  30. package/native/appkit/AppKitSnapshot.mm +91 -0
  31. package/native/appkit/AppKitTextInput.h +140 -0
  32. package/native/appkit/AppKitTextInput.mm +588 -0
  33. package/native/appkit/AppKitTextPeer.h +74 -0
  34. package/native/appkit/AppKitTextPeer.mm +353 -0
  35. package/native/appkit/AppKitTitleBar.h +90 -0
  36. package/native/appkit/AppKitTitleBar.mm +186 -0
  37. package/native/appkit/AppKitTouchDispatcher.h +138 -0
  38. package/native/appkit/AppKitTouchDispatcher.mm +514 -0
  39. package/native/appkit/AppKitWindowControl.mm +222 -0
  40. package/native/appkit/AppKitWindowModule.h +62 -0
  41. package/native/appkit/AppKitWindowModule.mm +194 -0
  42. package/native/appkit/ColorSchemeAppKit.mm +71 -0
  43. package/native/appkit/ComponentRegistryAppKit.mm +121 -0
  44. package/native/appkit/CoreTextLayout.h +43 -0
  45. package/native/appkit/CoreTextLayout.mm +252 -0
  46. package/native/appkit/CoreTextLayoutManager.mm +138 -0
  47. package/native/appkit/FontRegistryCoreText.mm +148 -0
  48. package/native/appkit/PlatformServicesAppKit.mm +540 -0
  49. package/native/appkit/RnAppKitView.h +417 -0
  50. package/native/appkit/RnAppKitView.mm +1605 -0
  51. package/native/appkit/RnTextLayout.h +38 -0
  52. package/native/appkit/RnTextLayout.mm +168 -0
  53. package/native/appkit/demo_layout_appkit.mm +93 -0
  54. package/native/appkit/main_appkit.mm +1860 -0
  55. package/native/appkit/mount_harness_appkit.mm +197 -0
  56. package/native/tests/test_appkit_accessibility.mm +170 -0
  57. package/native/tests/test_appkit_image.mm +237 -0
  58. package/native/tests/test_appkit_input.mm +852 -0
  59. package/native/tests/test_appkit_key_events.mm +150 -0
  60. package/native/tests/test_appkit_mounting.mm +430 -0
  61. package/native/tests/test_appkit_scrollbar.mm +132 -0
  62. package/native/tests/test_appkit_services.mm +57 -0
  63. package/native/tests/test_appkit_skia.mm +212 -0
  64. package/native/tests/test_appkit_text.mm +180 -0
  65. package/native/tests/test_appkit_textinput.mm +748 -0
  66. package/native/tests/test_appkit_titlebar.mm +107 -0
  67. package/native/tests/test_appkit_view.mm +237 -0
  68. package/package.json +26 -0
  69. package/react-native.config.js +22 -0
@@ -0,0 +1,222 @@
1
+ // The macOS half of core/WindowControl.h.
2
+ //
3
+ // The one place this differs from the other two in more than spelling is the
4
+ // coordinate system. AppKit's origin is the bottom left of the *primary*
5
+ // display and y grows upwards; every other desktop, and React Native, put the
6
+ // origin at the top left and grow downwards. So `windowBounds` and
7
+ // `setWindowPosition` flip, against the primary screen's height, and an app
8
+ // that remembers a position and restores it gets the same window back.
9
+ //
10
+ // Not flipping would be the subtler bug: a window would be restored the same
11
+ // distance from the wrong edge, which looks almost right on a large display and
12
+ // entirely wrong on a small one.
13
+
14
+ #include <vector>
15
+ #include "WindowControl.h"
16
+
17
+ #import <Cocoa/Cocoa.h>
18
+
19
+ namespace basalt {
20
+
21
+ namespace {
22
+
23
+
24
+ NSWindow *window() {
25
+ return NSApp.keyWindow != nil ? NSApp.keyWindow : NSApp.mainWindow;
26
+ }
27
+
28
+ // The height AppKit measures y from. `NSScreen.screens.firstObject` is the
29
+ // primary display, which is the one AppKit's global coordinates are anchored
30
+ // to -- not `mainScreen`, which is wherever the key window happens to be.
31
+ CGFloat primaryHeight() {
32
+ NSScreen *primary = NSScreen.screens.firstObject;
33
+ return primary != nil ? NSMaxY(primary.frame) : 0.0;
34
+ }
35
+
36
+ } // namespace
37
+
38
+ std::vector<DisplayInfo> displays() {
39
+ std::vector<DisplayInfo> found;
40
+ @autoreleasepool {
41
+ const CGFloat anchor = primaryHeight();
42
+ for (NSScreen *screen in NSScreen.screens) {
43
+ DisplayInfo info;
44
+ const NSRect frame = screen.frame;
45
+ const NSRect visible = screen.visibleFrame;
46
+
47
+ info.x = frame.origin.x;
48
+ // The same flip windowBounds does, and it has to be the same one: a
49
+ // display and a window are compared by apps that place windows, and two
50
+ // conventions in one API would be a bug nobody could see until a second
51
+ // monitor was above the first.
52
+ info.y = anchor - NSMaxY(frame);
53
+ info.width = frame.size.width;
54
+ info.height = frame.size.height;
55
+
56
+ // `visibleFrame` is what is left after the menu bar and the Dock, which
57
+ // is exactly what the work area means.
58
+ info.workX = visible.origin.x;
59
+ info.workY = anchor - NSMaxY(visible);
60
+ info.workWidth = visible.size.width;
61
+ info.workHeight = visible.size.height;
62
+
63
+ info.scaleFactor = screen.backingScaleFactor > 0.0 ? screen.backingScaleFactor : 1.0;
64
+ // `NSScreen.screens` is documented to put the primary first, and that is
65
+ // also the screen the coordinates above are anchored to -- so this is
66
+ // the same answer primaryHeight() relies on rather than a second one.
67
+ info.primary = found.empty();
68
+ found.push_back(info);
69
+ }
70
+ }
71
+ return found;
72
+ }
73
+
74
+ PointerPosition pointerPosition() {
75
+ PointerPosition where;
76
+ @autoreleasepool {
77
+ // Global, and available whether or not the pointer is over this app --
78
+ // which is the whole point of asking, and is not true on every desktop.
79
+ const NSPoint mouse = NSEvent.mouseLocation;
80
+ where.x = mouse.x;
81
+ where.y = primaryHeight() - mouse.y;
82
+ where.known = true;
83
+ }
84
+ return where;
85
+ }
86
+
87
+ WindowBounds windowBounds() {
88
+ WindowBounds bounds;
89
+ @autoreleasepool {
90
+ NSWindow *target = window();
91
+ if (target == nil) {
92
+ return bounds;
93
+ }
94
+ const NSRect frame = target.frame;
95
+ bounds.x = frame.origin.x;
96
+ // Top-left origin, growing downwards. See the header.
97
+ bounds.y = primaryHeight() - NSMaxY(frame);
98
+ bounds.width = frame.size.width;
99
+ bounds.height = frame.size.height;
100
+ bounds.fullScreen = (target.styleMask & NSWindowStyleMaskFullScreen) != 0;
101
+ bounds.maximized = target.isZoomed;
102
+ }
103
+ return bounds;
104
+ }
105
+
106
+ void setWindowSize(double width, double height) {
107
+ if (width <= 0.0 || height <= 0.0) {
108
+ return;
109
+ }
110
+ @autoreleasepool {
111
+ NSWindow *target = window();
112
+ if (target == nil) {
113
+ return;
114
+ }
115
+ NSRect frame = target.frame;
116
+ // The top edge stays where it is. Resizing a window from its bottom-left
117
+ // origin -- which is what setting the size alone would do -- moves its
118
+ // title bar, and every other desktop grows a window downwards.
119
+ frame.origin.y = NSMaxY(frame) - height;
120
+ frame.size = NSMakeSize(width, height);
121
+ [target setFrame:frame display:YES animate:NO];
122
+ }
123
+ }
124
+
125
+ void setWindowPosition(double x, double y) {
126
+ @autoreleasepool {
127
+ NSWindow *target = window();
128
+ if (target == nil) {
129
+ return;
130
+ }
131
+ // `setFrameTopLeftPoint` takes AppKit coordinates, so the y this was given
132
+ // -- measured down from the top -- is measured back up from the bottom.
133
+ [target setFrameTopLeftPoint:NSMakePoint(x, primaryHeight() - y)];
134
+ }
135
+ }
136
+
137
+ void centerWindow() {
138
+ @autoreleasepool {
139
+ // AppKit's own, which puts the window slightly above centre: that is where
140
+ // Apple's guidelines say a window goes, and an app asking to be centred on
141
+ // a Mac is asking for that rather than for arithmetic.
142
+ [window() center];
143
+ }
144
+ }
145
+
146
+ void setWindowFullScreen(bool fullScreen) {
147
+ @autoreleasepool {
148
+ NSWindow *target = window();
149
+ if (target == nil) {
150
+ return;
151
+ }
152
+ const bool already = (target.styleMask & NSWindowStyleMaskFullScreen) != 0;
153
+ // `toggleFullScreen:` is the only way in, and it toggles -- so asking for
154
+ // the state it is already in has to do nothing rather than reverse it.
155
+ if (already != fullScreen) {
156
+ [target toggleFullScreen:nil];
157
+ }
158
+ }
159
+ }
160
+
161
+ void applyWindowSizeLimits() {
162
+ @autoreleasepool {
163
+ NSWindow *target = window();
164
+ if (target == nil) {
165
+ return;
166
+ }
167
+ const WindowSizeLimits limits = windowSizeLimits();
168
+ // `contentMinSize` and `contentMaxSize` rather than `minSize`/`maxSize`:
169
+ // those are the frame, which includes the title bar, and an app asking for
170
+ // a minimum of 400x300 means 400x300 of its own content -- the same
171
+ // distinction `setWindowSize` already makes.
172
+ target.contentMinSize = NSMakeSize(limits.minWidth > 0.0 ? limits.minWidth : 0.0,
173
+ limits.minHeight > 0.0 ? limits.minHeight : 0.0);
174
+ // No maximum is CGFLOAT_MAX rather than zero, which would be a window that
175
+ // cannot be any size at all.
176
+ target.contentMaxSize = NSMakeSize(limits.maxWidth > 0.0 ? limits.maxWidth : CGFLOAT_MAX,
177
+ limits.maxHeight > 0.0 ? limits.maxHeight : CGFLOAT_MAX);
178
+ }
179
+ }
180
+
181
+ void setWindowResizable(bool resizable) {
182
+ @autoreleasepool {
183
+ NSWindow *target = window();
184
+ if (target == nil) {
185
+ return;
186
+ }
187
+ // A bit in the style mask, which is also what draws the zoom button: a
188
+ // window that cannot be resized should not offer to be.
189
+ if (resizable) {
190
+ target.styleMask |= NSWindowStyleMaskResizable;
191
+ } else {
192
+ target.styleMask &= ~NSWindowStyleMaskResizable;
193
+ }
194
+ }
195
+ }
196
+
197
+ void setWindowAlwaysOnTop(bool alwaysOnTop) {
198
+ @autoreleasepool {
199
+ NSWindow *target = window();
200
+ if (target == nil) {
201
+ return;
202
+ }
203
+ // Floating, not one of the higher levels. `NSFloatingWindowLevel` puts a
204
+ // window above other applications and still below the things that are
205
+ // supposed to outrank an application -- a screen lock, a system alert -- and
206
+ // asking for more than that is asking to be the bug report.
207
+ target.level = alwaysOnTop ? NSFloatingWindowLevel : NSNormalWindowLevel;
208
+ }
209
+ }
210
+
211
+ WindowCapabilities windowCapabilities() {
212
+ // The one desktop where every one of these is real.
213
+ WindowCapabilities capabilities;
214
+ capabilities.position = true;
215
+ capabilities.minimumSize = true;
216
+ capabilities.maximumSize = true;
217
+ capabilities.resizable = true;
218
+ capabilities.alwaysOnTop = true;
219
+ return capabilities;
220
+ }
221
+
222
+ } // namespace basalt
@@ -0,0 +1,62 @@
1
+ // `BasaltWindow` on macOS: the module src/TitleBar.js talks to.
2
+ //
3
+ // The same four methods Win32WindowModule has, because the JavaScript is
4
+ // shared and does not know which desktop it is on. What differs is underneath,
5
+ // in AppKitTitleBar.h.
6
+
7
+ #pragma once
8
+
9
+ #include <ReactCommon/TurboModule.h>
10
+
11
+ #include <memory>
12
+
13
+ namespace basalt {
14
+
15
+ class AppKitWindowModule : public facebook::react::TurboModule {
16
+ public:
17
+ static constexpr const char *kModuleName = "BasaltWindow";
18
+
19
+ explicit AppKitWindowModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
20
+ ~AppKitWindowModule() override;
21
+
22
+ private:
23
+ static facebook::jsi::Value setTitle(facebook::jsi::Runtime &runtime,
24
+ facebook::react::TurboModule &module,
25
+ const facebook::jsi::Value *args,
26
+ size_t count);
27
+ static facebook::jsi::Value setTitleBarColors(facebook::jsi::Runtime &runtime,
28
+ facebook::react::TurboModule &module,
29
+ const facebook::jsi::Value *args,
30
+ size_t count);
31
+ static facebook::jsi::Value setTitleBarStyle(facebook::jsi::Runtime &runtime,
32
+ facebook::react::TurboModule &module,
33
+ const facebook::jsi::Value *args,
34
+ size_t count);
35
+ static facebook::jsi::Value getTitleBarMetrics(facebook::jsi::Runtime &runtime,
36
+ facebook::react::TurboModule &module,
37
+ const facebook::jsi::Value *args,
38
+ size_t count);
39
+ // The caption buttons' functions, for an app drawing its own header.
40
+ static facebook::jsi::Value minimize(facebook::jsi::Runtime &runtime,
41
+ facebook::react::TurboModule &module,
42
+ const facebook::jsi::Value *args,
43
+ size_t count);
44
+ static facebook::jsi::Value toggleMaximize(facebook::jsi::Runtime &runtime,
45
+ facebook::react::TurboModule &module,
46
+ const facebook::jsi::Value *args,
47
+ size_t count);
48
+ static facebook::jsi::Value closeWindow(facebook::jsi::Runtime &runtime,
49
+ facebook::react::TurboModule &module,
50
+ const facebook::jsi::Value *args,
51
+ size_t count);
52
+ static facebook::jsi::Value startWindowDrag(facebook::jsi::Runtime &runtime,
53
+ facebook::react::TurboModule &module,
54
+ const facebook::jsi::Value *args,
55
+ size_t count);
56
+ static facebook::jsi::Value noop(facebook::jsi::Runtime &runtime,
57
+ facebook::react::TurboModule &module,
58
+ const facebook::jsi::Value *args,
59
+ size_t count);
60
+ };
61
+
62
+ } // namespace basalt
@@ -0,0 +1,194 @@
1
+ #import "AppKitWindowModule.h"
2
+
3
+ #import "AppKitTitleBar.h"
4
+ #include "PlatformServices.h"
5
+
6
+ #include "WindowControl.h"
7
+ #include "WindowMethods.h"
8
+ #include <cstdint>
9
+ #include <optional>
10
+ #include <string>
11
+ #include <utility>
12
+ #include <vector>
13
+
14
+ namespace basalt {
15
+
16
+ namespace {
17
+
18
+ using facebook::jsi::Object;
19
+ using facebook::jsi::Runtime;
20
+ using facebook::jsi::String;
21
+ using facebook::jsi::Value;
22
+ using facebook::react::TurboModule;
23
+
24
+ constexpr const char *kMetricsChangedEvent = "basaltTitleBarMetricsChanged";
25
+
26
+ // processColor's 0xAARRGGBB, or nullopt for anything that is not a number --
27
+ // which is how JavaScript asks for the system's colour back.
28
+ std::optional<uint32_t> colorArgument(const Value *args, size_t count, size_t index) {
29
+ if (index >= count || !args[index].isNumber()) {
30
+ return std::nullopt;
31
+ }
32
+ return static_cast<uint32_t>(static_cast<int64_t>(args[index].asNumber()));
33
+ }
34
+
35
+ Value metricsValue(Runtime &runtime, const TitleBarMetrics &metrics) {
36
+ Object object(runtime);
37
+ object.setProperty(runtime, "height", metrics.height);
38
+ object.setProperty(runtime, "buttonsWidth", metrics.buttonsWidth);
39
+ object.setProperty(runtime,
40
+ "style",
41
+ String::createFromAscii(
42
+ runtime, metrics.style == TitleBarStyle::Hidden ? "hidden" : "native"));
43
+ return Value(runtime, object);
44
+ }
45
+
46
+ } // namespace
47
+
48
+ AppKitWindowModule::AppKitWindowModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
49
+ : TurboModule(kModuleName, std::move(jsInvoker)) {
50
+ methodMap_["setTitle"] = MethodMetadata{1, setTitle};
51
+ methodMap_["setTitleBarColors"] = MethodMetadata{3, setTitleBarColors};
52
+ methodMap_["setTitleBarStyle"] = MethodMetadata{1, setTitleBarStyle};
53
+ methodMap_["getTitleBarMetrics"] = MethodMetadata{0, getTitleBarMetrics};
54
+ methodMap_["minimize"] = MethodMetadata{0, minimize};
55
+ methodMap_["toggleMaximize"] = MethodMetadata{0, toggleMaximize};
56
+ methodMap_["close"] = MethodMetadata{0, closeWindow};
57
+ methodMap_["startWindowDrag"] = MethodMetadata{0, startWindowDrag};
58
+ // A window's geometry. The bodies are in core/WindowMethods.h, written once:
59
+ // nothing about reading two numbers or shaping the answer differs per
60
+ // desktop, and three copies would be three chances to drift.
61
+ methodMap_["setSize"] = MethodMetadata{2, windowSetSize};
62
+ methodMap_["setPosition"] = MethodMetadata{2, windowSetPosition};
63
+ methodMap_["center"] = MethodMetadata{0, windowCenter};
64
+ methodMap_["setFullScreen"] = MethodMetadata{1, windowSetFullScreen};
65
+ // How big it may be, whether it may be resized at all, and whether it floats.
66
+ // Not every desktop does every one of these; `getCapabilities` is how an app
67
+ // finds out rather than guessing. See core/WindowControl.h.
68
+ methodMap_["setMinimumSize"] = MethodMetadata{2, windowSetMinimumSize};
69
+ methodMap_["setMaximumSize"] = MethodMetadata{2, windowSetMaximumSize};
70
+ methodMap_["setResizable"] = MethodMetadata{1, windowSetResizable};
71
+ methodMap_["setAlwaysOnTop"] = MethodMetadata{1, windowSetAlwaysOnTop};
72
+ methodMap_["getCapabilities"] = MethodMetadata{0, windowGetCapabilities};
73
+ methodMap_["getBounds"] = MethodMetadata{0, windowGetBounds};
74
+ // What a NativeEventEmitter over this module calls; the events go out as
75
+ // device events either way.
76
+ methodMap_["addListener"] = MethodMetadata{1, noop};
77
+ methodMap_["removeListeners"] = MethodMetadata{1, noop};
78
+
79
+ titleBar().setMetricsListener([this](const TitleBarMetrics &metrics) {
80
+ emitDeviceEvent(kMetricsChangedEvent, [metrics](Runtime &runtime, std::vector<Value> &args) {
81
+ args.emplace_back(metricsValue(runtime, metrics));
82
+ });
83
+ });
84
+
85
+ // The window's geometry, the same way the title bar's metrics go out: a
86
+ // device event, so `useWindow()` can hand an app live bounds rather than a
87
+ // snapshot it has to remember to refresh.
88
+ setWindowBoundsListener([this](const WindowBounds &bounds) {
89
+ emitDeviceEvent(kWindowBoundsEvent, [bounds](Runtime &runtime, std::vector<Value> &args) {
90
+ Object object(runtime);
91
+ object.setProperty(runtime, "x", bounds.x);
92
+ object.setProperty(runtime, "y", bounds.y);
93
+ object.setProperty(runtime, "width", bounds.width);
94
+ object.setProperty(runtime, "height", bounds.height);
95
+ object.setProperty(runtime, "fullScreen", bounds.fullScreen);
96
+ object.setProperty(runtime, "maximized", bounds.maximized);
97
+ args.emplace_back(Value(runtime, object));
98
+ });
99
+ });
100
+
101
+ // And ask for the bounds once, now that something is listening. The host
102
+ // primes the cache when its window appears, but this module is built lazily
103
+ // -- the first time JavaScript asks for it -- so an app that mounts before
104
+ // then would read a window of no size and never be told otherwise.
105
+ postToUiThread([] { notifyWindowBoundsChanged(); });
106
+ }
107
+
108
+ AppKitWindowModule::~AppKitWindowModule() {
109
+ titleBar().setMetricsListener(nullptr);
110
+ setWindowBoundsListener(nullptr);
111
+ }
112
+
113
+ Value AppKitWindowModule::setTitle(Runtime &runtime,
114
+ TurboModule & /*module*/,
115
+ const Value *args,
116
+ size_t count) {
117
+ std::optional<std::string> title;
118
+ if (count > 0 && args[0].isString()) {
119
+ title = args[0].getString(runtime).utf8(runtime);
120
+ }
121
+ postToUiThread([title = std::move(title)]() mutable { titleBar().setTitle(std::move(title)); });
122
+ return Value::undefined();
123
+ }
124
+
125
+ Value AppKitWindowModule::setTitleBarColors(Runtime & /*runtime*/,
126
+ TurboModule & /*module*/,
127
+ const Value *args,
128
+ size_t count) {
129
+ const auto background = colorArgument(args, count, 0);
130
+ const auto text = colorArgument(args, count, 1);
131
+ const auto border = colorArgument(args, count, 2);
132
+ postToUiThread([background, text, border] { titleBar().setColors(background, text, border); });
133
+ return Value::undefined();
134
+ }
135
+
136
+ Value AppKitWindowModule::setTitleBarStyle(Runtime &runtime,
137
+ TurboModule & /*module*/,
138
+ const Value *args,
139
+ size_t count) {
140
+ const bool hidden =
141
+ count > 0 && args[0].isString() && args[0].getString(runtime).utf8(runtime) == "hidden";
142
+ postToUiThread([hidden] {
143
+ titleBar().setStyle(hidden ? TitleBarStyle::Hidden : TitleBarStyle::Native);
144
+ });
145
+ return Value::undefined();
146
+ }
147
+
148
+ Value AppKitWindowModule::getTitleBarMetrics(Runtime &runtime,
149
+ TurboModule & /*module*/,
150
+ const Value * /*args*/,
151
+ size_t /*count*/) {
152
+ return metricsValue(runtime, titleBar().metrics());
153
+ }
154
+
155
+ Value AppKitWindowModule::minimize(Runtime & /*runtime*/,
156
+ TurboModule & /*module*/,
157
+ const Value * /*args*/,
158
+ size_t /*count*/) {
159
+ postToUiThread([] { titleBar().minimize(); });
160
+ return Value::undefined();
161
+ }
162
+
163
+ Value AppKitWindowModule::toggleMaximize(Runtime & /*runtime*/,
164
+ TurboModule & /*module*/,
165
+ const Value * /*args*/,
166
+ size_t /*count*/) {
167
+ postToUiThread([] { titleBar().toggleMaximize(); });
168
+ return Value::undefined();
169
+ }
170
+
171
+ Value AppKitWindowModule::closeWindow(Runtime & /*runtime*/,
172
+ TurboModule & /*module*/,
173
+ const Value * /*args*/,
174
+ size_t /*count*/) {
175
+ postToUiThread([] { titleBar().close(); });
176
+ return Value::undefined();
177
+ }
178
+
179
+ Value AppKitWindowModule::startWindowDrag(Runtime & /*runtime*/,
180
+ TurboModule & /*module*/,
181
+ const Value * /*args*/,
182
+ size_t /*count*/) {
183
+ postToUiThread([] { titleBar().startDrag(); });
184
+ return Value::undefined();
185
+ }
186
+
187
+ Value AppKitWindowModule::noop(Runtime & /*runtime*/,
188
+ TurboModule & /*module*/,
189
+ const Value * /*args*/,
190
+ size_t /*count*/) {
191
+ return Value::undefined();
192
+ }
193
+
194
+ } // namespace basalt
@@ -0,0 +1,71 @@
1
+ // macOS's half of the colour-scheme seam.
2
+ //
3
+ // `NSApp.effectiveAppearance` is the authority: it accounts for the system
4
+ // setting, any per-application override in defaults, and the accessibility
5
+ // contrast variants, none of which reading `AppleInterfaceStyle` out of
6
+ // NSUserDefaults would see.
7
+
8
+ #include "ColorScheme.h"
9
+
10
+ #import <Cocoa/Cocoa.h>
11
+
12
+ // Key-value observing needs an object to deliver to, and the C++ half is not
13
+ // one. Observing the *application's* effectiveAppearance rather than the
14
+ // `AppleInterfaceThemeChangedNotification` distributed notification: that one
15
+ // fires for the system setting only, so an app told to be dark on a light
16
+ // system would never hear anything.
17
+ @interface RnAppKitAppearanceObserver : NSObject
18
+ @end
19
+
20
+ @implementation RnAppKitAppearanceObserver
21
+
22
+ - (void)observeValueForKeyPath:(NSString *)keyPath
23
+ ofObject:(id)object
24
+ change:(NSDictionary *)change
25
+ context:(void *)context {
26
+ (void)keyPath;
27
+ (void)object;
28
+ (void)change;
29
+ (void)context;
30
+ basalt::notifyColorSchemeChanged();
31
+ }
32
+
33
+ @end
34
+
35
+ namespace basalt {
36
+
37
+ namespace {
38
+
39
+ // Kept for the life of the process: it *is* the observer registration, and
40
+ // letting it go would stop the notifications without saying so.
41
+ RnAppKitAppearanceObserver *gObserver = nil;
42
+
43
+ } // namespace
44
+
45
+ ColorScheme systemColorScheme() {
46
+ @autoreleasepool {
47
+ NSAppearance *appearance = NSApp != nil ? NSApp.effectiveAppearance : nil;
48
+ if (appearance == nil) {
49
+ // Before NSApplication exists -- a unit test, or a very early read. Light
50
+ // is React Native's default when a platform has no answer.
51
+ return ColorScheme::Light;
52
+ }
53
+ NSAppearanceName best = [appearance
54
+ bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]];
55
+ return [best isEqualToString:NSAppearanceNameDarkAqua] ? ColorScheme::Dark
56
+ : ColorScheme::Light;
57
+ }
58
+ }
59
+
60
+ void startObservingColorScheme() {
61
+ if (gObserver != nil || NSApp == nil) {
62
+ return;
63
+ }
64
+ gObserver = [[RnAppKitAppearanceObserver alloc] init];
65
+ [NSApp addObserver:gObserver
66
+ forKeyPath:@"effectiveAppearance"
67
+ options:NSKeyValueObservingOptionNew
68
+ context:nullptr];
69
+ }
70
+
71
+ } // namespace basalt
@@ -0,0 +1,121 @@
1
+ // The component registry for the macOS platform. See core/ComponentRegistry.h
2
+ // for why this is per-platform rather than shared.
3
+ //
4
+ // `<View>`, `<Text>`, `<ScrollView>`, `<Image>` and `<TextInput>` -- the
5
+ // components an ordinary app is built from, plus the root; and below them the
6
+ // controls, which are the ones a view layer needs a real toolkit peer for.
7
+ //
8
+ // Paragraph arrived with CoreTextLayoutManager.mm, and could not have arrived
9
+ // before it: registering it constructs a `TextLayoutManager`, whose stub this
10
+ // build drops so a platform's own can be the only definition. That is the link
11
+ // error phase 19 found, and it is what kept this list at one entry.
12
+ //
13
+ // Registering them early would cost nothing visible and be worse than the gap.
14
+ // A registered descriptor with no mounting peer does not error -- Fabric builds
15
+ // the shadow nodes, lays them out, and the app renders blank rectangles where
16
+ // its text should be. Leaving them out means React Native substitutes
17
+ // UnimplementedNativeView, which at least says so.
18
+
19
+ #include "ComponentRegistry.h"
20
+ #include "ExpoImageComponent.h"
21
+
22
+ #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
23
+
24
+ #include "SkiaPictureViewComponent.h"
25
+ #include <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
26
+ #include <react/renderer/components/image/ImageComponentDescriptor.h>
27
+ #include <react/renderer/components/iostextinput/TextInputComponentDescriptor.h>
28
+ #include <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
29
+ #include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
30
+ #include <react/renderer/components/switch/AppleSwitchComponentDescriptor.h>
31
+ #include <react/renderer/components/text/ParagraphComponentDescriptor.h>
32
+ #include <react/renderer/components/text/RawTextComponentDescriptor.h>
33
+ #include <react/renderer/components/text/TextComponentDescriptor.h>
34
+ #include <react/renderer/components/view/ViewComponentDescriptor.h>
35
+
36
+ namespace facebook::react {
37
+
38
+ ComponentRegistryFactory getDefaultComponentRegistryFactory() {
39
+ return [](const EventDispatcher::Weak &eventDispatcher,
40
+ const std::shared_ptr<const ContextContainer> &contextContainer) {
41
+ static auto providerRegistry = []() {
42
+ auto registry = std::make_shared<ComponentDescriptorProviderRegistry>();
43
+ registry->add(concreteComponentDescriptorProvider<ViewComponentDescriptor>());
44
+ // Text is three descriptors and only one of them mounts. <Text> becomes a
45
+ // Text node, its string a RawText node, and the outermost <Text> a
46
+ // Paragraph that folds the whole subtree into a single AttributedString.
47
+ registry->add(concreteComponentDescriptorProvider<ParagraphComponentDescriptor>());
48
+ registry->add(concreteComponentDescriptorProvider<TextComponentDescriptor>());
49
+ registry->add(concreteComponentDescriptorProvider<RawTextComponentDescriptor>());
50
+ // Unlike Image's, ScrollView's descriptor is a bare alias for
51
+ // ConcreteComponentDescriptor -- no manager, no ContextContainer entry.
52
+ // Leaving it out does not fail loudly: the registry silently substitutes
53
+ // UnimplementedNativeView, which has no ScrollViewState, and the symptom
54
+ // is a ScrollView that renders but never scrolls.
55
+ registry->add(concreteComponentDescriptorProvider<ScrollViewComponentDescriptor>());
56
+ // Image's descriptor pulls an ImageManager out of the ContextContainer,
57
+ // creating one if absent. React Native's cxx ImageManager is a stub, so
58
+ // it produces no pixels; AppKitImageLoader does that from the props.
59
+ registry->add(concreteComponentDescriptorProvider<ImageComponentDescriptor>());
60
+ // React Native's *iOS* TextInput, whose C++ is portable: it measures
61
+ // through a TextLayoutManager, which here is the Core Text one.
62
+ // Android's needs fbjni. Its component name is "TextInput".
63
+ registry->add(concreteComponentDescriptorProvider<TextInputComponentDescriptor>());
64
+ // @shopify/react-native-skia's <Canvas>. Registered whether or not the
65
+ // build has Skia in it, for the reason the Expo one below gives: it costs
66
+ // a name in a registry. Without Skia the view mounts and stays empty, and
67
+ // an app that draws has already failed at getEnforcing('RNSkiaModule').
68
+ registry->add(concreteComponentDescriptorProvider<SkiaPictureViewComponentDescriptor>());
69
+ // expo-image's view. Registered whether or not the build has Expo in it:
70
+ // the descriptor is ordinary Fabric C++, and an app that never renders
71
+ // one pays a registry entry. See core/ExpoImageComponent.h.
72
+ registry->add(concreteComponentDescriptorProvider<ExpoImageComponentDescriptor>());
73
+
74
+ // --- The controls -----------------------------------------------------
75
+ //
76
+ // Everything above is a box with something drawn in it. These are not:
77
+ // each one is a toolkit control, or a window, and each one needed the
78
+ // view layer to grow a peer before registering it here would have meant
79
+ // anything. Registering a descriptor with nothing to mount it does not
80
+ // error -- Fabric builds the shadow nodes, lays them out, and the app
81
+ // renders blank rectangles -- so this list and the mounting manager's
82
+ // `hasComponent` are two statements of the same fact and have to agree.
83
+
84
+ // <ActivityIndicator>. React Native's JavaScript writes the 20pt or 36pt
85
+ // box into the view's style, so the shadow node needs no measurement of
86
+ // its own; what the host draws inside it is a spinner.
87
+ registry->add(concreteComponentDescriptorProvider<ActivityIndicatorViewComponentDescriptor>());
88
+ // <Switch>. The descriptor is React Native's iOS one -- portable C++
89
+ // apart from a `measureContent` that instantiates a UISwitch, which
90
+ // core/DesktopControls.cpp replaces with a constant so a switch is the
91
+ // same size on all three desktops.
92
+ registry->add(concreteComponentDescriptorProvider<SwitchComponentDescriptor>());
93
+ // <Modal>. Not a second window: the shadow node is a root-kind node sized
94
+ // from its state and positioned absolutely, so a modal is an overlay
95
+ // filling the surface -- which is also what it is on the web. The host
96
+ // commits the window's size into that state; see
97
+ // core/DesktopControls.h's modalScreenSizeNeedsUpdate for why it must.
98
+ registry->add(concreteComponentDescriptorProvider<ModalHostViewComponentDescriptor>());
99
+ // <RefreshControl>. The spinner a scroll view shows while it refreshes.
100
+ // The gesture that starts it is in core/PullToRefresh.h, because a
101
+ // desktop has no rubber band to release.
102
+ registry->add(concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>());
103
+ // What React Native substitutes for a component no platform registered.
104
+ // Registering it is what turns "nothing happens" into a mounted view the
105
+ // tree dump can show, and it is what the comments above mean when they
106
+ // say the substitution is silent.
107
+ registry->add(concreteComponentDescriptorProvider<UnimplementedNativeViewComponentDescriptor>());
108
+ // React DevTools' element highlighter. It draws nothing here yet -- the
109
+ // highlight arrives as a command rather than as props -- but a DevTools
110
+ // session mounts one, and an unregistered component in the tree is a
111
+ // blank rectangle in the middle of the app.
112
+ registry->add(concreteComponentDescriptorProvider<DebuggingOverlayComponentDescriptor>());
113
+
114
+ return registry;
115
+ }();
116
+ return providerRegistry->createComponentDescriptorRegistry(
117
+ {eventDispatcher, contextContainer, nullptr});
118
+ };
119
+ }
120
+
121
+ } // namespace facebook::react