janela 0.17.1 → 0.18.0

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/bin/lib.mjs CHANGED
@@ -231,11 +231,20 @@ export function ffiManifest(shimLib, { platform = process.platform, macSdkPath =
231
231
  name: "wvOnMenu", symbol: "wv_on_menu",
232
232
  params: [
233
233
  "i32",
234
- { callback: { id: "menu", params: ["string", { context: "menu" }], returns: "i32", lifetime: "retained" } },
234
+ // The tag is the host's own registry index, so the click comes back as
235
+ // a number and TypeScript looks up the item's handler. Nothing is
236
+ // matched by string.
237
+ { callback: { id: "menu", params: ["i32", { context: "menu" }], returns: "i32", lifetime: "retained" } },
235
238
  { context: "menu" },
236
239
  ],
237
240
  returns: "i32",
238
241
  },
242
+ { name: "wvMenuSetEnabled", symbol: "wv_menu_set_enabled", params: ["i32", "i32", "i32"], returns: "i32" },
243
+ { name: "wvMenuSetChecked", symbol: "wv_menu_set_checked", params: ["i32", "i32", "i32"], returns: "i32" },
244
+ {
245
+ name: "wvMenuSetLabel", symbol: "wv_menu_set_label",
246
+ params: ["i32", "i32", "string"], returns: "i32",
247
+ },
239
248
  ];
240
249
 
241
250
  if (platform === "win32") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
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/runtime/janela.ts CHANGED
@@ -43,7 +43,10 @@ declare function wvDialog(
43
43
  ): number;
44
44
  declare function wvSetFullscreen(h: number, on: number): number;
45
45
  declare function wvSetMenu(h: number, spec: string): number;
46
- declare function wvOnMenu(h: number, cb: (id: string) => number): number;
46
+ declare function wvOnMenu(h: number, cb: (tag: number) => number): number;
47
+ declare function wvMenuSetEnabled(h: number, tag: number, on: number): number;
48
+ declare function wvMenuSetChecked(h: number, tag: number, on: number): number;
49
+ declare function wvMenuSetLabel(h: number, tag: number, label: string): number;
47
50
 
48
51
  const JOB_PENDING = 0;
49
52
  const JOB_OK = 1;
@@ -92,7 +95,6 @@ export type {
92
95
  DialogFilter,
93
96
  Events,
94
97
  FsCallback,
95
- MenuEntry,
96
98
  OpenDialogOptions,
97
99
  SaveDialogOptions,
98
100
  WindowConfig,
@@ -102,7 +104,6 @@ export type {
102
104
  // A project's `import { defineCommands } from "janela/host"` is rewritten to
103
105
  // this module by the CLI before scriptc sees it.
104
106
  export { defineCommands, defineEvents } from "./types";
105
- export { menuItem, menuSeparator, submenu } from "./types";
106
107
 
107
108
  import type {
108
109
  AsyncCommandHandler,
@@ -114,7 +115,6 @@ import type {
114
115
  DialogFilter,
115
116
  Events,
116
117
  FsCallback,
117
- MenuEntry,
118
118
  OpenDialogOptions,
119
119
  SaveDialogOptions,
120
120
  WindowConfig,
@@ -161,12 +161,18 @@ const TIMER_JOBS = -1;
161
161
  * interface (being signature-only) never is. A class receiver works even as a
162
162
  * plain function parameter, which is what `setup(app)` is.
163
163
  */
164
- // ---- menu flattening ---------------------------------------------------
164
+ // ---- menus -------------------------------------------------------------
165
165
  //
166
- // The shim renders menus but parses nothing structural: the tree is flattened
167
- // here into one row per line with 0x1f between fields, which needs only a
168
- // split on the native side. Keeping the parsing in TypeScript is the whole
169
- // point the native side stays a renderer.
166
+ // A menu item is an OBJECT that carries its own click handler, not a row with
167
+ // an id that something matches on later. muda Tauri's menu crate uses ids
168
+ // because Rust cannot attach a closure to an item across its global event
169
+ // channel, so it hands you `MenuId(String)` and you match on it; a typo there
170
+ // is silent. TypeScript has no such problem: the handler lives on the item, so
171
+ // there is no name to declare, to keep in sync, or to get wrong.
172
+ //
173
+ // The tag is an implementation detail the caller never sees: it indexes the
174
+ // handler registry here, is written into the wire format, comes back on a
175
+ // click, and is what setEnabled/setChecked/setLabel address.
170
176
 
171
177
  // NSEventModifierFlags.
172
178
  const MENU_SHIFT = 131072;
@@ -205,24 +211,116 @@ function menuSafe(v: string): string {
205
211
  return v.split("\x1f").join(" ").split("\n").join(" ");
206
212
  }
207
213
 
208
- function flattenMenu(entries: MenuEntry[], rows: string[]): void {
209
- for (let i = 0; i < entries.length; i++) {
210
- const e = entries[i];
211
- if (e.separator) {
212
- rows.push("-");
213
- continue;
214
+ /** Submenus and separators have nothing to run. */
215
+ function noop(): void {}
216
+
217
+ /**
218
+ * An entry in the application menu.
219
+ *
220
+ * Build one with `menuItem`, `menuSeparator` or `submenu` and keep the
221
+ * reference if you want to change it later — there is no lookup by name,
222
+ * because there are no names.
223
+ */
224
+ export class MenuItem {
225
+ label: string;
226
+ accel: string;
227
+ separator: boolean;
228
+ items: MenuItem[];
229
+ onClick: () => void;
230
+ enabled = true;
231
+ // Data on the base so the flattener can read it without a type test; only
232
+ // CheckMenuItem exposes a way to change it.
233
+ checkable = false;
234
+ checked = false;
235
+
236
+ // Assigned when the item is mounted by setMenu; -1 while detached. The
237
+ // window handle is enough to reach the FFI — holding the app itself would
238
+ // drag its two type parameters in here for no benefit.
239
+ tag = -1;
240
+ ownerHandle = -1;
241
+
242
+ constructor(
243
+ label: string,
244
+ accel: string,
245
+ separator: boolean,
246
+ items: MenuItem[],
247
+ onClick: () => void,
248
+ ) {
249
+ this.label = label;
250
+ this.accel = accel;
251
+ this.separator = separator;
252
+ this.items = items;
253
+ this.onClick = onClick;
254
+ }
255
+
256
+ /**
257
+ * Grey the item out, or bring it back.
258
+ *
259
+ * Applied immediately when the item is already in a menu, and remembered
260
+ * for the next `setMenu` when it is not — so state set before mounting is
261
+ * not lost.
262
+ */
263
+ setEnabled(on: boolean): void {
264
+ this.enabled = on;
265
+ if (this.ownerHandle >= 0 && this.tag >= 0) {
266
+ wvMenuSetEnabled(this.ownerHandle, this.tag, on ? 1 : 0);
267
+ }
268
+ }
269
+
270
+ /** Change the text without rebuilding the menu. */
271
+ setLabel(label: string): void {
272
+ this.label = label;
273
+ if (this.ownerHandle >= 0 && this.tag >= 0) {
274
+ wvMenuSetLabel(this.ownerHandle, this.tag, label);
214
275
  }
215
- const label = menuSafe(e.label);
216
- if (e.items.length > 0) {
217
- rows.push("S\x1f" + label);
218
- flattenMenu(e.items, rows);
219
- rows.push("E");
220
- continue;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * An item that can carry a tick.
281
+ *
282
+ * Separate from `MenuItem` because GTK decides this at construction: a tick
283
+ * needs `GtkCheckMenuItem`, a different widget, and an item cannot become one
284
+ * later. macOS and Windows would let any item show a check, but a method that
285
+ * works on two platforms and silently does nothing on the third is worse than
286
+ * one that is simply absent — so `setChecked` lives here, and calling it on a
287
+ * plain item is a compile error rather than a surprise on Linux.
288
+ */
289
+ export class CheckMenuItem extends MenuItem {
290
+ constructor(label: string, accel: string, onClick: () => void) {
291
+ super(label, accel, false, [], onClick);
292
+ this.checkable = true;
293
+ }
294
+
295
+ /** Tick or untick the item. */
296
+ setChecked(on: boolean): void {
297
+ this.checked = on;
298
+ if (this.ownerHandle >= 0 && this.tag >= 0) {
299
+ wvMenuSetChecked(this.ownerHandle, this.tag, on ? 1 : 0);
221
300
  }
222
- rows.push("I\x1f" + menuSafe(e.id) + "\x1f" + label + "\x1f" + parseAccel(e.accel));
223
301
  }
224
302
  }
225
303
 
304
+ /** A clickable entry. `accel` is "" for no shortcut. */
305
+ export function menuItem(label: string, accel: string, onClick: () => void): MenuItem {
306
+ return new MenuItem(label, accel, false, [], onClick);
307
+ }
308
+
309
+ /** A clickable entry that carries a tick. `accel` is "" for no shortcut. */
310
+ export function menuCheckItem(label: string, accel: string, onClick: () => void): CheckMenuItem {
311
+ return new CheckMenuItem(label, accel, onClick);
312
+ }
313
+
314
+ /** A divider. */
315
+ export function menuSeparator(): MenuItem {
316
+ return new MenuItem("", "", true, [], noop);
317
+ }
318
+
319
+ /** A submenu holding other entries. Nestable. */
320
+ export function submenu(label: string, items: MenuItem[]): MenuItem {
321
+ return new MenuItem(label, "", false, items, noop);
322
+ }
323
+
226
324
  export class JanelaAppImpl<
227
325
  C extends CommandShapes = CommandShapes,
228
326
  E = Record<string, unknown>,
@@ -231,6 +329,14 @@ export class JanelaAppImpl<
231
329
  names: string[] = [];
232
330
  handlers: CommandHandler[] = [];
233
331
 
332
+ // Menu items in tag order; the tag IS the index. Rebuilt by setMenu.
333
+ menuItems: MenuItem[] = [];
334
+ // The handlers, separately: scriptc does not support calling a closure held
335
+ // on an object property (SC1090), only one reached through an array — the
336
+ // same reason command handlers live in `handlers`.
337
+ menuHandlers: (() => void)[] = [];
338
+ menuBound = false;
339
+
234
340
  // ---- scheduling ----------------------------------------------------------
235
341
  // scriptc's event loop is parked for as long as the program sits inside the
236
342
  // wvRun() FFI call, so setTimeout/await never fire while the window is open.
@@ -584,34 +690,75 @@ export class JanelaAppImpl<
584
690
  * wholesale is what would. Calling it again replaces only what a previous
585
691
  * call added, so the menu can shrink as well as grow.
586
692
  *
587
- * Clicks arrive on `onMenu` with the entry's `id`.
693
+ * Each item carries its own handler; there are no ids to declare or match.
694
+ * Keep a reference to change an item later.
588
695
  *
589
696
  * ```ts
590
- * app.setMenu([
591
- * { label: "File", items: [
592
- * { label: "Open…", id: "open", accel: "CmdOrCtrl+O" },
593
- * { separator: true },
594
- * { label: "Close", id: "close" },
595
- * ]},
596
- * ]);
597
- * app.onMenu((id) => { if (id === "open") … });
697
+ * const save = menuItem("Save", "CmdOrCtrl+S", () => write());
698
+ * app.setMenu([submenu("File", [
699
+ * menuItem("Open…", "CmdOrCtrl+O", () => open()),
700
+ * menuSeparator(),
701
+ * save,
702
+ * ])]);
703
+ *
704
+ * save.setEnabled(false); // later, without rebuilding
598
705
  * ```
599
706
  *
600
707
  * Returns false where custom menus are not supported yet — everything but
601
708
  * macOS. The standard menu is unaffected either way.
602
709
  */
603
- setMenu(entries: MenuEntry[]): boolean {
710
+ setMenu(entries: MenuItem[]): boolean {
711
+ // A rebuild invalidates every tag from the previous call, so the registry
712
+ // is rebuilt with it. Items carried over keep working because they are
713
+ // re-tagged here; items dropped from the tree go inert, which is what
714
+ // "removed from the menu" should mean.
715
+ for (let i = 0; i < this.menuItems.length; i++) {
716
+ this.menuItems[i].tag = -1;
717
+ this.menuItems[i].ownerHandle = -1;
718
+ }
719
+ this.menuItems = [];
720
+ this.menuHandlers = [];
721
+
604
722
  const rows: string[] = [];
605
- flattenMenu(entries, rows);
723
+ this.flattenMenu(entries, rows);
724
+
725
+ if (!this.menuBound) {
726
+ // Registered once: the tag comes back and the item's own closure runs.
727
+ wvOnMenu(this.handle, (tag) => {
728
+ if (tag >= 0 && tag < this.menuHandlers.length) this.menuHandlers[tag]();
729
+ return 0;
730
+ });
731
+ this.menuBound = true;
732
+ }
606
733
  return wvSetMenu(this.handle, rows.join("\n")) === 0;
607
734
  }
608
735
 
609
- /** Called with the `id` of the clicked menu entry. */
610
- onMenu(cb: (id: string) => void): void {
611
- wvOnMenu(this.handle, (id) => {
612
- cb(id);
613
- return 0;
614
- });
736
+ /** Walks the tree, assigns each clickable item its registry tag. */
737
+ flattenMenu(entries: MenuItem[], rows: string[]): void {
738
+ for (let i = 0; i < entries.length; i++) {
739
+ const e = entries[i];
740
+ if (e.separator) {
741
+ rows.push("-");
742
+ continue;
743
+ }
744
+ const label = menuSafe(e.label);
745
+ if (e.items.length > 0) {
746
+ rows.push("S\x1f" + label);
747
+ this.flattenMenu(e.items, rows);
748
+ rows.push("E");
749
+ continue;
750
+ }
751
+ const tag = this.menuItems.length;
752
+ this.menuItems.push(e);
753
+ this.menuHandlers.push(e.onClick);
754
+ e.tag = tag;
755
+ e.ownerHandle = this.handle;
756
+ rows.push(
757
+ "I\x1f" + tag + "\x1f" + label + "\x1f" + parseAccel(e.accel) +
758
+ "\x1f" + (e.enabled ? "1" : "0") + "\x1f" + (e.checked ? "1" : "0") +
759
+ "\x1f" + (e.checkable ? "1" : "0"),
760
+ );
761
+ }
615
762
  }
616
763
 
617
764
  /**
package/runtime/types.ts CHANGED
@@ -41,56 +41,6 @@ export type AsyncCommandHandler = (
41
41
  */
42
42
  export type FsCallback = (err: string | null, text: string) => void;
43
43
 
44
- /**
45
- * One entry in the application menu.
46
- *
47
- * Every field is required rather than optional, and the entries are built with
48
- * `submenu`, `menuItem` and `menuSeparator` rather than written as literals.
49
- * That is a scriptc constraint made into a nicer API: a record whose fields are
50
- * all optional infers as `{ label: string | undefined, … }`, and an array
51
- * mixing a submenu, an item and a separator is a union that will not re-tag
52
- * into it ("union types must match exactly", SC2003). The helpers each return
53
- * the same total shape, so the array is homogeneous and the call site reads as
54
- * a tree.
55
- *
56
- * ```ts
57
- * app.setMenu([
58
- * submenu("File", [
59
- * menuItem("Open…", "open", "CmdOrCtrl+O"),
60
- * menuSeparator(),
61
- * ]),
62
- * ]);
63
- * ```
64
- */
65
- export type MenuEntry = {
66
- label: string;
67
- /** Sent to `onMenu` when clicked. Empty for submenus and separators. */
68
- id: string;
69
- /**
70
- * A shortcut like "CmdOrCtrl+O" or "CmdOrCtrl+Shift+S". Modifiers: Cmd,
71
- * Ctrl, CmdOrCtrl, Alt/Option, Shift. Empty for none.
72
- */
73
- accel: string;
74
- separator: boolean;
75
- /** Children. Empty for a leaf. */
76
- items: MenuEntry[];
77
- };
78
-
79
- /** A clickable entry. `accel` is "" for no shortcut. */
80
- export function menuItem(label: string, id: string, accel: string): MenuEntry {
81
- return { label: label, id: id, accel: accel, separator: false, items: [] };
82
- }
83
-
84
- /** A divider. */
85
- export function menuSeparator(): MenuEntry {
86
- return { label: "", id: "", accel: "", separator: true, items: [] };
87
- }
88
-
89
- /** A submenu holding other entries. Nestable. */
90
- export function submenu(label: string, items: MenuEntry[]): MenuEntry {
91
- return { label: label, id: "", accel: "", separator: false, items: items };
92
- }
93
- /** A named group of extensions offered in a dialog's file-type popup. */
94
44
  export interface DialogFilter {
95
45
  name: string;
96
46
  /** Bare extensions, no dot and no glob: ["png", "jpg"]. */
package/shim/wvshim.cc CHANGED
@@ -75,12 +75,20 @@ struct App {
75
75
  void *on_invoke_ctx = nullptr;
76
76
  void (*on_timer)(int32_t, void *) = nullptr;
77
77
  void *on_timer_ctx = nullptr;
78
- int32_t (*on_menu)(const uint8_t *, size_t, void *) = nullptr;
78
+ // The tag identifies the item; the host owns the numbering and hands it
79
+ // down, because the click handler lives in TypeScript now.
80
+ int32_t (*on_menu)(int32_t, void *) = nullptr;
79
81
  void *on_menu_ctx = nullptr;
80
82
 
81
- // Custom menu item ids, indexed by the NSMenuItem's tag. The menu bar is
82
- // process-global on macOS, so only one app can own it — see g_menu_owner.
83
- std::vector<std::string> menu_ids;
83
+ // Live NSMenuItems, indexed by tag, so setEnabled/setChecked/setLabel can
84
+ // reach one without rebuilding the bar. Retained; released on the next
85
+ // setMenu. The menu bar is process-global on macOS, so only one app can own
86
+ // it — see g_menu_owner.
87
+ //
88
+ // `void *` rather than `id`: this struct is compiled on every platform and
89
+ // `id` is Objective-C, so naming it here breaks the Linux and Windows
90
+ // builds. The Apple code casts on the way in and out.
91
+ std::vector<void *> menu_items;
84
92
  // How many submenus the standard bar installed, so setMenu appends after
85
93
  // them and can remove only its own on a later call.
86
94
  size_t std_menu_count = 0;
@@ -531,10 +539,7 @@ static int32_t g_menu_owner = -1;
531
539
  static void menu_clicked(long tag) {
532
540
  App *a = g_menu_owner >= 0 ? app_at(g_menu_owner) : nullptr;
533
541
  if (!a || !a->on_menu) return;
534
- if (tag < 0 || static_cast<size_t>(tag) >= a->menu_ids.size()) return;
535
- const std::string &id = a->menu_ids[static_cast<size_t>(tag)];
536
- a->on_menu(reinterpret_cast<const uint8_t *>(id.data()), id.size(),
537
- a->on_menu_ctx);
542
+ a->on_menu(static_cast<int32_t>(tag), a->on_menu_ctx);
538
543
  }
539
544
 
540
545
  // One shared target for every custom item; the tag says which one fired.
@@ -563,6 +568,21 @@ static id menu_target() {
563
568
  return instance;
564
569
  }
565
570
 
571
+ // Retains the item under its tag so a later setEnabled/setChecked/setLabel can
572
+ // find it. The vector is sized to fit rather than pushed, because the host's
573
+ // tags are registry indices and need not arrive in order.
574
+ static void remember_item(App *a, long tag, id item) {
575
+ if (tag < 0) return;
576
+ size_t at = static_cast<size_t>(tag);
577
+ if (a->menu_items.size() <= at) a->menu_items.resize(at + 1, nullptr);
578
+ a->menu_items[at] = webview::detail::objc::retain(item);
579
+ }
580
+
581
+ static id item_at(App *a, int32_t tag) {
582
+ if (tag < 0 || static_cast<size_t>(tag) >= a->menu_items.size()) return nullptr;
583
+ return static_cast<id>(a->menu_items[static_cast<size_t>(tag)]);
584
+ }
585
+
566
586
  // Appends the host's submenus to the standard menu bar rather than replacing
567
587
  // it: a custom menu must not be able to cost the app Cmd+Q and Cmd+V, which is
568
588
  // what replacing the bar wholesale would do.
@@ -583,7 +603,10 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
583
603
  objc::msg_send<void>(menubar, objc::selector("removeItemAtIndex:"),
584
604
  static_cast<NSInteger>(n - 1));
585
605
  }
586
- a->menu_ids.clear();
606
+ for (void *old_item : a->menu_items) {
607
+ if (old_item) objc::release(static_cast<id>(old_item));
608
+ }
609
+ a->menu_items.clear();
587
610
 
588
611
  std::vector<id> stack;
589
612
  stack.push_back(menubar);
@@ -605,6 +628,10 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
605
628
  cocoa::NSString_stringWithUTF8String(f[1]));
606
629
  objc::msg_send<void>(holder, objc::selector("setTitle:"),
607
630
  cocoa::NSString_stringWithUTF8String(f[1]));
631
+ // Without this AppKit decides each item's enabled state from the
632
+ // responder chain and setEnabled: is silently ignored — the item stays
633
+ // live however many times the host disables it.
634
+ objc::msg_send<void>(menu, objc::selector("setAutoenablesItems:"), false);
608
635
  objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
609
636
  objc::msg_send<void>(stack.back(), objc::selector("addItem:"), holder);
610
637
  stack.push_back(menu);
@@ -617,9 +644,11 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
617
644
  objc::msg_send<id>(objc::get_class("NSMenuItem"),
618
645
  objc::selector("separatorItem")));
619
646
  }
620
- } else if (kind == "I" && f.size() >= 5 && stack.size() > 1) {
621
- a->menu_ids.push_back(f[1]);
622
- long tag = static_cast<long>(a->menu_ids.size()) - 1;
647
+ } else if (kind == "I" && f.size() >= 8 && stack.size() > 1) {
648
+ // The host owns the tag: it indexes the handler registry in TypeScript,
649
+ // so the numbering has to come from there rather than from insertion
650
+ // order here.
651
+ long tag = strtol(f[1].c_str(), nullptr, 10);
623
652
  id it = objc::msg_send<id>(
624
653
  objc::msg_send<id>(objc::get_class("NSMenuItem"),
625
654
  objc::selector("alloc")),
@@ -635,9 +664,17 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
635
664
  NSUInteger mods = static_cast<NSUInteger>(strtoul(f[4].c_str(), nullptr, 10));
636
665
  objc::msg_send<void>(it, objc::selector("setKeyEquivalentModifierMask:"),
637
666
  mods);
667
+ objc::msg_send<void>(it, objc::selector("setEnabled:"), f[5] == "1");
668
+ // f[7] says the item is checkable. AppKit lets any item carry a state,
669
+ // so it is unused here — it is on the wire for the GTK renderer, which
670
+ // has to choose GtkCheckMenuItem at construction.
671
+ objc::msg_send<void>(it, objc::selector("setState:"),
672
+ static_cast<NSInteger>(f[6] == "1" ? 1 : 0));
638
673
  objc::msg_send<void>(stack.back(), objc::selector("addItem:"), it);
674
+ remember_item(a, tag, it);
639
675
  }
640
676
  }
677
+
641
678
  g_menu_owner = -1;
642
679
  for (int32_t i = 0; i < 8; i++) {
643
680
  if (app_at(i) == a) { g_menu_owner = i; break; }
@@ -1044,7 +1081,7 @@ int32_t wv_create(int32_t debug) {
1044
1081
  g_apps[i].on_timer_ctx = nullptr;
1045
1082
  g_apps[i].on_menu = nullptr;
1046
1083
  g_apps[i].on_menu_ctx = nullptr;
1047
- g_apps[i].menu_ids.clear();
1084
+ g_apps[i].menu_items.clear();
1048
1085
  g_apps[i].req.clear();
1049
1086
  g_apps[i].cur_id.clear();
1050
1087
  g_apps[i].reply.clear();
@@ -1358,9 +1395,7 @@ int32_t wv_set_menu(int32_t h, const uint8_t *p, size_t n) {
1358
1395
  }
1359
1396
 
1360
1397
  // Retained, like wv_on_invoke: registered once and valid until the app exits.
1361
- int32_t wv_on_menu(int32_t h,
1362
- int32_t (*cb)(const uint8_t *, size_t, void *),
1363
- void *ctx) {
1398
+ int32_t wv_on_menu(int32_t h, int32_t (*cb)(int32_t, void *), void *ctx) {
1364
1399
  App *a = app_at(h);
1365
1400
  if (!a) return -1;
1366
1401
  a->on_menu = cb;
@@ -1368,6 +1403,60 @@ int32_t wv_on_menu(int32_t h,
1368
1403
  return 0;
1369
1404
  }
1370
1405
 
1406
+ // Change one live item without rebuilding the bar. The tag is the host's
1407
+ // registry index, handed down by wv_set_menu. -1 for an unknown tag rather
1408
+ // than silence, so a stale handle is visible instead of a no-op.
1409
+ int32_t wv_menu_set_enabled(int32_t h, int32_t tag, int32_t on) {
1410
+ App *a = app_at(h);
1411
+ if (!a) return -1;
1412
+ #if defined(__APPLE__)
1413
+ using namespace webview::detail;
1414
+ objc::autoreleasepool arp;
1415
+ id it = item_at(a, tag);
1416
+ if (!it) return -1;
1417
+ objc::msg_send<void>(it, objc::selector("setEnabled:"), on != 0);
1418
+ return 0;
1419
+ #else
1420
+ (void)tag; (void)on;
1421
+ return -1;
1422
+ #endif
1423
+ }
1424
+
1425
+ int32_t wv_menu_set_checked(int32_t h, int32_t tag, int32_t on) {
1426
+ App *a = app_at(h);
1427
+ if (!a) return -1;
1428
+ #if defined(__APPLE__)
1429
+ using namespace webview::detail;
1430
+ objc::autoreleasepool arp;
1431
+ id it = item_at(a, tag);
1432
+ if (!it) return -1;
1433
+ // NSControlStateValueOn / Off.
1434
+ objc::msg_send<void>(it, objc::selector("setState:"),
1435
+ static_cast<NSInteger>(on != 0 ? 1 : 0));
1436
+ return 0;
1437
+ #else
1438
+ (void)tag; (void)on;
1439
+ return -1;
1440
+ #endif
1441
+ }
1442
+
1443
+ int32_t wv_menu_set_label(int32_t h, int32_t tag, const uint8_t *p, size_t n) {
1444
+ App *a = app_at(h);
1445
+ if (!a) return -1;
1446
+ #if defined(__APPLE__)
1447
+ using namespace webview::detail;
1448
+ objc::autoreleasepool arp;
1449
+ id it = item_at(a, tag);
1450
+ if (!it) return -1;
1451
+ objc::msg_send<void>(it, objc::selector("setTitle:"),
1452
+ cocoa::NSString_stringWithUTF8String(to_str(p, n)));
1453
+ return 0;
1454
+ #else
1455
+ (void)tag; (void)p; (void)n;
1456
+ return -1;
1457
+ #endif
1458
+ }
1459
+
1371
1460
  int32_t wv_set_fullscreen(int32_t h, int32_t on) {
1372
1461
  App *a = app_at(h);
1373
1462
  if (!a) return -1;