basalt-win32 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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,643 @@
1
+ // Windows' half of core/PlatformServices.h: the clipboard, opening a URL, an
2
+ // alert, and getting work onto the UI thread.
3
+ //
4
+ // The last of those is the one the mounting manager depends on, and it is the
5
+ // only one with a real design decision in it. See postToUiThread below.
6
+
7
+ #include "PlatformServices.h"
8
+ #include "ShareFallback.h"
9
+
10
+ #include "Win32Strings.h"
11
+ #include "Win32UiThread.h"
12
+
13
+ #include <windows.h>
14
+
15
+ #include <shellapi.h>
16
+ #include <shlobj.h>
17
+ #include <shobjidl.h>
18
+
19
+ #include <mutex>
20
+ #include <string>
21
+ #include <thread>
22
+ #include <utility>
23
+ #include <vector>
24
+
25
+ namespace basalt {
26
+
27
+ using win32::narrow;
28
+ using win32::widen;
29
+ namespace {
30
+
31
+ // --- the UI thread -----------------------------------------------------------
32
+ //
33
+ // A message-only window, created on whichever thread calls `installUiThread`
34
+ // first -- which the host does before it starts React Native, and which the
35
+ // tests do not do at all.
36
+ //
37
+ // A message-only window (HWND_MESSAGE) rather than posting to the host's real
38
+ // window: it exists before any surface does, it cannot be destroyed by a user
39
+ // closing something, and it keeps this seam from needing to know what the host
40
+ // draws into. It is also the mechanism a Win32 program is expected to use --
41
+ // PostMessage is the only thread-safe way into a message loop.
42
+ //
43
+ // GTK does this with g_idle_add_full and macOS with dispatch_async_f. All three
44
+ // are the same promise: the work runs on the UI thread, and work posted earlier
45
+ // runs first. That ordering is not a nicety -- a command that arrived before
46
+ // the transaction creating the view it names would find nothing and be dropped.
47
+
48
+ constexpr UINT kRunWork = WM_APP + 1;
49
+
50
+ struct UiThread {
51
+ std::mutex mutex;
52
+ std::vector<std::function<void()>> pending;
53
+ HWND window = nullptr;
54
+ std::thread::id threadId{};
55
+ };
56
+
57
+ UiThread &uiThread() {
58
+ static UiThread instance;
59
+ return instance;
60
+ }
61
+
62
+ void drainPendingWork() {
63
+ std::vector<std::function<void()>> work;
64
+ {
65
+ std::lock_guard<std::mutex> lock(uiThread().mutex);
66
+ work.swap(uiThread().pending);
67
+ }
68
+ for (auto &item : work) {
69
+ item();
70
+ }
71
+ }
72
+
73
+ LRESULT CALLBACK uiThreadProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
74
+ if (message == kRunWork) {
75
+ drainPendingWork();
76
+ return 0;
77
+ }
78
+ return DefWindowProc(hwnd, message, wparam, lparam);
79
+ }
80
+
81
+ } // namespace
82
+
83
+ // See Win32UiThread.h.
84
+
85
+ void installUiThread() {
86
+ if (uiThread().window != nullptr) {
87
+ return;
88
+ }
89
+ static const ATOM registered = [] {
90
+ WNDCLASSEXW windowClass{};
91
+ windowClass.cbSize = sizeof(windowClass);
92
+ windowClass.lpfnWndProc = uiThreadProc;
93
+ windowClass.hInstance = GetModuleHandleW(nullptr);
94
+ windowClass.lpszClassName = L"BasaltUiThreadDispatch";
95
+ return RegisterClassExW(&windowClass);
96
+ }();
97
+ (void)registered;
98
+
99
+ uiThread().threadId = std::this_thread::get_id();
100
+ uiThread().window = CreateWindowExW(0,
101
+ L"BasaltUiThreadDispatch",
102
+ L"",
103
+ 0,
104
+ 0,
105
+ 0,
106
+ 0,
107
+ 0,
108
+ HWND_MESSAGE,
109
+ nullptr,
110
+ GetModuleHandleW(nullptr),
111
+ nullptr);
112
+ }
113
+
114
+ bool hasUiThread() {
115
+ return uiThread().window != nullptr;
116
+ }
117
+
118
+ bool isUiThread() {
119
+ // With no UI thread installed there is only one thread that matters, so
120
+ // saying yes is both true and what keeps the tests from marshalling into a
121
+ // message loop nobody is pumping.
122
+ if (uiThread().window == nullptr) {
123
+ return true;
124
+ }
125
+ return std::this_thread::get_id() == uiThread().threadId;
126
+ }
127
+
128
+ void postToUiThread(std::function<void()> work) {
129
+ if (!work) {
130
+ return;
131
+ }
132
+
133
+ // No host, so no message loop to post to: run it here. This is the harness
134
+ // and the test path, and it is safe precisely because there is no second
135
+ // thread in it. A real host installs the window first, after which even work
136
+ // posted *from* the UI thread is queued rather than run inline -- which is
137
+ // what preserves ordering against work already in the queue, and is the same
138
+ // rule the other two platforms follow.
139
+ if (uiThread().window == nullptr) {
140
+ work();
141
+ return;
142
+ }
143
+
144
+ {
145
+ std::lock_guard<std::mutex> lock(uiThread().mutex);
146
+ uiThread().pending.push_back(std::move(work));
147
+ }
148
+ PostMessageW(uiThread().window, kRunWork, 0, 0);
149
+ }
150
+
151
+ void postDelayed(double milliseconds, std::function<void()> work) {
152
+ if (!work) {
153
+ return;
154
+ }
155
+ if (uiThread().window == nullptr) {
156
+ work();
157
+ return;
158
+ }
159
+ // A timer queue rather than SetTimer: SetTimer's resolution is the message
160
+ // loop's and it coalesces, which is wrong for something a JavaScript timer is
161
+ // waiting on. The callback arrives on a pool thread, so it is bounced onto
162
+ // the UI thread rather than running there.
163
+ struct Delayed {
164
+ std::function<void()> work;
165
+ HANDLE timer = nullptr;
166
+ };
167
+ auto *delayed = new Delayed{std::move(work), nullptr};
168
+ const auto fire = [](PVOID parameter, BOOLEAN) {
169
+ auto *item = static_cast<Delayed *>(parameter);
170
+ postToUiThread([item] {
171
+ item->work();
172
+ if (item->timer != nullptr) {
173
+ DeleteTimerQueueTimer(nullptr, item->timer, nullptr);
174
+ }
175
+ delete item;
176
+ });
177
+ };
178
+ if (!CreateTimerQueueTimer(&delayed->timer,
179
+ nullptr,
180
+ fire,
181
+ delayed,
182
+ static_cast<DWORD>(milliseconds < 0 ? 0 : milliseconds),
183
+ 0,
184
+ WT_EXECUTEONLYONCE)) {
185
+ // Could not schedule it; running late is better than never.
186
+ postToUiThread(std::move(delayed->work));
187
+ delete delayed;
188
+ }
189
+ }
190
+
191
+ // --- clipboard ---------------------------------------------------------------
192
+
193
+ namespace {
194
+
195
+ // The clipboard is one lock shared by every window on the desktop, and
196
+ // OpenClipboard does not wait for it: if a window anywhere has it open, the
197
+ // call fails at once with access denied. Clipboard listeners are windows, and
198
+ // each is told of every change and opens the clipboard to read it, so a read or
199
+ // a write that lands while one is reading is refused -- and before this, the
200
+ // refusal was silent: a write simply did not happen.
201
+ //
202
+ // Found as js/modules.js failing "clipboard takes unicode", its second write in
203
+ // a row, in 2 of 15 runs on the development machine, which compare_hosts.sh
204
+ // reported as Windows disagreeing with Linux. What was holding the clipboard
205
+ // was never caught: with this retry in place and logging on every refusal, 190
206
+ // further runs saw none at all. So the retry is not proven to be what fixed
207
+ // that; tests/test_win32_clipboard.cpp is what shows it is needed, by holding
208
+ // the clipboard from a window and failing without it.
209
+ //
210
+ // A few short tries is what everyone does, Chromium included. Sleep(5) sleeps
211
+ // a scheduler tick, about 15ms, so this is up to ~150ms on the UI thread --
212
+ // only when somebody else has the clipboard, and a listener holds it for a
213
+ // read rather than for long.
214
+ bool openClipboard() {
215
+ constexpr int kAttempts = 10;
216
+ constexpr DWORD kPauseMs = 5;
217
+ for (int attempt = 0; attempt < kAttempts; ++attempt) {
218
+ if (OpenClipboard(nullptr)) {
219
+ return true;
220
+ }
221
+ Sleep(kPauseMs);
222
+ }
223
+ return false;
224
+ }
225
+
226
+ } // namespace
227
+
228
+ std::string clipboardText() {
229
+ if (!openClipboard()) {
230
+ return {};
231
+ }
232
+ std::string result;
233
+ // CF_UNICODETEXT rather than CF_TEXT: the ANSI variant loses anything outside
234
+ // the active code page, and a clipboard is exactly where that shows up.
235
+ if (const HANDLE handle = GetClipboardData(CF_UNICODETEXT)) {
236
+ if (const auto *text = static_cast<const wchar_t *>(GlobalLock(handle))) {
237
+ result = narrow(text);
238
+ GlobalUnlock(handle);
239
+ }
240
+ }
241
+ CloseClipboard();
242
+ return result;
243
+ }
244
+
245
+ void setClipboardText(const std::string &text) {
246
+ if (!openClipboard()) {
247
+ return;
248
+ }
249
+ EmptyClipboard();
250
+
251
+ const std::wstring wide = widen(text);
252
+ const size_t bytes = (wide.size() + 1) * sizeof(wchar_t);
253
+ // GMEM_MOVEABLE, and ownership passes to the clipboard on success -- freeing
254
+ // it afterwards is a use-after-free that usually survives long enough to be
255
+ // mysterious.
256
+ if (const HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes)) {
257
+ if (auto *destination = static_cast<wchar_t *>(GlobalLock(handle))) {
258
+ memcpy(destination, wide.c_str(), bytes);
259
+ GlobalUnlock(handle);
260
+ if (SetClipboardData(CF_UNICODETEXT, handle) == nullptr) {
261
+ GlobalFree(handle);
262
+ }
263
+ } else {
264
+ GlobalFree(handle);
265
+ }
266
+ }
267
+ CloseClipboard();
268
+ }
269
+
270
+ // --- opening a URL -----------------------------------------------------------
271
+
272
+ bool canOpenUrl(const std::string &url) {
273
+ // Answered by asking the registry whether the scheme is registered, which is
274
+ // what "can open" means on Windows: a scheme is a key under HKCR with a
275
+ // URL Protocol value. Not by trying to open it, which would open it.
276
+ const size_t colon = url.find(':');
277
+ if (colon == std::string::npos || colon == 0) {
278
+ return false;
279
+ }
280
+ const std::wstring scheme = widen(url.substr(0, colon));
281
+
282
+ HKEY key = nullptr;
283
+ if (RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) {
284
+ return false;
285
+ }
286
+ const bool isProtocol =
287
+ RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS;
288
+ RegCloseKey(key);
289
+ return isProtocol;
290
+ }
291
+
292
+ bool openUrl(const std::string &url) {
293
+ const std::wstring wide = widen(url);
294
+ // ShellExecute returns a fake HINSTANCE; anything above 32 is success. That
295
+ // is the documented contract, odd as it looks.
296
+ const auto result = reinterpret_cast<INT_PTR>(
297
+ ShellExecuteW(nullptr, L"open", wide.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
298
+ return result > 32;
299
+ }
300
+
301
+ // --- sharing -----------------------------------------------------------------
302
+
303
+ // Windows does have a share UI -- the DataTransferManager's, the one the Share
304
+ // charm opens -- and reaching it means WinRT interop:
305
+ // `IDataTransferManagerInterop::ShowShareUIForWindow` against an HWND, plus the
306
+ // activation and the data-requested handler. That is worth doing and is not
307
+ // done here, because it cannot be tested from the machine this was written on
308
+ // and a share sheet that fails is worse than one that is plainly minimal. See
309
+ // docs/BACKLOG.md.
310
+ //
311
+ // Until then, the picker core/ShareFallback.h builds out of a clipboard and a
312
+ // mail client, which is what every desktop has. Nothing here beyond the call:
313
+ // it goes through showAlert, setClipboardText and openUrl, all of which are
314
+ // implemented above.
315
+ void shareContent(const ShareRequest &request, ShareCallback onDone) {
316
+ shareThroughFallbackPicker(request, std::move(onDone));
317
+ }
318
+
319
+ // --- file dialogs -------------------------------------------------------------
320
+
321
+ namespace {
322
+
323
+ // The filters, in the shape IFileDialog takes: one COMDLG_FILTERSPEC per entry,
324
+ // with the extensions joined into a single semicolon-separated pattern. The
325
+ // strings have to outlive the call, which is why they are built into a vector
326
+ // that the caller keeps rather than into temporaries.
327
+ struct FilterStorage {
328
+ std::vector<std::wstring> names;
329
+ std::vector<std::wstring> patterns;
330
+ std::vector<COMDLG_FILTERSPEC> specs;
331
+ };
332
+
333
+ FilterStorage buildFilters(const std::vector<FileFilter> &filters) {
334
+ FilterStorage storage;
335
+ storage.names.reserve(filters.size());
336
+ storage.patterns.reserve(filters.size());
337
+ for (const FileFilter &filter : filters) {
338
+ std::wstring pattern;
339
+ for (const std::string &extension : filter.extensions) {
340
+ if (!pattern.empty()) {
341
+ pattern += L";";
342
+ }
343
+ pattern += L"*.";
344
+ pattern += widen(extension);
345
+ }
346
+ storage.names.push_back(widen(filter.name.empty() ? "Files" : filter.name));
347
+ storage.patterns.push_back(std::move(pattern));
348
+ }
349
+ // Built second, because a vector that grows moves its strings and the specs
350
+ // hold pointers into them.
351
+ storage.specs.reserve(storage.names.size());
352
+ for (size_t i = 0; i < storage.names.size(); i++) {
353
+ storage.specs.push_back(
354
+ COMDLG_FILTERSPEC{storage.names[i].c_str(), storage.patterns[i].c_str()});
355
+ }
356
+ return storage;
357
+ }
358
+
359
+ std::string pathOf(IShellItem *item) {
360
+ PWSTR path = nullptr;
361
+ if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) || path == nullptr) {
362
+ return {};
363
+ }
364
+ const std::string result = narrow(path);
365
+ CoTaskMemFree(path);
366
+ return result;
367
+ }
368
+
369
+ } // namespace
370
+
371
+ void showFileDialog(const FileDialogRequest &request, FileDialogCallback onDone) {
372
+ // Onto the UI thread. IFileDialog::Show runs its own modal loop and must be
373
+ // called on a thread with an apartment, which is the one the host's window
374
+ // lives on -- and running it on the JavaScript thread would deadlock the
375
+ // runtime.
376
+ postToUiThread([request, onDone = std::move(onDone)] {
377
+ const bool saving = request.kind == FileDialogRequest::Kind::SaveFile;
378
+
379
+ IFileDialog *dialog = nullptr;
380
+ const HRESULT created = CoCreateInstance(saving ? CLSID_FileSaveDialog : CLSID_FileOpenDialog,
381
+ nullptr,
382
+ CLSCTX_INPROC_SERVER,
383
+ IID_IFileDialog,
384
+ (void **)&dialog);
385
+ if (FAILED(created) || dialog == nullptr) {
386
+ onDone(true, {});
387
+ return;
388
+ }
389
+
390
+ DWORD options = 0;
391
+ dialog->GetOptions(&options);
392
+ options |= FOS_FORCEFILESYSTEM;
393
+ if (request.kind == FileDialogRequest::Kind::OpenFolder) {
394
+ options |= FOS_PICKFOLDERS;
395
+ }
396
+ if (request.kind == FileDialogRequest::Kind::OpenFile && request.multiple) {
397
+ options |= FOS_ALLOWMULTISELECT;
398
+ }
399
+ dialog->SetOptions(options);
400
+
401
+ if (!request.title.empty()) {
402
+ dialog->SetTitle(widen(request.title).c_str());
403
+ }
404
+ if (!request.confirmLabel.empty()) {
405
+ dialog->SetOkButtonLabel(widen(request.confirmLabel).c_str());
406
+ }
407
+ // A folder picker with file-type filters is a dialog that filters nothing
408
+ // and shows a combo box saying so.
409
+ FilterStorage filters;
410
+ if (request.kind != FileDialogRequest::Kind::OpenFolder && !request.filters.empty()) {
411
+ filters = buildFilters(request.filters);
412
+ if (!filters.specs.empty()) {
413
+ dialog->SetFileTypes(static_cast<UINT>(filters.specs.size()), filters.specs.data());
414
+ }
415
+ }
416
+ if (!request.defaultPath.empty()) {
417
+ const std::wstring wide = widen(request.defaultPath);
418
+ const size_t slash = wide.find_last_of(L"\\/");
419
+ const std::wstring directory = slash == std::wstring::npos ? L"" : wide.substr(0, slash);
420
+ const std::wstring name = slash == std::wstring::npos ? wide : wide.substr(slash + 1);
421
+
422
+ if (saving && !name.empty()) {
423
+ dialog->SetFileName(name.c_str());
424
+ }
425
+ const std::wstring folder = saving ? directory : wide;
426
+ if (!folder.empty()) {
427
+ IShellItem *item = nullptr;
428
+ if (SUCCEEDED(SHCreateItemFromParsingName(folder.c_str(), nullptr, IID_IShellItem,
429
+ (void **)&item)) &&
430
+ item != nullptr) {
431
+ dialog->SetFolder(item);
432
+ item->Release();
433
+ }
434
+ }
435
+ }
436
+
437
+ const HRESULT shown = dialog->Show(GetActiveWindow());
438
+ if (FAILED(shown)) {
439
+ // HRESULT_FROM_WIN32(ERROR_CANCELLED) is Cancel, and every other failure
440
+ // is reported the same way on purpose: an app can do nothing different
441
+ // with "the dialog would not open" than with "the person said no".
442
+ dialog->Release();
443
+ onDone(true, {});
444
+ return;
445
+ }
446
+
447
+ std::vector<std::string> paths;
448
+ IFileOpenDialog *openDialog = nullptr;
449
+ if (!saving && SUCCEEDED(dialog->QueryInterface(IID_IFileOpenDialog, (void **)&openDialog)) &&
450
+ openDialog != nullptr) {
451
+ IShellItemArray *items = nullptr;
452
+ if (SUCCEEDED(openDialog->GetResults(&items)) && items != nullptr) {
453
+ DWORD count = 0;
454
+ items->GetCount(&count);
455
+ for (DWORD i = 0; i < count; i++) {
456
+ IShellItem *item = nullptr;
457
+ if (SUCCEEDED(items->GetItemAt(i, &item)) && item != nullptr) {
458
+ std::string path = pathOf(item);
459
+ if (!path.empty()) {
460
+ paths.push_back(std::move(path));
461
+ }
462
+ item->Release();
463
+ }
464
+ }
465
+ items->Release();
466
+ }
467
+ openDialog->Release();
468
+ } else {
469
+ IShellItem *item = nullptr;
470
+ if (SUCCEEDED(dialog->GetResult(&item)) && item != nullptr) {
471
+ std::string path = pathOf(item);
472
+ if (!path.empty()) {
473
+ paths.push_back(std::move(path));
474
+ }
475
+ item->Release();
476
+ }
477
+ }
478
+
479
+ dialog->Release();
480
+ onDone(paths.empty(), paths);
481
+ });
482
+ }
483
+
484
+ // --- menus --------------------------------------------------------------------
485
+
486
+ namespace {
487
+
488
+ // "Ctrl+R" as a menu label suffix. Windows draws an accelerator by convention
489
+ // rather than by property: the text after a tab in an item's string is
490
+ // right-aligned, and that is all an HMENU knows about shortcuts. Nothing here
491
+ // binds the key -- the host does.
492
+ std::wstring withShortcut(const std::string &label, const std::string &shortcut) {
493
+ std::wstring text = widen(label);
494
+ if (!shortcut.empty()) {
495
+ text += L"\t";
496
+ text += widen(shortcut);
497
+ }
498
+ return text;
499
+ }
500
+
501
+ } // namespace
502
+
503
+ void showMenu(const MenuRequest &request, MenuCallback onChosen) {
504
+ // Onto the UI thread. TrackPopupMenuEx runs its own message loop and must be
505
+ // called on the thread that owns the window -- and running it on the
506
+ // JavaScript thread would deadlock the runtime, which is the same reason
507
+ // showAlert and the GTK and AppKit menus marshal.
508
+ postToUiThread([request, onChosen = std::move(onChosen)] {
509
+ HWND window = GetActiveWindow();
510
+ if (window == nullptr) {
511
+ window = GetForegroundWindow();
512
+ }
513
+ if (window == nullptr) {
514
+ onChosen(-1);
515
+ return;
516
+ }
517
+
518
+ HMENU menu = CreatePopupMenu();
519
+ if (menu == nullptr) {
520
+ onChosen(-1);
521
+ return;
522
+ }
523
+
524
+ for (size_t i = 0; i < request.entries.size(); i++) {
525
+ const MenuEntry &entry = request.entries[i];
526
+ if (entry.isSeparator()) {
527
+ AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
528
+ continue;
529
+ }
530
+ // The command id is the index plus one: TrackPopupMenu returns zero for
531
+ // "nothing was chosen", so zero cannot also mean the first item.
532
+ const std::wstring text = withShortcut(entry.label, entry.shortcut);
533
+ AppendMenuW(menu,
534
+ MF_STRING | (entry.enabled ? MF_ENABLED : MF_GRAYED),
535
+ static_cast<UINT_PTR>(i + 1),
536
+ text.c_str());
537
+ }
538
+
539
+ POINT at;
540
+ if (request.x >= 0.0 && request.y >= 0.0) {
541
+ // The request is in the window's client coordinates, which is where every
542
+ // other coordinate in this host lives; TrackPopupMenuEx wants screen.
543
+ at.x = static_cast<LONG>(request.x);
544
+ at.y = static_cast<LONG>(request.y);
545
+ ClientToScreen(window, &at);
546
+ } else if (!GetCursorPos(&at)) {
547
+ at.x = 0;
548
+ at.y = 0;
549
+ }
550
+
551
+ // SetForegroundWindow before and the null PostMessage after are both from
552
+ // the documented TrackPopupMenu recipe: without them a menu opened from a
553
+ // window that is not foreground stays up after a click elsewhere, because
554
+ // it never receives the message that would dismiss it.
555
+ SetForegroundWindow(window);
556
+ const int chosen = TrackPopupMenuEx(menu,
557
+ TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY,
558
+ at.x,
559
+ at.y,
560
+ window,
561
+ nullptr);
562
+ PostMessageW(window, WM_NULL, 0, 0);
563
+ DestroyMenu(menu);
564
+
565
+ onChosen(chosen > 0 ? chosen - 1 : -1);
566
+ });
567
+ }
568
+
569
+ // --- alerts ------------------------------------------------------------------
570
+
571
+ void showAlert(const AlertRequest &request, AlertCallback onButton) {
572
+ // MessageBox, which means up to three buttons and no text field. React
573
+ // Native's Alert allows more of both.
574
+ //
575
+ // TaskDialogIndirect would give arbitrary buttons and is the right answer;
576
+ // it needs a comctl32 v6 manifest, which is a host concern rather than a
577
+ // seam concern, so it waits for the host. A prompt needs a custom dialog on
578
+ // every platform -- GTK has the same gap, per docs/BACKLOG.md -- and is not
579
+ // faked here.
580
+ //
581
+ // Onto the UI thread, and back at once. PlatformServices.h says this must
582
+ // not block, and it did: MessageBoxW ran right here, on the
583
+ // JavaScript thread, so while an alert was up the runtime rendered nothing,
584
+ // and a window closed meanwhile never quit -- shutdown joins the JavaScript
585
+ // thread, which was sitting in a dialog nobody would answer. showMenu's
586
+ // comment below even says it marshals "for the same reason showAlert" does.
587
+ // It did not; now it does. The end-to-end suite never saw it because its
588
+ // alerts are answered by BASALT_TEST_DIALOG before this is reached; the
589
+ // cross-host comparison, which does not script dialogs, hung on js/share.js.
590
+ const std::wstring title = widen(request.title);
591
+ const std::wstring message = widen(request.message);
592
+
593
+ UINT type = MB_OK;
594
+ if (request.buttons.size() == 2) {
595
+ type = MB_OKCANCEL;
596
+ } else if (request.buttons.size() >= 3) {
597
+ type = MB_YESNOCANCEL;
598
+ }
599
+
600
+ const size_t buttonCount = request.buttons.size();
601
+ postToUiThread([title, message, type, buttonCount, onButton = std::move(onButton)] {
602
+ // Owned by the app's window rather than by nothing: modal to it, placed
603
+ // over it, and destroyed with it -- so a window that closes while an alert
604
+ // is up takes the alert along instead of leaving it on screen for a
605
+ // process that is trying to exit.
606
+ HWND owner = GetActiveWindow();
607
+ if (owner == nullptr) {
608
+ owner = GetForegroundWindow();
609
+ }
610
+ // Task-modal only when there is no window to own it: then it is the one
611
+ // way to keep the rest of this thread's windows from taking input.
612
+ const UINT modality = owner == nullptr ? MB_TASKMODAL : 0;
613
+ const int pressed = MessageBoxW(owner, message.c_str(), title.c_str(), type | modality);
614
+ // Zero is the dialog going away without an answer, which is what its
615
+ // owner being destroyed does. There is no button to report, and the app
616
+ // asking has already been told its window is closing.
617
+ if (!onButton || pressed == 0) {
618
+ return;
619
+ }
620
+
621
+ // Reported as an index into the buttons the app gave, which is what React
622
+ // Native's callback means, rather than as Windows' IDOK/IDCANCEL.
623
+ int index = 0;
624
+ switch (pressed) {
625
+ case IDOK:
626
+ case IDYES:
627
+ index = 0;
628
+ break;
629
+ case IDNO:
630
+ index = 1;
631
+ break;
632
+ case IDCANCEL:
633
+ index = buttonCount == 0 ? 0 : static_cast<int>(buttonCount) - 1;
634
+ break;
635
+ default:
636
+ index = 0;
637
+ break;
638
+ }
639
+ onButton(index, std::string{});
640
+ });
641
+ }
642
+
643
+ } // namespace basalt