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,540 @@
1
+ // macOS's half of the platform-services seam: clipboard, opening a URL, alerts.
2
+
3
+ #include "PlatformServices.h"
4
+ #include "TestDialog.h"
5
+
6
+ #include <cstdint>
7
+ #include <vector>
8
+
9
+ #import <Cocoa/Cocoa.h>
10
+ // UTType, which is what a file panel's filters are now that
11
+ // `allowedFileTypes` -- which took the bare extensions a caller gives -- is
12
+ // deprecated. The umbrella header only forward-declares the class.
13
+ #import <UniformTypeIdentifiers/UTType.h>
14
+
15
+ // The picker's delegate, and its owner.
16
+ //
17
+ // NSSharingServicePicker holds its delegate weakly and is itself released as
18
+ // soon as `showRelativeToRect:` returns, so something has to keep both alive
19
+ // until the person has chosen. That something is this, which retains itself
20
+ // until the delegate method fires -- the same shape AppKitTouchDispatcher's
21
+ // input target has, for the same reason.
22
+ @interface RnAppKitSharePicker : NSObject <NSSharingServicePickerDelegate>
23
+ - (instancetype)initWithItems:(NSArray *)items done:(basalt::ShareCallback)done;
24
+ - (void)show:(NSView *)anchor;
25
+ @end
26
+
27
+ @implementation RnAppKitSharePicker {
28
+ NSSharingServicePicker *_picker;
29
+ basalt::ShareCallback _done;
30
+ RnAppKitSharePicker *_self;
31
+ }
32
+
33
+ - (instancetype)initWithItems:(NSArray *)items done:(basalt::ShareCallback)done {
34
+ self = [super init];
35
+ if (self != nil) {
36
+ _picker = [[NSSharingServicePicker alloc] initWithItems:items];
37
+ _picker.delegate = self;
38
+ _done = std::move(done);
39
+ }
40
+ return self;
41
+ }
42
+
43
+ - (void)show:(NSView *)anchor {
44
+ // Retained until the delegate answers. Without this the picker is on screen
45
+ // and the only thing that knows what to do with the answer has been freed.
46
+ _self = self;
47
+ [_picker showRelativeToRect:NSMakeRect(0, 0, 1, 1)
48
+ ofView:anchor
49
+ preferredEdge:NSMinYEdge];
50
+ }
51
+
52
+ - (void)sharingServicePicker:(NSSharingServicePicker *)picker
53
+ didChooseSharingService:(NSSharingService *)service {
54
+ (void)picker;
55
+ // nil means the picker was dismissed without a choice, which is React
56
+ // Native's `dismissedAction` and not a failure.
57
+ if (_done) {
58
+ _done(service != nil ? basalt::ShareOutcome::Shared : basalt::ShareOutcome::Dismissed, "");
59
+ _done = nullptr;
60
+ }
61
+ _picker.delegate = nil;
62
+ _self = nil;
63
+ }
64
+
65
+ @end
66
+
67
+ // The target every popup menu item sends its action to.
68
+ //
69
+ // NSMenuItem needs an Objective-C target and a C++ lambda is not one. One
70
+ // object per menu, holding the index its items carry as tags -- the same shape
71
+ // the switch's target had before it was replaced, and for the same reason.
72
+ @interface RnAppKitMenuTarget : NSObject
73
+ @property(nonatomic) NSInteger chosen;
74
+ - (void)pick:(NSMenuItem *)sender;
75
+ @end
76
+
77
+ @implementation RnAppKitMenuTarget
78
+
79
+ - (instancetype)init {
80
+ self = [super init];
81
+ if (self != nil) {
82
+ // Nothing chosen. -1 is the dismissal the seam documents, and it is the
83
+ // answer unless an item says otherwise.
84
+ _chosen = -1;
85
+ }
86
+ return self;
87
+ }
88
+
89
+ - (void)pick:(NSMenuItem *)sender {
90
+ _chosen = sender.tag;
91
+ }
92
+
93
+ @end
94
+
95
+ namespace basalt {
96
+
97
+ // --- Clipboard ---------------------------------------------------------------
98
+
99
+ std::string clipboardText() {
100
+ @autoreleasepool {
101
+ NSString *text = [NSPasteboard.generalPasteboard stringForType:NSPasteboardTypeString];
102
+ return text != nil ? std::string(text.UTF8String) : std::string{};
103
+ }
104
+ }
105
+
106
+ void setClipboardText(const std::string &text) {
107
+ @autoreleasepool {
108
+ NSPasteboard *pasteboard = NSPasteboard.generalPasteboard;
109
+ // Clearing first is not optional: without it the new string is added
110
+ // alongside whatever types were already there, and a paste can pick the old
111
+ // one.
112
+ [pasteboard clearContents];
113
+ NSString *value = [NSString stringWithUTF8String:text.c_str()];
114
+ [pasteboard setString:value != nil ? value : @"" forType:NSPasteboardTypeString];
115
+ }
116
+ }
117
+
118
+ // --- Opening things ----------------------------------------------------------
119
+
120
+ bool canOpenUrl(const std::string &url) {
121
+ @autoreleasepool {
122
+ NSString *string = [NSString stringWithUTF8String:url.c_str()];
123
+ if (string == nil) {
124
+ return false;
125
+ }
126
+ NSURL *parsed = [NSURL URLWithString:string];
127
+ if (parsed == nil || parsed.scheme == nil) {
128
+ return false;
129
+ }
130
+ // Whether an application is registered for the scheme. Asking this is a
131
+ // Launch Services lookup, which is the honest answer -- unlike on iOS it
132
+ // needs no declared list of schemes.
133
+ return [NSWorkspace.sharedWorkspace URLForApplicationToOpenURL:parsed] != nil;
134
+ }
135
+ }
136
+
137
+ bool openUrl(const std::string &url) {
138
+ @autoreleasepool {
139
+ NSString *string = [NSString stringWithUTF8String:url.c_str()];
140
+ if (string == nil) {
141
+ return false;
142
+ }
143
+ NSURL *parsed = [NSURL URLWithString:string];
144
+ if (parsed == nil) {
145
+ return false;
146
+ }
147
+ return [NSWorkspace.sharedWorkspace openURL:parsed];
148
+ }
149
+ }
150
+
151
+ // --- Sharing -----------------------------------------------------------------
152
+
153
+ // The one desktop with a share service of its own, so the one that does not go
154
+ // through core/ShareFallback.h.
155
+ //
156
+ // NSSharingServicePicker is the real thing: the list is whatever the machine
157
+ // has -- Mail, Messages, AirDrop, whatever an app installed -- and this host
158
+ // supplies only the items and a place to show it.
159
+ //
160
+ // Anchored to the bottom-left of the window rather than to a view the user
161
+ // pressed. A share sheet on a Mac normally hangs off the button that opened it,
162
+ // and React Native's API does not say which button that was: `Share.share()`
163
+ // takes content and nothing about where it came from. `options.anchor` is a
164
+ // node handle on iOS and is not carried here.
165
+ void shareContent(const ShareRequest &request, ShareCallback onDone) {
166
+ // The only dialog on any of the three desktops that is not built on an alert,
167
+ // so the only one the instrument has to be consulted for by hand; everything
168
+ // else reaches it through core/TestDialog.h's presentAlert. A picker that is
169
+ // up stops [NSApp terminate:] outright, so an automated run that opened one
170
+ // would hang until it was killed.
171
+ if (const std::optional<int> scripted = scriptedDialogButton()) {
172
+ onDone(*scripted < 0 ? ShareOutcome::Dismissed : ShareOutcome::Shared, "");
173
+ return;
174
+ }
175
+
176
+ // Copied before the block rather than captured by reference. The caller's
177
+ // request is a stack temporary and this returns before the block runs, so a
178
+ // reference into it would be read after it is gone -- which reads as a share
179
+ // with nothing in it, not as a crash.
180
+ const ShareRequest content = request;
181
+
182
+ // Onto the main thread and back at once, for the same reason showAlert is:
183
+ // this is called from the JavaScript thread, and the picker's own run loop
184
+ // there would deadlock the runtime.
185
+ dispatch_async(dispatch_get_main_queue(), ^{
186
+ @autoreleasepool {
187
+ NSMutableArray *items = [NSMutableArray array];
188
+ if (!content.message.empty()) {
189
+ NSString *message = [NSString stringWithUTF8String:content.message.c_str()];
190
+ if (message != nil) {
191
+ [items addObject:message];
192
+ }
193
+ }
194
+ if (!content.url.empty()) {
195
+ NSString *text = [NSString stringWithUTF8String:content.url.c_str()];
196
+ NSURL *url = text != nil ? [NSURL URLWithString:text] : nil;
197
+ // A URL as an NSURL rather than as a string, because that is what makes
198
+ // a service treat it as a link -- Mail attaches it, Messages previews
199
+ // it. One that does not parse is shared as the text it is.
200
+ [items addObject:url != nil ? (id)url : (id)(text != nil ? text : @"")];
201
+ }
202
+ if (items.count == 0) {
203
+ onDone(ShareOutcome::Failed, "nothing to share");
204
+ return;
205
+ }
206
+
207
+ NSWindow *window = NSApp.keyWindow != nil ? NSApp.keyWindow : NSApp.mainWindow;
208
+ NSView *anchor = window.contentView;
209
+ if (anchor == nil) {
210
+ onDone(ShareOutcome::Failed, "no window to show a share sheet in");
211
+ return;
212
+ }
213
+
214
+ RnAppKitSharePicker *picker = [[RnAppKitSharePicker alloc] initWithItems:items done:onDone];
215
+ [picker show:anchor];
216
+ }
217
+ });
218
+ }
219
+
220
+ // --- File dialogs -------------------------------------------------------------
221
+
222
+ namespace {
223
+
224
+ // The filters, as the content types NSOpenPanel and NSSavePanel take.
225
+ //
226
+ // `allowedContentTypes` rather than `allowedFileTypes`: the second is
227
+ // deprecated since macOS 12 and takes bare extensions, which is what the caller
228
+ // gave -- so this is the one place that has to translate. An extension macOS
229
+ // has never heard of yields no type and is dropped, which is the same thing the
230
+ // panel would do with it.
231
+ NSArray<UTType *> *contentTypesFor(const std::vector<FileFilter> &filters) {
232
+ NSMutableArray<UTType *> *types = [NSMutableArray array];
233
+ for (const FileFilter &filter : filters) {
234
+ for (const std::string &extension : filter.extensions) {
235
+ NSString *text = [NSString stringWithUTF8String:extension.c_str()];
236
+ if (text == nil) {
237
+ continue;
238
+ }
239
+ UTType *type = [UTType typeWithFilenameExtension:text];
240
+ if (type != nil) {
241
+ [types addObject:type];
242
+ }
243
+ }
244
+ }
245
+ return types;
246
+ }
247
+
248
+ std::vector<std::string> pathsFrom(NSArray<NSURL *> *urls) {
249
+ std::vector<std::string> paths;
250
+ for (NSURL *url in urls) {
251
+ if (url.path != nil) {
252
+ paths.emplace_back(url.path.UTF8String);
253
+ }
254
+ }
255
+ return paths;
256
+ }
257
+
258
+ } // namespace
259
+
260
+ void showFileDialog(const FileDialogRequest &request, FileDialogCallback onDone) {
261
+ // Copied before the block, for the same reason every other request in this
262
+ // file is: the caller's is a stack temporary and this returns first.
263
+ const FileDialogRequest content = request;
264
+
265
+ dispatch_async(dispatch_get_main_queue(), ^{
266
+ @autoreleasepool {
267
+ NSSavePanel *panel = nil;
268
+ if (content.kind == FileDialogRequest::Kind::SaveFile) {
269
+ panel = [NSSavePanel savePanel];
270
+ } else {
271
+ NSOpenPanel *open = [NSOpenPanel openPanel];
272
+ open.canChooseFiles = content.kind == FileDialogRequest::Kind::OpenFile;
273
+ open.canChooseDirectories = content.kind == FileDialogRequest::Kind::OpenFolder;
274
+ open.allowsMultipleSelection = content.multiple;
275
+ panel = open;
276
+ }
277
+
278
+ if (!content.title.empty()) {
279
+ NSString *title = [NSString stringWithUTF8String:content.title.c_str()];
280
+ // `message`, not `title`: a panel's title has not been shown since
281
+ // macOS 10.11, and the message is where the words actually appear.
282
+ panel.message = title != nil ? title : @"";
283
+ }
284
+ if (!content.confirmLabel.empty()) {
285
+ NSString *label = [NSString stringWithUTF8String:content.confirmLabel.c_str()];
286
+ panel.prompt = label != nil ? label : @"";
287
+ }
288
+ if (!content.filters.empty()) {
289
+ NSArray<UTType *> *types = contentTypesFor(content.filters);
290
+ if (types.count > 0) {
291
+ panel.allowedContentTypes = types;
292
+ }
293
+ }
294
+ if (!content.defaultPath.empty()) {
295
+ NSString *text = [NSString stringWithUTF8String:content.defaultPath.c_str()];
296
+ if (text != nil) {
297
+ if (content.kind == FileDialogRequest::Kind::SaveFile) {
298
+ // A save takes the folder and the name separately, and the caller
299
+ // may have given either or both in one string.
300
+ NSString *directory = text.stringByDeletingLastPathComponent;
301
+ NSString *name = text.lastPathComponent;
302
+ if (directory.length > 0) {
303
+ panel.directoryURL = [NSURL fileURLWithPath:directory isDirectory:YES];
304
+ }
305
+ if (name.length > 0) {
306
+ panel.nameFieldStringValue = name;
307
+ }
308
+ } else {
309
+ panel.directoryURL = [NSURL fileURLWithPath:text isDirectory:YES];
310
+ }
311
+ }
312
+ }
313
+
314
+ const auto finish = [onDone, panel](NSModalResponse response) {
315
+ if (response != NSModalResponseOK) {
316
+ onDone(true, {});
317
+ return;
318
+ }
319
+ NSArray<NSURL *> *urls = [panel isKindOfClass:[NSOpenPanel class]]
320
+ ? ((NSOpenPanel *)panel).URLs
321
+ : (panel.URL != nil ? @[panel.URL] : @[]);
322
+ const std::vector<std::string> paths = pathsFrom(urls);
323
+ onDone(paths.empty(), paths);
324
+ };
325
+
326
+ // A sheet where there is a window, for the same reason the alert is one:
327
+ // `runModal` spins its own run loop on the main thread and holds every
328
+ // mount transaction, timer and animation frame until somebody chooses.
329
+ NSWindow *window = NSApp.keyWindow != nil ? NSApp.keyWindow : NSApp.mainWindow;
330
+ if (window != nil) {
331
+ [panel beginSheetModalForWindow:window
332
+ completionHandler:^(NSModalResponse response) {
333
+ finish(response);
334
+ }];
335
+ return;
336
+ }
337
+ [panel beginWithCompletionHandler:^(NSModalResponse response) {
338
+ finish(response);
339
+ }];
340
+ }
341
+ });
342
+ }
343
+
344
+ // --- Menus --------------------------------------------------------------------
345
+
346
+ namespace {
347
+
348
+ // "Ctrl+R" into AppKit's key equivalent and modifier mask. Display only: the
349
+ // menu is opened by a key the host binds, and these are drawn beside the
350
+ // labels. An unparseable string becomes an empty equivalent, which draws
351
+ // nothing rather than something wrong.
352
+ NSString *keyEquivalentFor(const std::string &shortcut, NSEventModifierFlags *mask) {
353
+ *mask = 0;
354
+ if (shortcut.empty()) {
355
+ return @"";
356
+ }
357
+ size_t start = 0;
358
+ while (true) {
359
+ const size_t plus = shortcut.find('+', start);
360
+ const std::string part = shortcut.substr(
361
+ start, plus == std::string::npos ? std::string::npos : plus - start);
362
+ if (plus == std::string::npos) {
363
+ NSString *key = [NSString stringWithUTF8String:part.c_str()];
364
+ return key != nil ? key.lowercaseString : @"";
365
+ }
366
+ if (part == "Ctrl" || part == "Control") {
367
+ *mask |= NSEventModifierFlagControl;
368
+ } else if (part == "Shift") {
369
+ *mask |= NSEventModifierFlagShift;
370
+ } else if (part == "Alt" || part == "Option") {
371
+ *mask |= NSEventModifierFlagOption;
372
+ } else if (part == "Cmd" || part == "Meta" || part == "Super") {
373
+ *mask |= NSEventModifierFlagCommand;
374
+ } else {
375
+ *mask = 0;
376
+ return @"";
377
+ }
378
+ start = plus + 1;
379
+ }
380
+ }
381
+
382
+ } // namespace
383
+
384
+ void showMenu(const MenuRequest &request, MenuCallback onChosen) {
385
+ // Copied before the block, for the same reason showAlert and shareContent
386
+ // copy theirs: the caller's request is a stack temporary and this returns
387
+ // before the block runs.
388
+ const MenuRequest content = request;
389
+
390
+ // Onto the main thread and back at once. `popUpMenuPositioningItem` runs the
391
+ // menu's own tracking loop, which may only happen on the main thread and
392
+ // which would deadlock the runtime if it happened on the JavaScript one.
393
+ dispatch_async(dispatch_get_main_queue(), ^{
394
+ @autoreleasepool {
395
+ NSWindow *window = NSApp.keyWindow != nil ? NSApp.keyWindow : NSApp.mainWindow;
396
+ NSView *anchor = window.contentView;
397
+ if (anchor == nil) {
398
+ onChosen(-1);
399
+ return;
400
+ }
401
+
402
+ NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
403
+ menu.autoenablesItems = NO;
404
+ RnAppKitMenuTarget *target = [[RnAppKitMenuTarget alloc] init];
405
+
406
+ for (size_t i = 0; i < content.entries.size(); i++) {
407
+ const MenuEntry &entry = content.entries[i];
408
+ if (entry.isSeparator()) {
409
+ [menu addItem:[NSMenuItem separatorItem]];
410
+ continue;
411
+ }
412
+ NSEventModifierFlags mask = 0;
413
+ NSString *equivalent = keyEquivalentFor(entry.shortcut, &mask);
414
+ NSString *label = [NSString stringWithUTF8String:entry.label.c_str()];
415
+ NSMenuItem *item = [menu addItemWithTitle:label != nil ? label : @""
416
+ action:@selector(pick:)
417
+ keyEquivalent:equivalent];
418
+ item.keyEquivalentModifierMask = mask;
419
+ item.target = target;
420
+ item.enabled = entry.enabled;
421
+ // The index the caller gets back, which counts separators -- so it is
422
+ // the position in the vector rather than in this menu.
423
+ item.tag = static_cast<NSInteger>(i);
424
+ }
425
+
426
+ NSPoint at;
427
+ if (content.x >= 0.0 && content.y >= 0.0) {
428
+ // The request is in window coordinates with a top-left origin, which is
429
+ // what every view in this host uses and what AppKit does not: the
430
+ // content view is flipped, so the point is already in its terms.
431
+ at = NSMakePoint(content.x, content.y);
432
+ } else {
433
+ // At the pointer, which is what a menu opened from the keyboard wants.
434
+ const NSPoint inWindow = [window convertPointFromScreen:NSEvent.mouseLocation];
435
+ at = [anchor convertPoint:inWindow fromView:nil];
436
+ }
437
+
438
+ // Blocks until the menu is dismissed, which is what a menu does. Safe
439
+ // here and not on the JavaScript thread; see above.
440
+ [menu popUpMenuPositioningItem:nil atLocation:at inView:anchor];
441
+ onChosen(static_cast<int>(target.chosen));
442
+ }
443
+ });
444
+ }
445
+
446
+ // --- Alerts ------------------------------------------------------------------
447
+
448
+ void showAlert(const AlertRequest &request, AlertCallback onButton) {
449
+ // Copied before the block, for the same reason shareContent does it: a block
450
+ // capturing a variable of reference type captures the reference, not the
451
+ // object behind it. The caller's request is a stack local and this returns
452
+ // before the block runs, so reading through it afterwards is reading freed
453
+ // memory -- which shows up as a dialog with no text in it rather than as a
454
+ // crash, and was found by the share sheet next door coming out empty.
455
+ const AlertRequest content = request;
456
+
457
+ // Onto the main thread and back at once. This is called from the JavaScript
458
+ // thread, and a modal run loop there would deadlock the runtime -- so the
459
+ // dialog is scheduled and this returns immediately, with the answer arriving
460
+ // through the callback.
461
+ dispatch_async(dispatch_get_main_queue(), ^{
462
+ @autoreleasepool {
463
+ NSAlert *alert = [[NSAlert alloc] init];
464
+ NSString *title = [NSString stringWithUTF8String:content.title.c_str()];
465
+ NSString *message = [NSString stringWithUTF8String:content.message.c_str()];
466
+ alert.messageText = title != nil ? title : @"";
467
+ alert.informativeText = message != nil ? message : @"";
468
+
469
+ // AppKit draws buttons right to left from the first added, and React
470
+ // Native's first button is the one it considers primary -- so adding them
471
+ // in order puts the primary on the right, which is where a Mac user
472
+ // expects it and which is also what the returned index has to agree with.
473
+ for (const auto &label : content.buttons) {
474
+ NSString *text = [NSString stringWithUTF8String:label.c_str()];
475
+ [alert addButtonWithTitle:text != nil ? text : @"OK"];
476
+ }
477
+
478
+ NSTextField *field = nil;
479
+ if (content.hasTextInput) {
480
+ field = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 260, 24)];
481
+ NSString *value = [NSString stringWithUTF8String:content.defaultText.c_str()];
482
+ field.stringValue = value != nil ? value : @"";
483
+ alert.accessoryView = field;
484
+ // Otherwise the field is on screen and not typing into.
485
+ [alert.window setInitialFirstResponder:field];
486
+ }
487
+
488
+ // NSAlertFirstButtonReturn is 1000, and buttons count up from there in
489
+ // the order they were added -- which is React Native's order.
490
+ const auto finish = [onButton, field](NSModalResponse response) {
491
+ std::string text;
492
+ if (field != nil && field.stringValue.UTF8String != nullptr) {
493
+ text = field.stringValue.UTF8String;
494
+ }
495
+ onButton((int)(response - NSAlertFirstButtonReturn), text);
496
+ };
497
+
498
+ // A sheet rather than `runModal`, and not only because it looks more like
499
+ // a Mac. `runModal` spins its own run loop on the main thread, which
500
+ // stops the main queue -- so every mount transaction, timer and animation
501
+ // frame would be held until somebody clicked a button. A sheet returns
502
+ // immediately and answers through its completion handler, which is also
503
+ // what the GTK side's `gtk_alert_dialog_choose` does.
504
+ // `?:` with an omitted middle is a GNU extension, and this project
505
+ // builds with -Werror.
506
+ NSWindow *window = NSApp.keyWindow != nil ? NSApp.keyWindow : NSApp.mainWindow;
507
+ if (window != nil) {
508
+ [alert beginSheetModalForWindow:window
509
+ completionHandler:^(NSModalResponse response) {
510
+ finish(response);
511
+ }];
512
+ return;
513
+ }
514
+
515
+ // No window to attach to -- before the first one is on screen. A modal
516
+ // run loop is the only option left, and blocking briefly is better than
517
+ // dropping the alert.
518
+ finish([alert runModal]);
519
+ }
520
+ });
521
+ }
522
+
523
+ void postDelayed(double milliseconds, std::function<void()> work) {
524
+ const auto when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(milliseconds * NSEC_PER_MSEC));
525
+ dispatch_after(when, dispatch_get_main_queue(), ^{
526
+ work();
527
+ });
528
+ }
529
+
530
+ void postToUiThread(std::function<void()> work) {
531
+ dispatch_async(dispatch_get_main_queue(), ^{
532
+ work();
533
+ });
534
+ }
535
+
536
+ bool isUiThread() {
537
+ return [NSThread isMainThread];
538
+ }
539
+
540
+ } // namespace basalt