basalt-gtk 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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. package/react-native.config.js +21 -0
@@ -0,0 +1,583 @@
1
+ // Linux's half of the platform-services seam: clipboard, opening a URI, alerts.
2
+
3
+ #include "PlatformServices.h"
4
+ #include "ShareFallback.h"
5
+
6
+ #include <gtk/gtk.h>
7
+
8
+ #include <cstdlib>
9
+ #include <memory>
10
+ #include <string>
11
+ #include <utility>
12
+ #include <vector>
13
+
14
+ namespace basalt {
15
+
16
+ namespace {
17
+
18
+ GdkClipboard *defaultClipboard() {
19
+ GdkDisplay *display = gdk_display_get_default();
20
+ return display != nullptr ? gdk_display_get_clipboard(display) : nullptr;
21
+ }
22
+
23
+ } // namespace
24
+
25
+ // --- Clipboard ---------------------------------------------------------------
26
+
27
+ // GdkClipboard reads asynchronously -- it may have to ask another process for
28
+ // the text -- and this seam is synchronous because `Clipboard.getString()`
29
+ // returns a promise that core resolves at once.
30
+ //
31
+ // The compromise is the content provider's *local* value, which is what this
32
+ // application last put there, plus nothing. Reading another application's
33
+ // clipboard needs the async read and a promise core does not yet hand down.
34
+ // Saying so here beats an empty string that looks like an empty clipboard.
35
+ std::string clipboardText() {
36
+ GdkClipboard *clipboard = defaultClipboard();
37
+ if (clipboard == nullptr) {
38
+ return {};
39
+ }
40
+
41
+ GdkContentProvider *provider = gdk_clipboard_get_content(clipboard);
42
+ if (provider == nullptr) {
43
+ // Owned by another application. See the note above.
44
+ return {};
45
+ }
46
+
47
+ GValue value = G_VALUE_INIT;
48
+ g_value_init(&value, G_TYPE_STRING);
49
+ std::string text;
50
+ if (gdk_content_provider_get_value(provider, &value, nullptr)) {
51
+ const char *string = g_value_get_string(&value);
52
+ if (string != nullptr) {
53
+ text = string;
54
+ }
55
+ }
56
+ g_value_unset(&value);
57
+ return text;
58
+ }
59
+
60
+ void setClipboardText(const std::string &text) {
61
+ GdkClipboard *clipboard = defaultClipboard();
62
+ if (clipboard == nullptr) {
63
+ return;
64
+ }
65
+ gdk_clipboard_set_text(clipboard, text.c_str());
66
+ }
67
+
68
+ // --- Opening things ----------------------------------------------------------
69
+
70
+ bool canOpenUrl(const std::string &url) {
71
+ if (url.empty()) {
72
+ return false;
73
+ }
74
+ char *scheme = g_uri_parse_scheme(url.c_str());
75
+ if (scheme == nullptr) {
76
+ return false;
77
+ }
78
+ // Whether anything is registered to handle the scheme. The desktop's own
79
+ // answer, from the same database that would do the opening.
80
+ GAppInfo *handler = g_app_info_get_default_for_uri_scheme(scheme);
81
+ g_free(scheme);
82
+ if (handler == nullptr) {
83
+ return false;
84
+ }
85
+ g_object_unref(handler);
86
+ return true;
87
+ }
88
+
89
+ bool openUrl(const std::string &url) {
90
+ GError *error = nullptr;
91
+ const gboolean launched =
92
+ g_app_info_launch_default_for_uri(url.c_str(), nullptr, &error);
93
+ if (launched == FALSE) {
94
+ if (error != nullptr) {
95
+ g_warning("could not open %s: %s", url.c_str(), error->message);
96
+ g_clear_error(&error);
97
+ }
98
+ return false;
99
+ }
100
+ return true;
101
+ }
102
+
103
+ // --- Sharing -----------------------------------------------------------------
104
+
105
+ // No share service on this desktop, and no portal for one either: the
106
+ // freedesktop specifications have `xdg-email` and nothing that offers a choice
107
+ // of destinations. So this is the picker core/ShareFallback.h builds out of a
108
+ // clipboard and a mail client, which is what every desktop does have.
109
+ //
110
+ // Nothing here beyond the call, because there is nothing platform-specific
111
+ // left in it -- it goes through showAlert, setClipboardText and openUrl, all of
112
+ // which are implemented above.
113
+ void shareContent(const ShareRequest &request, ShareCallback onDone) {
114
+ shareThroughFallbackPicker(request, std::move(onDone));
115
+ }
116
+
117
+ // --- Alerts ------------------------------------------------------------------
118
+
119
+ namespace {
120
+
121
+ struct PendingAlert {
122
+ AlertRequest request;
123
+ AlertCallback onButton;
124
+ GtkWidget *field{nullptr};
125
+ };
126
+
127
+ void onAlertResponse(GObject *source, GAsyncResult *result, gpointer userData) {
128
+ std::unique_ptr<PendingAlert> pending{static_cast<PendingAlert *>(userData)};
129
+
130
+ GError *error = nullptr;
131
+ const int index =
132
+ gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
133
+ if (error != nullptr) {
134
+ // Dismissed without choosing -- Escape, or the window closed. React Native
135
+ // has no "cancelled" here, and reporting the last button would run whatever
136
+ // it does; index 0 is the primary, which is what a plain alert's only
137
+ // button is.
138
+ g_clear_error(&error);
139
+ pending->onButton(0, {});
140
+ return;
141
+ }
142
+ pending->onButton(index, {});
143
+ }
144
+
145
+ gboolean showAlertOnMainThread(gpointer userData) {
146
+ auto *pending = static_cast<PendingAlert *>(userData);
147
+
148
+ GtkAlertDialog *dialog = gtk_alert_dialog_new("%s", pending->request.title.c_str());
149
+ if (!pending->request.message.empty()) {
150
+ gtk_alert_dialog_set_detail(dialog, pending->request.message.c_str());
151
+ }
152
+
153
+ std::vector<const char *> labels;
154
+ labels.reserve(pending->request.buttons.size() + 1);
155
+ for (const auto &label : pending->request.buttons) {
156
+ labels.push_back(label.c_str());
157
+ }
158
+ labels.push_back(nullptr);
159
+ gtk_alert_dialog_set_buttons(dialog, labels.data());
160
+ // The first is React Native's primary, and the same one onAlertResponse
161
+ // reports when the dialog is dismissed without a choice.
162
+ gtk_alert_dialog_set_default_button(dialog, 0);
163
+ gtk_alert_dialog_set_cancel_button(dialog, 0);
164
+
165
+ gtk_alert_dialog_choose(dialog, nullptr, nullptr, onAlertResponse, pending);
166
+ g_object_unref(dialog);
167
+ return G_SOURCE_REMOVE;
168
+ }
169
+
170
+ } // namespace
171
+
172
+ void showAlert(const AlertRequest &request, AlertCallback onButton) {
173
+ if (request.hasTextInput) {
174
+ // GtkAlertDialog has no text field, and building one means a GtkDialog with
175
+ // its own layout. Not done: a prompt that silently became a plain alert
176
+ // would lose whatever the user was meant to type.
177
+ g_warning("Alert.prompt is not implemented on this platform");
178
+ }
179
+
180
+ auto *pending = new PendingAlert{request, std::move(onButton), nullptr};
181
+ // Onto the GTK main thread. This is called from the JavaScript thread, and
182
+ // a dialog may only be created there.
183
+ g_idle_add_full(G_PRIORITY_DEFAULT, showAlertOnMainThread, pending, nullptr);
184
+ }
185
+
186
+ // --- File dialogs -------------------------------------------------------------
187
+
188
+ namespace {
189
+
190
+ // The window a popup belongs to. GTK has no "the app's window": what it has is
191
+ // a list of toplevels, so this takes the active one, or the first visible one
192
+ // when nothing is focused -- which is what an automated run without a
193
+ // compositor looks like.
194
+ GtkWindow *activeWindow() {
195
+ GListModel *toplevels = gtk_window_get_toplevels();
196
+ if (toplevels == nullptr) {
197
+ return nullptr;
198
+ }
199
+ GtkWindow *fallback = nullptr;
200
+ const guint count = g_list_model_get_n_items(toplevels);
201
+ for (guint i = 0; i < count; i++) {
202
+ auto *window = static_cast<GtkWindow *>(g_list_model_get_item(toplevels, i));
203
+ if (window == nullptr) {
204
+ continue;
205
+ }
206
+ const bool visible = gtk_widget_get_visible(GTK_WIDGET(window)) != FALSE;
207
+ if (visible && gtk_window_is_active(window)) {
208
+ g_object_unref(window);
209
+ return window;
210
+ }
211
+ if (visible && fallback == nullptr) {
212
+ fallback = window;
213
+ }
214
+ g_object_unref(window);
215
+ }
216
+ return fallback;
217
+ }
218
+
219
+ struct PendingFileDialog {
220
+ FileDialogRequest request;
221
+ FileDialogCallback onDone;
222
+ };
223
+
224
+ // The filters, as a GListModel of GtkFileFilter. GTK takes them that way and
225
+ // not as a list of patterns, which is why this is not two lines.
226
+ GListModel *buildFilters(const std::vector<FileFilter> &filters) {
227
+ if (filters.empty()) {
228
+ return nullptr;
229
+ }
230
+ GListStore *store = g_list_store_new(GTK_TYPE_FILE_FILTER);
231
+ for (const FileFilter &filter : filters) {
232
+ GtkFileFilter *entry = gtk_file_filter_new();
233
+ gtk_file_filter_set_name(entry, filter.name.empty() ? nullptr : filter.name.c_str());
234
+ for (const std::string &extension : filter.extensions) {
235
+ // A glob rather than a MIME type: an extension is what the caller gave,
236
+ // and guessing a type from it would be a second guess on top of theirs.
237
+ const std::string pattern = "*." + extension;
238
+ gtk_file_filter_add_pattern(entry, pattern.c_str());
239
+ }
240
+ g_list_store_append(store, entry);
241
+ g_object_unref(entry);
242
+ }
243
+ return G_LIST_MODEL(store);
244
+ }
245
+
246
+ // One answer, whichever call produced it. GTK's three finishers return a GFile
247
+ // or a GListModel of them, and both arrive here as paths.
248
+ void answerWith(PendingFileDialog *pending, GFile *file, GListModel *files, GError *error) {
249
+ std::vector<std::string> paths;
250
+ if (file != nullptr) {
251
+ char *path = g_file_get_path(file);
252
+ if (path != nullptr) {
253
+ paths.emplace_back(path);
254
+ g_free(path);
255
+ }
256
+ }
257
+ if (files != nullptr) {
258
+ const guint count = g_list_model_get_n_items(files);
259
+ for (guint i = 0; i < count; i++) {
260
+ auto *item = static_cast<GFile *>(g_list_model_get_item(files, i));
261
+ if (item == nullptr) {
262
+ continue;
263
+ }
264
+ char *path = g_file_get_path(item);
265
+ if (path != nullptr) {
266
+ paths.emplace_back(path);
267
+ g_free(path);
268
+ }
269
+ g_object_unref(item);
270
+ }
271
+ }
272
+
273
+ // An error here is a dismissal: GTK reports Cancel as
274
+ // GTK_DIALOG_ERROR_DISMISSED rather than as an empty answer. A real failure
275
+ // is reported the same way on purpose -- an app cannot do anything different
276
+ // with "the portal is not running" than with "the person said no", and
277
+ // rejecting the promise would make every caller write a catch for it.
278
+ const bool canceled = error != nullptr || paths.empty();
279
+ pending->onDone(canceled, paths);
280
+ }
281
+
282
+ void onOpenFinished(GObject *source, GAsyncResult *result, gpointer userData) {
283
+ std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
284
+ GError *error = nullptr;
285
+ GFile *file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source), result, &error);
286
+ answerWith(pending.get(), file, nullptr, error);
287
+ g_clear_object(&file);
288
+ g_clear_error(&error);
289
+ }
290
+
291
+ void onOpenMultipleFinished(GObject *source, GAsyncResult *result, gpointer userData) {
292
+ std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
293
+ GError *error = nullptr;
294
+ GListModel *files = gtk_file_dialog_open_multiple_finish(GTK_FILE_DIALOG(source), result, &error);
295
+ answerWith(pending.get(), nullptr, files, error);
296
+ g_clear_object(&files);
297
+ g_clear_error(&error);
298
+ }
299
+
300
+ void onSaveFinished(GObject *source, GAsyncResult *result, gpointer userData) {
301
+ std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
302
+ GError *error = nullptr;
303
+ GFile *file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), result, &error);
304
+ answerWith(pending.get(), file, nullptr, error);
305
+ g_clear_object(&file);
306
+ g_clear_error(&error);
307
+ }
308
+
309
+ void onFolderFinished(GObject *source, GAsyncResult *result, gpointer userData) {
310
+ std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
311
+ GError *error = nullptr;
312
+ GFile *folder = gtk_file_dialog_select_folder_finish(GTK_FILE_DIALOG(source), result, &error);
313
+ answerWith(pending.get(), folder, nullptr, error);
314
+ g_clear_object(&folder);
315
+ g_clear_error(&error);
316
+ }
317
+
318
+ gboolean showFileDialogOnMainThread(gpointer userData) {
319
+ auto *pending = static_cast<PendingFileDialog *>(userData);
320
+ const FileDialogRequest &request = pending->request;
321
+
322
+ GtkFileDialog *dialog = gtk_file_dialog_new();
323
+ if (!request.title.empty()) {
324
+ gtk_file_dialog_set_title(dialog, request.title.c_str());
325
+ }
326
+ if (!request.confirmLabel.empty()) {
327
+ gtk_file_dialog_set_accept_label(dialog, request.confirmLabel.c_str());
328
+ }
329
+ if (GListModel *filters = buildFilters(request.filters)) {
330
+ gtk_file_dialog_set_filters(dialog, filters);
331
+ g_object_unref(filters);
332
+ }
333
+ if (!request.defaultPath.empty()) {
334
+ if (request.kind == FileDialogRequest::Kind::SaveFile) {
335
+ // A save takes a name and a folder separately, and the caller may have
336
+ // given either or both in one string.
337
+ GFile *file = g_file_new_for_path(request.defaultPath.c_str());
338
+ char *name = g_file_get_basename(file);
339
+ GFile *folder = g_file_get_parent(file);
340
+ if (name != nullptr) {
341
+ gtk_file_dialog_set_initial_name(dialog, name);
342
+ }
343
+ if (folder != nullptr) {
344
+ gtk_file_dialog_set_initial_folder(dialog, folder);
345
+ }
346
+ g_free(name);
347
+ g_clear_object(&folder);
348
+ g_object_unref(file);
349
+ } else {
350
+ GFile *folder = g_file_new_for_path(request.defaultPath.c_str());
351
+ gtk_file_dialog_set_initial_folder(dialog, folder);
352
+ g_object_unref(folder);
353
+ }
354
+ }
355
+
356
+ GtkWindow *parent = activeWindow();
357
+ switch (request.kind) {
358
+ case FileDialogRequest::Kind::OpenFile:
359
+ if (request.multiple) {
360
+ gtk_file_dialog_open_multiple(dialog, parent, nullptr, onOpenMultipleFinished, pending);
361
+ } else {
362
+ gtk_file_dialog_open(dialog, parent, nullptr, onOpenFinished, pending);
363
+ }
364
+ break;
365
+ case FileDialogRequest::Kind::SaveFile:
366
+ gtk_file_dialog_save(dialog, parent, nullptr, onSaveFinished, pending);
367
+ break;
368
+ case FileDialogRequest::Kind::OpenFolder:
369
+ gtk_file_dialog_select_folder(dialog, parent, nullptr, onFolderFinished, pending);
370
+ break;
371
+ }
372
+ g_object_unref(dialog);
373
+ return G_SOURCE_REMOVE;
374
+ }
375
+
376
+ } // namespace
377
+
378
+ void showFileDialog(const FileDialogRequest &request, FileDialogCallback onDone) {
379
+ auto *pending = new PendingFileDialog{request, std::move(onDone)};
380
+ // Onto the GTK main thread, for the same reason showAlert is: this is called
381
+ // from the JavaScript thread, and a dialog may only be made here.
382
+ g_idle_add_full(G_PRIORITY_DEFAULT, showFileDialogOnMainThread, pending, nullptr);
383
+ }
384
+
385
+ // --- Menus -------------------------------------------------------------------
386
+
387
+ namespace {
388
+
389
+ struct PendingMenu {
390
+ MenuRequest request;
391
+ MenuCallback onChosen;
392
+ GtkWidget *popover{nullptr};
393
+ bool answered{false};
394
+ };
395
+
396
+ // "Ctrl+R" into the "<Control>r" GTK wants for a displayed accelerator.
397
+ // Display only: nothing here binds the key. An unparseable string becomes
398
+ // empty, which shows no accelerator rather than a wrong one.
399
+ std::string toGtkAccelerator(const std::string &shortcut) {
400
+ if (shortcut.empty()) {
401
+ return {};
402
+ }
403
+ std::string out;
404
+ size_t start = 0;
405
+ while (true) {
406
+ const size_t plus = shortcut.find('+', start);
407
+ const std::string part = shortcut.substr(
408
+ start, plus == std::string::npos ? std::string::npos : plus - start);
409
+ if (plus == std::string::npos) {
410
+ // The key itself, lowercased: GTK matches keyval names, and "R" is not one.
411
+ for (char c : part) {
412
+ out += static_cast<char>(g_ascii_tolower(c));
413
+ }
414
+ break;
415
+ }
416
+ if (part == "Ctrl" || part == "Control") {
417
+ out += "<Control>";
418
+ } else if (part == "Shift") {
419
+ out += "<Shift>";
420
+ } else if (part == "Alt" || part == "Option") {
421
+ out += "<Alt>";
422
+ } else if (part == "Cmd" || part == "Meta" || part == "Super") {
423
+ out += "<Meta>";
424
+ } else {
425
+ return {};
426
+ }
427
+ start = plus + 1;
428
+ }
429
+ return out;
430
+ }
431
+
432
+ // Answers once, whichever comes first: an item activating, or the popover
433
+ // closing with nothing chosen. Both happen for a menu that was used, and the
434
+ // order is not guaranteed.
435
+ void finishMenu(PendingMenu *pending, int index) {
436
+ if (pending->answered) {
437
+ return;
438
+ }
439
+ pending->answered = true;
440
+ pending->onChosen(index);
441
+ }
442
+
443
+ void onMenuItemActivated(GSimpleAction *action, GVariant * /*parameter*/, gpointer userData) {
444
+ auto *pending = static_cast<PendingMenu *>(userData);
445
+ // The action is named "item<N>", which is where the index comes from: a
446
+ // GMenu carries no index of its own, and the position in the model is not
447
+ // the position in the vector once separators are sections.
448
+ const char *name = g_action_get_name(G_ACTION(action));
449
+ finishMenu(pending, name != nullptr ? std::atoi(name + 4) : -1);
450
+ if (pending->popover != nullptr) {
451
+ gtk_popover_popdown(GTK_POPOVER(pending->popover));
452
+ }
453
+ }
454
+
455
+ void onMenuClosed(GtkPopover * /*popover*/, gpointer userData) {
456
+ auto *pending = static_cast<PendingMenu *>(userData);
457
+ finishMenu(pending, -1);
458
+ // Unparented from an idle rather than here: GTK is still inside the
459
+ // popover's own signal emission, and destroying the widget it is emitting
460
+ // from is how a popup menu turns into a use-after-free.
461
+ g_idle_add_full(
462
+ G_PRIORITY_DEFAULT_IDLE,
463
+ [](gpointer data) -> gboolean {
464
+ std::unique_ptr<PendingMenu> owned{static_cast<PendingMenu *>(data)};
465
+ if (owned->popover != nullptr) {
466
+ gtk_widget_unparent(owned->popover);
467
+ }
468
+ return G_SOURCE_REMOVE;
469
+ },
470
+ pending,
471
+ nullptr);
472
+ }
473
+
474
+ gboolean showMenuOnMainThread(gpointer userData) {
475
+ auto *pending = static_cast<PendingMenu *>(userData);
476
+
477
+ GtkWindow *window = activeWindow();
478
+ GtkWidget *anchor = window != nullptr ? gtk_window_get_child(window) : nullptr;
479
+ if (anchor == nullptr) {
480
+ finishMenu(pending, -1);
481
+ delete pending;
482
+ return G_SOURCE_REMOVE;
483
+ }
484
+
485
+ // Separators are sections rather than entries: a GMenu has no separator item,
486
+ // and two sections are drawn with a line between them. The indexes the
487
+ // caller gets back still count them, which is what the action names carry.
488
+ GMenu *model = g_menu_new();
489
+ GMenu *section = g_menu_new();
490
+ auto *actions = g_simple_action_group_new();
491
+
492
+ for (size_t i = 0; i < pending->request.entries.size(); i++) {
493
+ const MenuEntry &entry = pending->request.entries[i];
494
+ if (entry.isSeparator()) {
495
+ g_menu_append_section(model, nullptr, G_MENU_MODEL(section));
496
+ g_object_unref(section);
497
+ section = g_menu_new();
498
+ continue;
499
+ }
500
+
501
+ const std::string name = "item" + std::to_string(i);
502
+ auto *action = g_simple_action_new(name.c_str(), nullptr);
503
+ g_simple_action_set_enabled(action, entry.enabled ? TRUE : FALSE);
504
+ g_signal_connect(action, "activate", G_CALLBACK(onMenuItemActivated), pending);
505
+ g_action_map_add_action(G_ACTION_MAP(actions), G_ACTION(action));
506
+ g_object_unref(action);
507
+
508
+ GMenuItem *item = g_menu_item_new(entry.label.c_str(), ("menu." + name).c_str());
509
+ const std::string accelerator = toGtkAccelerator(entry.shortcut);
510
+ if (!accelerator.empty()) {
511
+ g_menu_item_set_attribute(item, "accel", "s", accelerator.c_str());
512
+ }
513
+ g_menu_append_item(section, item);
514
+ g_object_unref(item);
515
+ }
516
+ g_menu_append_section(model, nullptr, G_MENU_MODEL(section));
517
+ g_object_unref(section);
518
+
519
+ GtkWidget *popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(model));
520
+ g_object_unref(model);
521
+ pending->popover = popover;
522
+
523
+ gtk_widget_insert_action_group(popover, "menu", G_ACTION_GROUP(actions));
524
+ g_object_unref(actions);
525
+
526
+ gtk_widget_set_parent(popover, anchor);
527
+ gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
528
+ if (pending->request.x >= 0.0 && pending->request.y >= 0.0) {
529
+ const GdkRectangle at = {static_cast<int>(pending->request.x),
530
+ static_cast<int>(pending->request.y),
531
+ 1,
532
+ 1};
533
+ gtk_popover_set_pointing_to(GTK_POPOVER(popover), &at);
534
+ }
535
+ g_signal_connect(popover, "closed", G_CALLBACK(onMenuClosed), pending);
536
+ gtk_popover_popup(GTK_POPOVER(popover));
537
+ return G_SOURCE_REMOVE;
538
+ }
539
+
540
+ } // namespace
541
+
542
+ void showMenu(const MenuRequest &request, MenuCallback onChosen) {
543
+ auto *pending = new PendingMenu{request, std::move(onChosen), nullptr, false};
544
+ // Onto the GTK main thread, for the same reason showAlert is: this can be
545
+ // called from the JavaScript thread, and a widget may only be made here.
546
+ g_idle_add_full(G_PRIORITY_DEFAULT, showMenuOnMainThread, pending, nullptr);
547
+ }
548
+
549
+ void postDelayed(double milliseconds, std::function<void()> work) {
550
+ // Heap-allocated because g_timeout_add takes a void*, and freed by the
551
+ // destroy notify whether the callback ran or the source was removed.
552
+ auto *held = new std::function<void()>(std::move(work));
553
+ g_timeout_add_full(
554
+ G_PRIORITY_DEFAULT,
555
+ static_cast<guint>(milliseconds),
556
+ [](gpointer data) -> gboolean {
557
+ (*static_cast<std::function<void()> *>(data))();
558
+ return G_SOURCE_REMOVE;
559
+ },
560
+ held,
561
+ [](gpointer data) { delete static_cast<std::function<void()> *>(data); });
562
+ }
563
+
564
+ void postToUiThread(std::function<void()> work) {
565
+ auto *held = new std::function<void()>(std::move(work));
566
+ g_idle_add_full(
567
+ G_PRIORITY_DEFAULT,
568
+ [](gpointer data) -> gboolean {
569
+ (*static_cast<std::function<void()> *>(data))();
570
+ return G_SOURCE_REMOVE;
571
+ },
572
+ held,
573
+ [](gpointer data) { delete static_cast<std::function<void()> *>(data); });
574
+ }
575
+
576
+ bool isUiThread() {
577
+ // The thread running the default main context is the one GTK draws from, and
578
+ // asking that rather than remembering a thread id keeps this true in a test
579
+ // binary that never starts a loop -- where the honest answer is "no".
580
+ return g_main_context_is_owner(g_main_context_default()) != 0;
581
+ }
582
+
583
+ } // namespace basalt