janela 0.16.0 → 0.16.1

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 (2) hide show
  1. package/package.json +1 -1
  2. package/shim/wvshim.cc +142 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Desktop, iOS and Android apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
package/shim/wvshim.cc CHANGED
@@ -358,6 +358,137 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
358
358
 
359
359
  #if defined(__APPLE__)
360
360
 
361
+ // A standard macOS main menu.
362
+ //
363
+ // On macOS a Command-key shortcut is a MENU key equivalent, not a
364
+ // window-manager gesture the way Alt+F4 is on Windows. With no main menu
365
+ // nothing claims Cmd+Q, Cmd+C, Cmd+V, Cmd+Z or Cmd+A, and a webview app simply
366
+ // appears to ignore them — the window had zero menu bars, which is exactly
367
+ // what that looks like from the outside.
368
+ //
369
+ // webview.h leaves this to the embedder on purpose: webview/webview#127 is
370
+ // still open, and #237, which added precisely this Edit menu, was closed with
371
+ // "anyone who is impatient can always take this code on their own". Tauri does
372
+ // the same thing one layer up, through muda.
373
+ //
374
+ // Every item here is a STANDARD AppKit selector travelling up the responder
375
+ // chain, so there is nothing to call back into TypeScript for and no new FFI
376
+ // surface. WKWebView answers the editing ones itself. Custom menus — a
377
+ // declarative table passed down from the host — are a separate feature.
378
+ static void install_main_menu() {
379
+ using namespace webview::detail;
380
+ objc::autoreleasepool arp;
381
+
382
+ id app = objc::msg_send<id>(objc::get_class("NSApplication"),
383
+ objc::selector("sharedApplication"));
384
+ if (!app) return;
385
+ // Idempotent: an embedder that already installed a menu keeps it.
386
+ if (objc::msg_send<id>(app, objc::selector("mainMenu"))) return;
387
+
388
+ // NSEventModifierFlags. The default for a key equivalent is Command alone,
389
+ // so only the combinations need naming.
390
+ const NSUInteger kShift = 1UL << 17;
391
+ const NSUInteger kControl = 1UL << 18;
392
+ const NSUInteger kOption = 1UL << 19;
393
+ const NSUInteger kCommand = 1UL << 20;
394
+
395
+ auto str = [](const std::string &v) {
396
+ return cocoa::NSString_stringWithUTF8String(v);
397
+ };
398
+ auto alloc_init = [](const char *cls) {
399
+ return objc::msg_send<id>(
400
+ objc::msg_send<id>(objc::get_class(cls), objc::selector("alloc")),
401
+ objc::selector("init"));
402
+ };
403
+
404
+ // AppKit labels the first submenu from the bundle, not from this title, but
405
+ // "About <name>" and "Quit <name>" are ours to spell.
406
+ id process = objc::msg_send<id>(objc::get_class("NSProcessInfo"),
407
+ objc::selector("processInfo"));
408
+ const char *raw =
409
+ process ? objc::msg_send<const char *>(
410
+ objc::msg_send<id>(process, objc::selector("processName")),
411
+ objc::selector("UTF8String"))
412
+ : nullptr;
413
+ std::string name = raw ? raw : "App";
414
+
415
+ id menubar = alloc_init("NSMenu");
416
+
417
+ auto submenu = [&](const std::string &title) {
418
+ id holder = alloc_init("NSMenuItem");
419
+ id menu = objc::msg_send<id>(
420
+ objc::msg_send<id>(objc::get_class("NSMenu"), objc::selector("alloc")),
421
+ objc::selector("initWithTitle:"), str(title));
422
+ objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
423
+ objc::msg_send<void>(menubar, objc::selector("addItem:"), holder);
424
+ return menu;
425
+ };
426
+
427
+ auto item = [&](id menu, const std::string &title, const char *sel,
428
+ const std::string &key, NSUInteger mods) {
429
+ id it = objc::msg_send<id>(
430
+ objc::msg_send<id>(objc::get_class("NSMenuItem"),
431
+ objc::selector("alloc")),
432
+ objc::selector("initWithTitle:action:keyEquivalent:"), str(title),
433
+ sel ? objc::selector(sel) : nullptr, str(key));
434
+ if (mods) {
435
+ objc::msg_send<void>(
436
+ it, objc::selector("setKeyEquivalentModifierMask:"), mods);
437
+ }
438
+ objc::msg_send<void>(menu, objc::selector("addItem:"), it);
439
+ };
440
+ auto separator = [&](id menu) {
441
+ objc::msg_send<void>(menu, objc::selector("addItem:"),
442
+ objc::msg_send<id>(objc::get_class("NSMenuItem"),
443
+ objc::selector("separatorItem")));
444
+ };
445
+
446
+ id app_menu = submenu(name);
447
+ item(app_menu, "About " + name, "orderFrontStandardAboutPanel:", "", 0);
448
+ separator(app_menu);
449
+ item(app_menu, "Hide " + name, "hide:", "h", 0);
450
+ item(app_menu, "Hide Others", "hideOtherApplications:", "h",
451
+ kOption | kCommand);
452
+ item(app_menu, "Show All", "unhideAllApplications:", "", 0);
453
+ separator(app_menu);
454
+ // performClose:, not terminate: — measured, not assumed. Both quit and
455
+ // both exit 0, but terminate: exits the process itself, so the host never
456
+ // returns from wv_run and anything after app.run() is skipped;
457
+ // performClose: goes through the same window-close path the red button
458
+ // uses, the run loop unwinds, and the host prints its own "run returned 0".
459
+ // One shutdown path instead of two. muda picks terminate: because Tauri's
460
+ // app logic is native and multi-window; janela is single-window, so closing
461
+ // the window IS quitting. If janela ever grows multiple windows this has to
462
+ // become a real Quit that closes all of them.
463
+ //
464
+ // (An earlier note here claimed performClose: did not fire at all. That was
465
+ // a bad test: posted key events go to the FRONTMOST app, and the app was not
466
+ // active. With it activated first, both actions work.)
467
+ item(app_menu, "Quit " + name, "performClose:", "q", 0);
468
+
469
+ id edit = submenu("Edit");
470
+ item(edit, "Undo", "undo:", "z", 0);
471
+ item(edit, "Redo", "redo:", "z", kShift | kCommand);
472
+ separator(edit);
473
+ item(edit, "Cut", "cut:", "x", 0);
474
+ item(edit, "Copy", "copy:", "c", 0);
475
+ item(edit, "Paste", "paste:", "v", 0);
476
+ item(edit, "Select All", "selectAll:", "a", 0);
477
+
478
+ id view = submenu("View");
479
+ item(view, "Enter Full Screen", "toggleFullScreen:", "f",
480
+ kControl | kCommand);
481
+
482
+ id window = submenu("Window");
483
+ item(window, "Minimize", "performMiniaturize:", "m", 0);
484
+ item(window, "Close", "performClose:", "w", 0);
485
+
486
+ objc::msg_send<void>(app, objc::selector("setMainMenu:"), menubar);
487
+ // Lets AppKit add the standard Window-menu bookkeeping (window list,
488
+ // Bring All to Front).
489
+ objc::msg_send<void>(app, objc::selector("setWindowsMenu:"), window);
490
+ }
491
+
361
492
  bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
362
493
  std::string &error) {
363
494
  (void)error;
@@ -608,6 +739,14 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
608
739
 
609
740
  #endif
610
741
 
742
+ #if !defined(__APPLE__)
743
+ // Windows and Linux need no menu for these shortcuts: Alt+F4 is a
744
+ // window-manager message the win32 backend already answers as WM_CLOSE, and
745
+ // the editing keys are handled inside WebView2 and WebKitGTK. A menu here
746
+ // would be a feature, not a fix.
747
+ static void install_main_menu() {}
748
+ #endif
749
+
611
750
  // Runs on the UI thread with no TS frame beneath it — see the note on the job
612
751
  // pool above for why that matters.
613
752
  void dialog_on_ui_thread(webview_t, void *arg) {
@@ -728,6 +867,9 @@ int32_t wv_create(int32_t debug) {
728
867
  if (g_apps[i].used) continue;
729
868
  webview_t w = webview_create(debug, nullptr);
730
869
  if (!w) return -1;
870
+ // After webview_create, which is what brings NSApplication into being.
871
+ // A no-op off macOS.
872
+ install_main_menu();
731
873
  // Field-wise reset: App holds a thread and atomics, so it is not
732
874
  // copy-assignable from a temporary.
733
875
  g_apps[i].binds.clear();