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 +3 -0
- package/package.json +1 -1
- package/runtime/ios.ts +128 -0
- package/runtime/janela.ts +108 -32
- package/shim/wvshim.cc +1294 -235
- package/templates/main.ts +19 -0
- package/templates/react/files/src-host/main.ts +19 -0
- package/templates/solid/files/src-host/main.ts +19 -0
- package/templates/svelte/files/src-host/main.ts +19 -0
- package/templates/vue/files/src-host/main.ts +19 -0
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
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
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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 === "
|
|
191
|
-
mods = mods |
|
|
192
|
-
} else if (p === "
|
|
193
|
-
|
|
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 |
|
|
202
|
+
mods = mods | MOD_CTRL;
|
|
198
203
|
} else if (p === "alt" || p === "option") {
|
|
199
|
-
mods = mods |
|
|
204
|
+
mods = mods | MOD_ALT;
|
|
200
205
|
} else if (p === "shift") {
|
|
201
|
-
mods = mods |
|
|
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):
|
|
269
|
+
setEnabled(on: boolean): boolean {
|
|
264
270
|
this.enabled = on;
|
|
265
|
-
if (this.ownerHandle
|
|
266
|
-
|
|
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):
|
|
276
|
+
setLabel(label: string): boolean {
|
|
272
277
|
this.label = label;
|
|
273
|
-
if (this.ownerHandle
|
|
274
|
-
|
|
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):
|
|
299
|
+
/** Tick or untick the item. Returns as `setEnabled` does. */
|
|
300
|
+
setChecked(on: boolean): boolean {
|
|
297
301
|
this.checked = on;
|
|
298
|
-
if (this.ownerHandle
|
|
299
|
-
|
|
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
|
-
*
|
|
708
|
-
*
|
|
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
|