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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// The macOS half of core/MenuModel.h: the application menu, in the system
|
|
2
|
+
// menu bar.
|
|
3
|
+
//
|
|
4
|
+
// This is the platform where a menu bar is not decoration. `NSApplication`
|
|
5
|
+
// hands every key event to the main menu's `performKeyEquivalent:` before the
|
|
6
|
+
// responder chain sees it, so Cmd-C only reaches a text field's editor if there
|
|
7
|
+
// is a Copy item to route it. Until this file existed the host installed a menu
|
|
8
|
+
// with one Quit item, and the consequence was quiet and complete: copy, cut,
|
|
9
|
+
// paste, undo and select-all did nothing in a <TextInput> on macOS, on every
|
|
10
|
+
// app, with nothing in any log.
|
|
11
|
+
//
|
|
12
|
+
// Which is why the default below is not a courtesy. An app that never renders a
|
|
13
|
+
// <Menu> still gets an Edit menu, because an app that never renders a <Menu>
|
|
14
|
+
// still has text fields.
|
|
15
|
+
//
|
|
16
|
+
// ## Roles, and why they have no target
|
|
17
|
+
//
|
|
18
|
+
// Every role maps to a selector and a `nil` target. That is the whole trick:
|
|
19
|
+
// a menu item with no target sends its action down the responder chain, so
|
|
20
|
+
// `copy:` arrives at whatever has focus -- the field editor of whichever
|
|
21
|
+
// NSTextField the person is typing in. Targeting anything, including this file,
|
|
22
|
+
// would deliver it to the wrong object and do nothing.
|
|
23
|
+
|
|
24
|
+
#include "MenuModel.h"
|
|
25
|
+
|
|
26
|
+
#import <Cocoa/Cocoa.h>
|
|
27
|
+
|
|
28
|
+
#include <string>
|
|
29
|
+
#include <utility>
|
|
30
|
+
#include <vector>
|
|
31
|
+
|
|
32
|
+
// The target for an app's own items. Roles do not use it; see the header.
|
|
33
|
+
@interface RnAppKitMenuTargetBar : NSObject
|
|
34
|
+
- (instancetype)initWithHandler:(std::function<void(int)>)handler;
|
|
35
|
+
- (void)chose:(NSMenuItem *)sender;
|
|
36
|
+
@end
|
|
37
|
+
|
|
38
|
+
@implementation RnAppKitMenuTargetBar {
|
|
39
|
+
std::function<void(int)> _handler;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
- (instancetype)initWithHandler:(std::function<void(int)>)handler {
|
|
43
|
+
self = [super init];
|
|
44
|
+
if (self != nil) {
|
|
45
|
+
_handler = std::move(handler);
|
|
46
|
+
}
|
|
47
|
+
return self;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
- (void)chose:(NSMenuItem *)sender {
|
|
51
|
+
if (_handler) {
|
|
52
|
+
_handler(static_cast<int>(sender.tag));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@end
|
|
57
|
+
|
|
58
|
+
namespace basalt {
|
|
59
|
+
|
|
60
|
+
namespace {
|
|
61
|
+
|
|
62
|
+
// Held for as long as the menu is installed: NSMenuItem's target is weak, so
|
|
63
|
+
// nothing else would keep it alive and every custom item would stop working
|
|
64
|
+
// the moment the menu was built.
|
|
65
|
+
__strong RnAppKitMenuTargetBar *&menuTarget() {
|
|
66
|
+
static __strong RnAppKitMenuTargetBar *target = nil;
|
|
67
|
+
return target;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A role's selector, or nullptr when the name is not one this platform
|
|
71
|
+
// implements. `delete:` is spelled `delete:` and is a real NSResponder action
|
|
72
|
+
// despite the keyword.
|
|
73
|
+
SEL selectorForRole(const std::string &role) {
|
|
74
|
+
if (role == "about") {
|
|
75
|
+
return @selector(orderFrontStandardAboutPanel:);
|
|
76
|
+
}
|
|
77
|
+
if (role == "quit") {
|
|
78
|
+
return @selector(terminate:);
|
|
79
|
+
}
|
|
80
|
+
if (role == "undo") {
|
|
81
|
+
return @selector(undo:);
|
|
82
|
+
}
|
|
83
|
+
if (role == "redo") {
|
|
84
|
+
return @selector(redo:);
|
|
85
|
+
}
|
|
86
|
+
if (role == "cut") {
|
|
87
|
+
return @selector(cut:);
|
|
88
|
+
}
|
|
89
|
+
if (role == "copy") {
|
|
90
|
+
return @selector(copy:);
|
|
91
|
+
}
|
|
92
|
+
if (role == "paste") {
|
|
93
|
+
return @selector(paste:);
|
|
94
|
+
}
|
|
95
|
+
if (role == "delete") {
|
|
96
|
+
return @selector(delete:);
|
|
97
|
+
}
|
|
98
|
+
if (role == "selectAll") {
|
|
99
|
+
return @selector(selectAll:);
|
|
100
|
+
}
|
|
101
|
+
if (role == "minimize") {
|
|
102
|
+
return @selector(performMiniaturize:);
|
|
103
|
+
}
|
|
104
|
+
if (role == "zoom") {
|
|
105
|
+
return @selector(performZoom:);
|
|
106
|
+
}
|
|
107
|
+
if (role == "close") {
|
|
108
|
+
return @selector(performClose:);
|
|
109
|
+
}
|
|
110
|
+
if (role == "togglefullscreen") {
|
|
111
|
+
return @selector(toggleFullScreen:);
|
|
112
|
+
}
|
|
113
|
+
return nullptr;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The shortcut a role brings with it. macOS has a standard one for each, and an
|
|
117
|
+
// app that spelled its own would be fighting the platform.
|
|
118
|
+
NSString *keyEquivalentForRole(const std::string &role, NSEventModifierFlags *mask) {
|
|
119
|
+
*mask = NSEventModifierFlagCommand;
|
|
120
|
+
if (role == "quit") {
|
|
121
|
+
return @"q";
|
|
122
|
+
}
|
|
123
|
+
if (role == "undo") {
|
|
124
|
+
return @"z";
|
|
125
|
+
}
|
|
126
|
+
if (role == "redo") {
|
|
127
|
+
*mask |= NSEventModifierFlagShift;
|
|
128
|
+
return @"z";
|
|
129
|
+
}
|
|
130
|
+
if (role == "cut") {
|
|
131
|
+
return @"x";
|
|
132
|
+
}
|
|
133
|
+
if (role == "copy") {
|
|
134
|
+
return @"c";
|
|
135
|
+
}
|
|
136
|
+
if (role == "paste") {
|
|
137
|
+
return @"v";
|
|
138
|
+
}
|
|
139
|
+
if (role == "selectAll") {
|
|
140
|
+
return @"a";
|
|
141
|
+
}
|
|
142
|
+
if (role == "minimize") {
|
|
143
|
+
return @"m";
|
|
144
|
+
}
|
|
145
|
+
if (role == "close") {
|
|
146
|
+
return @"w";
|
|
147
|
+
}
|
|
148
|
+
if (role == "togglefullscreen") {
|
|
149
|
+
*mask = NSEventModifierFlagControl | NSEventModifierFlagCommand;
|
|
150
|
+
return @"f";
|
|
151
|
+
}
|
|
152
|
+
*mask = 0;
|
|
153
|
+
return @"";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The platform's own word for a role. An app that gave a label keeps it; one
|
|
157
|
+
// that did not gets what every other Mac application says, localised by nobody
|
|
158
|
+
// -- which is a gap and is in docs/BACKLOG.md.
|
|
159
|
+
NSString *defaultLabelForRole(const std::string &role) {
|
|
160
|
+
if (role == "about") {
|
|
161
|
+
NSString *name = NSRunningApplication.currentApplication.localizedName;
|
|
162
|
+
return [@"About " stringByAppendingString:name != nil ? name : @"this app"];
|
|
163
|
+
}
|
|
164
|
+
if (role == "quit") {
|
|
165
|
+
NSString *name = NSRunningApplication.currentApplication.localizedName;
|
|
166
|
+
return [@"Quit " stringByAppendingString:name != nil ? name : @"this app"];
|
|
167
|
+
}
|
|
168
|
+
if (role == "undo") {
|
|
169
|
+
return @"Undo";
|
|
170
|
+
}
|
|
171
|
+
if (role == "redo") {
|
|
172
|
+
return @"Redo";
|
|
173
|
+
}
|
|
174
|
+
if (role == "cut") {
|
|
175
|
+
return @"Cut";
|
|
176
|
+
}
|
|
177
|
+
if (role == "copy") {
|
|
178
|
+
return @"Copy";
|
|
179
|
+
}
|
|
180
|
+
if (role == "paste") {
|
|
181
|
+
return @"Paste";
|
|
182
|
+
}
|
|
183
|
+
if (role == "delete") {
|
|
184
|
+
return @"Delete";
|
|
185
|
+
}
|
|
186
|
+
if (role == "selectAll") {
|
|
187
|
+
return @"Select All";
|
|
188
|
+
}
|
|
189
|
+
if (role == "minimize") {
|
|
190
|
+
return @"Minimize";
|
|
191
|
+
}
|
|
192
|
+
if (role == "zoom") {
|
|
193
|
+
return @"Zoom";
|
|
194
|
+
}
|
|
195
|
+
if (role == "close") {
|
|
196
|
+
return @"Close";
|
|
197
|
+
}
|
|
198
|
+
if (role == "togglefullscreen") {
|
|
199
|
+
return @"Toggle Full Screen";
|
|
200
|
+
}
|
|
201
|
+
return @"";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// "CmdOrCtrl+Shift+O" into AppKit's pair. Only used for an app's own items: a
|
|
205
|
+
// role brings the platform's shortcut.
|
|
206
|
+
NSString *keyEquivalentFrom(const std::string &accelerator, NSEventModifierFlags *mask) {
|
|
207
|
+
*mask = 0;
|
|
208
|
+
if (accelerator.empty()) {
|
|
209
|
+
return @"";
|
|
210
|
+
}
|
|
211
|
+
size_t start = 0;
|
|
212
|
+
while (true) {
|
|
213
|
+
const size_t plus = accelerator.find('+', start);
|
|
214
|
+
const std::string part =
|
|
215
|
+
accelerator.substr(start, plus == std::string::npos ? std::string::npos : plus - start);
|
|
216
|
+
if (plus == std::string::npos) {
|
|
217
|
+
NSString *key = [NSString stringWithUTF8String:part.c_str()];
|
|
218
|
+
return key != nil ? key.lowercaseString : @"";
|
|
219
|
+
}
|
|
220
|
+
if (part == "Cmd" || part == "Command" || part == "CmdOrCtrl" || part == "CommandOrControl") {
|
|
221
|
+
*mask |= NSEventModifierFlagCommand;
|
|
222
|
+
} else if (part == "Ctrl" || part == "Control") {
|
|
223
|
+
*mask |= NSEventModifierFlagControl;
|
|
224
|
+
} else if (part == "Shift") {
|
|
225
|
+
*mask |= NSEventModifierFlagShift;
|
|
226
|
+
} else if (part == "Alt" || part == "Option") {
|
|
227
|
+
*mask |= NSEventModifierFlagOption;
|
|
228
|
+
} else {
|
|
229
|
+
*mask = 0;
|
|
230
|
+
return @"";
|
|
231
|
+
}
|
|
232
|
+
start = plus + 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void addItems(NSMenu *menu, const std::vector<MenuItemModel> &items);
|
|
237
|
+
|
|
238
|
+
void addItem(NSMenu *menu, const MenuItemModel &item) {
|
|
239
|
+
if (item.separator) {
|
|
240
|
+
[menu addItem:[NSMenuItem separatorItem]];
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!item.submenu.empty()) {
|
|
245
|
+
NSString *label = [NSString stringWithUTF8String:item.label.c_str()];
|
|
246
|
+
NSMenuItem *parent = [menu addItemWithTitle:label != nil ? label : @""
|
|
247
|
+
action:nullptr
|
|
248
|
+
keyEquivalent:@""];
|
|
249
|
+
NSMenu *submenu = [[NSMenu alloc] initWithTitle:label != nil ? label : @""];
|
|
250
|
+
addItems(submenu, item.submenu);
|
|
251
|
+
parent.submenu = submenu;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!item.role.empty()) {
|
|
256
|
+
SEL selector = selectorForRole(item.role);
|
|
257
|
+
if (selector == nullptr) {
|
|
258
|
+
// An unknown role is dropped. A menu item that does nothing is worse
|
|
259
|
+
// than one that is not there.
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
NSEventModifierFlags mask = 0;
|
|
263
|
+
NSString *key = keyEquivalentForRole(item.role, &mask);
|
|
264
|
+
NSString *label = item.label.empty() ? defaultLabelForRole(item.role)
|
|
265
|
+
: [NSString stringWithUTF8String:item.label.c_str()];
|
|
266
|
+
NSMenuItem *added = [menu addItemWithTitle:label != nil ? label : @""
|
|
267
|
+
action:selector
|
|
268
|
+
keyEquivalent:key];
|
|
269
|
+
added.keyEquivalentModifierMask = mask;
|
|
270
|
+
// No target, deliberately: see the header. This is what sends `copy:` down
|
|
271
|
+
// the responder chain to whichever field has focus.
|
|
272
|
+
added.target = nil;
|
|
273
|
+
added.enabled = item.enabled;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
NSEventModifierFlags mask = 0;
|
|
278
|
+
NSString *key = keyEquivalentFrom(item.accelerator, &mask);
|
|
279
|
+
NSString *label = [NSString stringWithUTF8String:item.label.c_str()];
|
|
280
|
+
NSMenuItem *added = [menu addItemWithTitle:label != nil ? label : @""
|
|
281
|
+
action:@selector(chose:)
|
|
282
|
+
keyEquivalent:key];
|
|
283
|
+
added.keyEquivalentModifierMask = mask;
|
|
284
|
+
added.target = menuTarget();
|
|
285
|
+
added.tag = item.id;
|
|
286
|
+
added.enabled = item.enabled;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
void addItems(NSMenu *menu, const std::vector<MenuItemModel> &items) {
|
|
290
|
+
// Items enable themselves from this project's model rather than from
|
|
291
|
+
// AppKit's automatic validation, which would grey out every custom item --
|
|
292
|
+
// nothing in the responder chain implements `chose:`.
|
|
293
|
+
menu.autoenablesItems = NO;
|
|
294
|
+
for (const MenuItemModel &item : items) {
|
|
295
|
+
addItem(menu, item);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// What an app gets without asking. The Edit menu is the part that matters: see
|
|
300
|
+
// the header for what its absence did.
|
|
301
|
+
MenuModel defaultMenu() {
|
|
302
|
+
MenuItemModel app;
|
|
303
|
+
app.label = "App";
|
|
304
|
+
app.submenu = {
|
|
305
|
+
MenuItemModel{.role = "about"},
|
|
306
|
+
MenuItemModel{.separator = true},
|
|
307
|
+
MenuItemModel{.role = "minimize"},
|
|
308
|
+
MenuItemModel{.role = "close"},
|
|
309
|
+
MenuItemModel{.separator = true},
|
|
310
|
+
MenuItemModel{.role = "quit"},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
MenuItemModel edit;
|
|
314
|
+
edit.label = "Edit";
|
|
315
|
+
edit.submenu = {
|
|
316
|
+
MenuItemModel{.role = "undo"},
|
|
317
|
+
MenuItemModel{.role = "redo"},
|
|
318
|
+
MenuItemModel{.separator = true},
|
|
319
|
+
MenuItemModel{.role = "cut"},
|
|
320
|
+
MenuItemModel{.role = "copy"},
|
|
321
|
+
MenuItemModel{.role = "paste"},
|
|
322
|
+
MenuItemModel{.role = "delete"},
|
|
323
|
+
MenuItemModel{.role = "selectAll"},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return {app, edit};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
} // namespace
|
|
330
|
+
|
|
331
|
+
bool applicationMenuSupported() {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
namespace {
|
|
336
|
+
|
|
337
|
+
// How a key equivalent reads in the dump: "cmd+shift+z". Lower case and in a
|
|
338
|
+
// fixed order, so the text is the same however AppKit stored the mask.
|
|
339
|
+
std::string describeKey(NSMenuItem *item) {
|
|
340
|
+
if (item.keyEquivalent.length == 0) {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
std::string out = " [";
|
|
344
|
+
const NSEventModifierFlags mask = item.keyEquivalentModifierMask;
|
|
345
|
+
if ((mask & NSEventModifierFlagControl) != 0) {
|
|
346
|
+
out += "ctrl+";
|
|
347
|
+
}
|
|
348
|
+
if ((mask & NSEventModifierFlagOption) != 0) {
|
|
349
|
+
out += "alt+";
|
|
350
|
+
}
|
|
351
|
+
if ((mask & NSEventModifierFlagShift) != 0) {
|
|
352
|
+
out += "shift+";
|
|
353
|
+
}
|
|
354
|
+
if ((mask & NSEventModifierFlagCommand) != 0) {
|
|
355
|
+
out += "cmd+";
|
|
356
|
+
}
|
|
357
|
+
out += item.keyEquivalent.lowercaseString.UTF8String;
|
|
358
|
+
out += "]";
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
void describeInto(std::string &out, NSMenu *menu, int depth) {
|
|
363
|
+
for (NSMenuItem *item in menu.itemArray) {
|
|
364
|
+
out.append(static_cast<size_t>(depth) * 2, ' ');
|
|
365
|
+
if (item.isSeparatorItem) {
|
|
366
|
+
out += "-\n";
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
out += item.title.UTF8String != nullptr ? item.title.UTF8String : "";
|
|
370
|
+
out += describeKey(item);
|
|
371
|
+
if (!item.isEnabled) {
|
|
372
|
+
out += " (disabled)";
|
|
373
|
+
}
|
|
374
|
+
out += "\n";
|
|
375
|
+
if (item.submenu != nil) {
|
|
376
|
+
describeInto(out, item.submenu, depth + 1);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
} // namespace
|
|
382
|
+
|
|
383
|
+
// Not only what was asked for. macOS adds items of its own to menus it
|
|
384
|
+
// recognises by title -- "Close All" to a File menu, dictation and Emoji &
|
|
385
|
+
// Symbols to an Edit one -- and they are in the dump because they are in the
|
|
386
|
+
// menu. A test reads it by looking for what it expects rather than by comparing
|
|
387
|
+
// the whole thing.
|
|
388
|
+
std::string describeApplicationMenu() {
|
|
389
|
+
@autoreleasepool {
|
|
390
|
+
std::string out;
|
|
391
|
+
if (NSApp.mainMenu != nil) {
|
|
392
|
+
describeInto(out, NSApp.mainMenu, 0);
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
void setApplicationMenu(const MenuModel &menu, std::function<void(int)> onChosen) {
|
|
399
|
+
@autoreleasepool {
|
|
400
|
+
menuTarget() = [[RnAppKitMenuTargetBar alloc] initWithHandler:std::move(onChosen)];
|
|
401
|
+
|
|
402
|
+
const MenuModel &model = menu.empty() ? defaultMenu() : menu;
|
|
403
|
+
NSMenu *bar = [[NSMenu alloc] init];
|
|
404
|
+
for (const MenuItemModel &top : model) {
|
|
405
|
+
// A bare command in a menu bar is not a thing any desktop has.
|
|
406
|
+
if (top.submenu.empty()) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
NSString *label = [NSString stringWithUTF8String:top.label.c_str()];
|
|
410
|
+
NSMenuItem *item = [[NSMenuItem alloc] init];
|
|
411
|
+
// The bar draws the submenu's title rather than the item's, but the item
|
|
412
|
+
// carries it too: that is what the dump reads, and what a test asserting
|
|
413
|
+
// "there is a File menu" can see.
|
|
414
|
+
item.title = label != nil ? label : @"";
|
|
415
|
+
NSMenu *submenu = [[NSMenu alloc] initWithTitle:label != nil ? label : @""];
|
|
416
|
+
addItems(submenu, top.submenu);
|
|
417
|
+
item.submenu = submenu;
|
|
418
|
+
[bar addItem:item];
|
|
419
|
+
}
|
|
420
|
+
NSApp.mainMenu = bar;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
} // namespace basalt
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// The macOS mounting manager.
|
|
2
|
+
//
|
|
3
|
+
// `IMountingManager` is the one interface `ReactHost` requires in order to put
|
|
4
|
+
// a surface on screen. Almost none of what that takes is here: the mutation
|
|
5
|
+
// walk, the view registry, the event-emitter bookkeeping and the surface roots
|
|
6
|
+
// are in `core/MountingWalk.h`, written once and shared with GTK. What is left
|
|
7
|
+
// below is the part that is genuinely AppKit -- making a view, parenting it,
|
|
8
|
+
// and turning a ShadowView into layer state.
|
|
9
|
+
//
|
|
10
|
+
// That division is the claim this file exists to test. If it holds, a third
|
|
11
|
+
// desktop is a view layer and a few hundred lines rather than a second port of
|
|
12
|
+
// everything.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
#import "AppKitImageLoader.h"
|
|
17
|
+
#import "AppKitScrollView.h"
|
|
18
|
+
#import "AppKitTextInput.h"
|
|
19
|
+
|
|
20
|
+
#include "MountingWalk.h"
|
|
21
|
+
#include "RnAppKitView.h"
|
|
22
|
+
|
|
23
|
+
#include <react/renderer/uimanager/IMountingManager.h>
|
|
24
|
+
|
|
25
|
+
#include <string>
|
|
26
|
+
|
|
27
|
+
namespace basalt {
|
|
28
|
+
|
|
29
|
+
class AppKitMountingManager final : public facebook::react::IMountingManager,
|
|
30
|
+
public MountingWalk<AppKitMountingManager, RnAppKitView *> {
|
|
31
|
+
public:
|
|
32
|
+
|
|
33
|
+
// The image loader, for the host to hand to React Native's
|
|
34
|
+
// `ImageLoaderModule` -- which is what `Image.getSize` and `Image.prefetch`
|
|
35
|
+
// reach. Shared so that module can hold a weak reference to it.
|
|
36
|
+
std::shared_ptr<AppKitImageLoader> imageLoader() const { return imageLoader_; }
|
|
37
|
+
AppKitMountingManager();
|
|
38
|
+
~AppKitMountingManager() noexcept override;
|
|
39
|
+
|
|
40
|
+
// --- IMountingManager -----------------------------------------------------
|
|
41
|
+
|
|
42
|
+
// Called on the JS thread, from the event loop's "update the rendering" step.
|
|
43
|
+
// Hands the transaction to the main queue; does not touch a view.
|
|
44
|
+
void executeMount(facebook::react::SurfaceId surfaceId,
|
|
45
|
+
facebook::react::MountingTransaction &&transaction) override;
|
|
46
|
+
|
|
47
|
+
void dispatchCommand(const facebook::react::ShadowView &shadowView,
|
|
48
|
+
const std::string &commandName,
|
|
49
|
+
const folly::dynamic &args) override;
|
|
50
|
+
|
|
51
|
+
facebook::react::ComponentRegistryFactory getComponentRegistryFactory() override;
|
|
52
|
+
|
|
53
|
+
bool hasComponent(const std::string &name) override;
|
|
54
|
+
|
|
55
|
+
// ReactCxxPlatform hands the UIManager to the mounting manager and to nothing
|
|
56
|
+
// else. Reanimated needs it, and is constructed from JavaScript where no host
|
|
57
|
+
// object is in reach, so this passes it on. See core/UIManagerAccess.h.
|
|
58
|
+
void setUIManager(std::weak_ptr<facebook::react::UIManager> uiManager) noexcept override;
|
|
59
|
+
|
|
60
|
+
// --- Host-facing ----------------------------------------------------------
|
|
61
|
+
//
|
|
62
|
+
// createSurfaceRoot, destroySurfaceRoot, getSurfaceRoot, viewForTag and
|
|
63
|
+
// eventEmitterForTag all come from MountingWalk.
|
|
64
|
+
|
|
65
|
+
// Applies a transaction to the view tree. Invoked on the main thread by
|
|
66
|
+
// executeMount's marshalling -- not part of IMountingManager, and never to be
|
|
67
|
+
// called directly. Public because the dispatch block has to reach it, and
|
|
68
|
+
// because the tests call it rather than spinning a run loop.
|
|
69
|
+
void applyTransaction(facebook::react::SurfaceId surfaceId,
|
|
70
|
+
facebook::react::MountingTransaction &&transaction);
|
|
71
|
+
|
|
72
|
+
// A press ended on this view. A <Switch> is toggled from React Native's own
|
|
73
|
+
// touch path rather than by the NSSwitch's own input, so that a press means
|
|
74
|
+
// the same thing on three desktops -- and so that the same synthesised tap
|
|
75
|
+
// the rest of the suite uses can reach it. See applyControlPeer.
|
|
76
|
+
//
|
|
77
|
+
// A no-op for every view that is not a switch.
|
|
78
|
+
void pressedView(facebook::react::Tag tag);
|
|
79
|
+
|
|
80
|
+
// The main-thread half of dispatchCommand, public for the same reason
|
|
81
|
+
// applyTransaction is.
|
|
82
|
+
void applyCommand(facebook::react::Tag tag,
|
|
83
|
+
const std::string &commandName,
|
|
84
|
+
const folly::dynamic &args);
|
|
85
|
+
|
|
86
|
+
private:
|
|
87
|
+
// --- What MountingWalk asks of a platform ---------------------------------
|
|
88
|
+
friend class MountingWalk<AppKitMountingManager, RnAppKitView *>;
|
|
89
|
+
|
|
90
|
+
RnAppKitView *createView(const facebook::react::ShadowView &shadowView);
|
|
91
|
+
RnAppKitView *createRootView(facebook::react::Tag tag);
|
|
92
|
+
void destroyView(RnAppKitView *view);
|
|
93
|
+
void insertChild(RnAppKitView *parent, RnAppKitView *child, int index);
|
|
94
|
+
void removeChild(RnAppKitView *parent, RnAppKitView *child);
|
|
95
|
+
void updateView(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
96
|
+
void forgetTag(facebook::react::Tag tag);
|
|
97
|
+
|
|
98
|
+
// The AppKit half of a control: an NSProgressIndicator for
|
|
99
|
+
// <ActivityIndicator> and <RefreshControl>, an NSSwitch for <Switch>.
|
|
100
|
+
// Everything that is not a view -- which props mean what, which scroll view
|
|
101
|
+
// a refresh control belongs to, when `onShow` fires -- is in MountingWalk.
|
|
102
|
+
void applyControlPeer(RnAppKitView *view, const basalt::ControlState &state);
|
|
103
|
+
|
|
104
|
+
// React DevTools' overlay rectangles. The parsing and the clearing are in
|
|
105
|
+
// MountingWalk; this is the drawing.
|
|
106
|
+
void setHighlights(RnAppKitView *view, const std::vector<basalt::Highlight> &highlights);
|
|
107
|
+
|
|
108
|
+
// --- The AppKit half of updateView ----------------------------------------
|
|
109
|
+
void applyProps(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
110
|
+
void applyText(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
111
|
+
void applyScrollView(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
112
|
+
void applyImage(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
113
|
+
void applyAccessibility(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
114
|
+
void applyTextInput(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
115
|
+
void applyLayoutMetrics(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
116
|
+
|
|
117
|
+
AppKitScrollViewManager scrollViews_;
|
|
118
|
+
AppKitTextInputManager textInputs_;
|
|
119
|
+
// Shared rather than held by value: React Native's `ImageLoaderModule` takes
|
|
120
|
+
// a `weak_ptr<IImageLoader>`, and this is the loader it gets.
|
|
121
|
+
std::shared_ptr<AppKitImageLoader> imageLoader_ = std::make_shared<AppKitImageLoader>();
|
|
122
|
+
|
|
123
|
+
// A <Switch> is a controlled component: the widget is not allowed to decide
|
|
124
|
+
// its own state, so the value React last sent is kept here and put straight
|
|
125
|
+
// back after the toggle that told JavaScript about it -- which is what React
|
|
126
|
+
// Native's own iOS switch does.
|
|
127
|
+
std::unordered_map<facebook::react::Tag, bool> switchValues_;
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
// The source each <Image> is currently showing, so that a mutation which
|
|
131
|
+
// changed only layout does not restart the load.
|
|
132
|
+
std::unordered_map<facebook::react::Tag, std::string> imageUris_;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
} // namespace basalt
|