janela 0.18.1 → 0.19.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
@@ -239,6 +239,9 @@ export function ffiManifest(shimLib, { platform = process.platform, macSdkPath =
239
239
  ],
240
240
  returns: "i32",
241
241
  },
242
+ // No handle: the responder chain and the key window are process state,
243
+ // not per-window, so this is the one call that takes only its argument.
244
+ { name: "wvPerformAction", symbol: "wv_perform_action", params: ["string"], returns: "i32" },
242
245
  { name: "wvMenuSetEnabled", symbol: "wv_menu_set_enabled", params: ["i32", "i32", "i32"], returns: "i32" },
243
246
  { name: "wvMenuSetChecked", symbol: "wv_menu_set_checked", params: ["i32", "i32", "i32"], returns: "i32" },
244
247
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.18.1",
3
+ "version": "0.19.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/ios.ts CHANGED
@@ -102,6 +102,121 @@ function pending(api: string, why: string): string {
102
102
  * scheduled by the shell, the library re-entered when they fire) is the whole
103
103
  * of that change on this side.
104
104
  */
105
+ // ---- menus ---------------------------------------------------------------
106
+ //
107
+ // A mirror of the desktop lane's menu builders, so a project's src-host can
108
+ // declare a menu and still compile for a phone. Nothing renders here — iOS
109
+ // has no menu bar — but the objects are real, so handlers, `setEnabled` and
110
+ // the rest are ordinary no-ops rather than missing symbols.
111
+ //
112
+ // Duplicated rather than shared because the CLI copies ONE of these two files
113
+ // in as `./janela`: a shared module would have to inject the FFI the desktop
114
+ // setters call, which is more machinery than the forty lines it would save.
115
+
116
+ /** Submenus and separators have nothing to run. */
117
+ function menuNoop(): void {}
118
+
119
+ /** An entry in the application menu. See the desktop lane for what it does. */
120
+ export class MenuItem {
121
+ label: string;
122
+ accel: string;
123
+ separator: boolean;
124
+ items: MenuItem[];
125
+ onClick: () => void;
126
+ enabled = true;
127
+ checkable = false;
128
+ checked = false;
129
+ tag = -1;
130
+ ownerHandle = -1;
131
+
132
+ constructor(
133
+ label: string,
134
+ accel: string,
135
+ separator: boolean,
136
+ items: MenuItem[],
137
+ onClick: () => void,
138
+ ) {
139
+ this.label = label;
140
+ this.accel = accel;
141
+ this.separator = separator;
142
+ this.items = items;
143
+ this.onClick = onClick;
144
+ }
145
+
146
+ /** Records the state. False: there is no live menu here to change. */
147
+ setEnabled(on: boolean): boolean {
148
+ this.enabled = on;
149
+ return false;
150
+ }
151
+
152
+ /** Records the state. False: there is no live menu here to change. */
153
+ setLabel(label: string): boolean {
154
+ this.label = label;
155
+ return false;
156
+ }
157
+ }
158
+
159
+ /** An item that can carry a tick. */
160
+ export class CheckMenuItem extends MenuItem {
161
+ constructor(label: string, accel: string, onClick: () => void) {
162
+ super(label, accel, false, [], onClick);
163
+ this.checkable = true;
164
+ }
165
+
166
+ /** Records the state. False: there is no live menu here to change. */
167
+ setChecked(on: boolean): boolean {
168
+ this.checked = on;
169
+ return false;
170
+ }
171
+ }
172
+
173
+ /** A clickable entry. `accel` is "" for no shortcut. */
174
+ export function menuItem(label: string, accel: string, onClick: () => void): MenuItem {
175
+ return new MenuItem(label, accel, false, [], onClick);
176
+ }
177
+
178
+ /** A clickable entry that carries a tick. */
179
+ export function menuCheckItem(label: string, accel: string, onClick: () => void): CheckMenuItem {
180
+ return new CheckMenuItem(label, accel, onClick);
181
+ }
182
+
183
+ /** A divider. */
184
+ export function menuSeparator(): MenuItem {
185
+ return new MenuItem("", "", true, [], menuNoop);
186
+ }
187
+
188
+ /** A submenu holding other entries. Nestable. */
189
+ export function submenu(label: string, items: MenuItem[]): MenuItem {
190
+ return new MenuItem(label, "", false, items, menuNoop);
191
+ }
192
+
193
+ /**
194
+ * The platform's own actions.
195
+ *
196
+ * Every one returns false here. On desktop these are AppKit selectors, Win32
197
+ * messages and WebKitGTK editing commands; a phone has none of them, and the
198
+ * editing gestures it does have belong to UIKit and need no menu to reach.
199
+ */
200
+ export const predefined = {
201
+ about: (): boolean => false,
202
+ hide: (): boolean => false,
203
+ hideOthers: (): boolean => false,
204
+ showAll: (): boolean => false,
205
+ quit: (): boolean => false,
206
+ close: (): boolean => false,
207
+
208
+ undo: (): boolean => false,
209
+ redo: (): boolean => false,
210
+ cut: (): boolean => false,
211
+ copy: (): boolean => false,
212
+ paste: (): boolean => false,
213
+ selectAll: (): boolean => false,
214
+
215
+ minimize: (): boolean => false,
216
+ zoom: (): boolean => false,
217
+ fullscreen: (): boolean => false,
218
+ };
219
+
105
220
  /**
106
221
  * A running janela app on iOS, typed by the contract it serves.
107
222
  *
@@ -462,6 +577,19 @@ export class JanelaAppImpl<
462
577
  console.error(pending("quit", "iOS apps are dismissed by the user"));
463
578
  }
464
579
 
580
+ /**
581
+ * @remarks Returns false on iOS: a phone has no menu bar to put this in.
582
+ *
583
+ * Present so a project's `src-host/main.ts` compiles on both lanes — the
584
+ * whole reason this file mirrors the desktop one. The items are still built,
585
+ * so their handlers and any later `setEnabled` are ordinary no-ops rather
586
+ * than crashes; what is absent is somewhere to render them.
587
+ */
588
+ setMenu(_entries: MenuItem[]): boolean {
589
+ console.error(pending("setMenu", "a phone has no menu bar"));
590
+ return false;
591
+ }
592
+
465
593
  /**
466
594
  * Present for source compatibility with the desktop lane; UIKit owns the run
467
595
  * loop here, so the shell shows the page rather than this.
package/runtime/janela.ts CHANGED
@@ -44,6 +44,7 @@ declare function wvDialog(
44
44
  declare function wvSetFullscreen(h: number, on: number): number;
45
45
  declare function wvSetMenu(h: number, spec: string): number;
46
46
  declare function wvOnMenu(h: number, cb: (tag: number) => number): number;
47
+ declare function wvPerformAction(action: string): number;
47
48
  declare function wvMenuSetEnabled(h: number, tag: number, on: number): number;
48
49
  declare function wvMenuSetChecked(h: number, tag: number, on: number): number;
49
50
  declare function wvMenuSetLabel(h: number, tag: number, label: string): number;
@@ -174,31 +175,35 @@ const TIMER_JOBS = -1;
174
175
  // handler registry here, is written into the wire format, comes back on a
175
176
  // click, and is what setEnabled/setChecked/setLabel address.
176
177
 
177
- // NSEventModifierFlags.
178
- const MENU_SHIFT = 131072;
179
- const MENU_CONTROL = 262144;
180
- const MENU_OPTION = 524288;
181
- const MENU_COMMAND = 1048576;
182
-
183
- /** "CmdOrCtrl+Shift+O" -> "o<US>1179648". Unknown words are taken as the key. */
178
+ // Modifiers travel SYMBOLICALLY, not as one platform's constants.
179
+ //
180
+ // They used to be NSEventModifierFlags integers, which only worked because
181
+ // macOS was the only renderer. Naming the intent instead lets each platform
182
+ // map it — and it is the only way "CmdOrCtrl" can mean what it says, since the
183
+ // runtime does not know which platform it was built for.
184
+ const MOD_PRIMARY = 1; // CmdOrCtrl: Command on macOS, Control elsewhere
185
+ const MOD_SHIFT = 2;
186
+ const MOD_ALT = 4; // Option on macOS
187
+ const MOD_CTRL = 8; // Control, explicitly, on every platform
188
+ const MOD_CMD = 16; // Command, explicitly; ignored where there is none
189
+
190
+ /** "CmdOrCtrl+Shift+O" -> "o<US>3". Unknown words are taken as the key. */
184
191
  function parseAccel(accel: string): string {
185
192
  const parts = accel.split("+");
186
193
  let mods = 0;
187
194
  let key = "";
188
195
  for (let i = 0; i < parts.length; i++) {
189
196
  const p = parts[i].toLowerCase();
190
- if (p === "cmd" || p === "command" || p === "meta" || p === "super") {
191
- mods = mods | MENU_COMMAND;
192
- } else if (p === "cmdorctrl" || p === "commandorcontrol") {
193
- // macOS is the only platform with custom menus so far, so this is
194
- // Command here; the Windows and Linux renderers will read it as Control.
195
- mods = mods | MENU_COMMAND;
197
+ if (p === "cmdorctrl" || p === "commandorcontrol") {
198
+ mods = mods | MOD_PRIMARY;
199
+ } else if (p === "cmd" || p === "command" || p === "meta" || p === "super") {
200
+ mods = mods | MOD_CMD;
196
201
  } else if (p === "ctrl" || p === "control") {
197
- mods = mods | MENU_CONTROL;
202
+ mods = mods | MOD_CTRL;
198
203
  } else if (p === "alt" || p === "option") {
199
- mods = mods | MENU_OPTION;
204
+ mods = mods | MOD_ALT;
200
205
  } else if (p === "shift") {
201
- mods = mods | MENU_SHIFT;
206
+ mods = mods | MOD_SHIFT;
202
207
  } else if (p !== "") {
203
208
  key = p;
204
209
  }
@@ -258,21 +263,20 @@ export class MenuItem {
258
263
  *
259
264
  * Applied immediately when the item is already in a menu, and remembered
260
265
  * for the next `setMenu` when it is not — so state set before mounting is
261
- * not lost.
266
+ * not lost. The return says which of the two happened: true when the live
267
+ * menu was changed, false when the value was only recorded.
262
268
  */
263
- setEnabled(on: boolean): void {
269
+ setEnabled(on: boolean): boolean {
264
270
  this.enabled = on;
265
- if (this.ownerHandle >= 0 && this.tag >= 0) {
266
- wvMenuSetEnabled(this.ownerHandle, this.tag, on ? 1 : 0);
267
- }
271
+ if (this.ownerHandle < 0 || this.tag < 0) return false;
272
+ return wvMenuSetEnabled(this.ownerHandle, this.tag, on ? 1 : 0) === 0;
268
273
  }
269
274
 
270
275
  /** Change the text without rebuilding the menu. */
271
- setLabel(label: string): void {
276
+ setLabel(label: string): boolean {
272
277
  this.label = label;
273
- if (this.ownerHandle >= 0 && this.tag >= 0) {
274
- wvMenuSetLabel(this.ownerHandle, this.tag, label);
275
- }
278
+ if (this.ownerHandle < 0 || this.tag < 0) return false;
279
+ return wvMenuSetLabel(this.ownerHandle, this.tag, label) === 0;
276
280
  }
277
281
  }
278
282
 
@@ -292,12 +296,11 @@ export class CheckMenuItem extends MenuItem {
292
296
  this.checkable = true;
293
297
  }
294
298
 
295
- /** Tick or untick the item. */
296
- setChecked(on: boolean): void {
299
+ /** Tick or untick the item. Returns as `setEnabled` does. */
300
+ setChecked(on: boolean): boolean {
297
301
  this.checked = on;
298
- if (this.ownerHandle >= 0 && this.tag >= 0) {
299
- wvMenuSetChecked(this.ownerHandle, this.tag, on ? 1 : 0);
300
- }
302
+ if (this.ownerHandle < 0 || this.tag < 0) return false;
303
+ return wvMenuSetChecked(this.ownerHandle, this.tag, on ? 1 : 0) === 0;
301
304
  }
302
305
  }
303
306
 
@@ -306,6 +309,70 @@ export function menuItem(label: string, accel: string, onClick: () => void): Men
306
309
  return new MenuItem(label, accel, false, [], onClick);
307
310
  }
308
311
 
312
+ /**
313
+ * The platform's own actions, callable from anywhere.
314
+ *
315
+ * ```ts
316
+ * const close = menuItem("Close", "CmdOrCtrl+W", () => predefined.close());
317
+ * const copy = menuItem("Copy", "CmdOrCtrl+C", () => predefined.copy());
318
+ *
319
+ * // and they compose, which a fixed "predefined item" never could:
320
+ * menuItem("Save and close", "", () => { write(); predefined.close(); });
321
+ * ```
322
+ *
323
+ * Copy, paste and undo are the interesting ones: they are not "do this" but a
324
+ * selector sent up the responder chain, and by the time a handler here runs
325
+ * the menu click has already been delivered to us. So these ask the platform
326
+ * to send the action up the chain at that moment, which is what lets it reach
327
+ * the webview's editing context. `document.execCommand("paste")` cannot —
328
+ * webviews block it.
329
+ *
330
+ * An action with no equivalent on a platform returns false rather than
331
+ * pretending. What exists where:
332
+ *
333
+ * - everywhere: `quit`, `close`, `minimize`, `zoom`, `fullscreen`, `about`,
334
+ * `undo`, `redo`, `cut`, `copy`, `paste`, `selectAll`
335
+ * - macOS only: `hide`, `hideOthers`, `showAll`
336
+ *
337
+ * The three that stop at macOS are not a missing API but a missing concept.
338
+ * `hide` is application state there — windows vanish, the app stays in the
339
+ * Dock, one click brings it back. Hiding a window on Windows or Linux instead
340
+ * takes it out of the taskbar with no way back short of a tray icon, and
341
+ * `hideOthers` / `showAll` reach into other applications: the nearest Windows
342
+ * call minimizes you too, and on Wayland there is nothing at all.
343
+ *
344
+ * `about` shows each platform's own About box — NSApplication's panel,
345
+ * `ShellAboutW`, `gtk_show_about_dialog` — named after the process, which is
346
+ * what the macOS application menu shows. A richer one is your app's to build.
347
+ *
348
+ * Each platform reaches the editing commands by its own route: AppKit
349
+ * selectors up the responder chain, WebKitGTK's
350
+ * `webkit_web_view_execute_editing_command`, and on Windows the DevTools
351
+ * protocol, since WebView2 runs the page out of process and exposes no
352
+ * copy/paste entry point of its own. On Windows and Linux the call is
353
+ * dispatched rather than awaited, so a `true` there means the browser was
354
+ * asked, not that it has finished.
355
+ */
356
+ export const predefined = {
357
+ about: (): boolean => wvPerformAction("about") === 0,
358
+ hide: (): boolean => wvPerformAction("hide") === 0,
359
+ hideOthers: (): boolean => wvPerformAction("hideOthers") === 0,
360
+ showAll: (): boolean => wvPerformAction("showAll") === 0,
361
+ quit: (): boolean => wvPerformAction("quit") === 0,
362
+ close: (): boolean => wvPerformAction("closeWindow") === 0,
363
+
364
+ undo: (): boolean => wvPerformAction("undo") === 0,
365
+ redo: (): boolean => wvPerformAction("redo") === 0,
366
+ cut: (): boolean => wvPerformAction("cut") === 0,
367
+ copy: (): boolean => wvPerformAction("copy") === 0,
368
+ paste: (): boolean => wvPerformAction("paste") === 0,
369
+ selectAll: (): boolean => wvPerformAction("selectAll") === 0,
370
+
371
+ minimize: (): boolean => wvPerformAction("minimize") === 0,
372
+ zoom: (): boolean => wvPerformAction("zoom") === 0,
373
+ fullscreen: (): boolean => wvPerformAction("fullscreen") === 0,
374
+ };
375
+
309
376
  /** A clickable entry that carries a tick. `accel` is "" for no shortcut. */
310
377
  export function menuCheckItem(label: string, accel: string, onClick: () => void): CheckMenuItem {
311
378
  return new CheckMenuItem(label, accel, onClick);
@@ -704,8 +771,17 @@ export class JanelaAppImpl<
704
771
  * save.setEnabled(false); // later, without rebuilding
705
772
  * ```
706
773
  *
707
- * Returns false where custom menus are not supported yet — everything but
708
- * macOS. The standard menu is unaffected either way.
774
+ * Renders on all three desktop platforms: an NSMenu bar, a Win32 menu, or a
775
+ * GtkMenuBar above the webview. Returns false if the platform refused —
776
+ * GTK 4, where GtkMenuBar no longer exists, is the one case that does.
777
+ *
778
+ * What differs is the FLOOR beneath it. macOS gets a standard bar whether
779
+ * you set one or not, because there a Command shortcut is a menu key
780
+ * equivalent and an app with no menu has no Cmd+Q or Cmd+V. Windows and
781
+ * Linux need no such floor: Alt+F4 is the window manager's, and the editing
782
+ * keys belong to WebView2 and WebKitGTK. So an app that never calls this
783
+ * has a full menu bar on macOS and none elsewhere — which is what an app
784
+ * with nothing in its menus should look like on each.
709
785
  */
710
786
  setMenu(entries: MenuItem[]): boolean {
711
787
  // A rebuild invalidates every tag from the previous call, so the registry