Skip to content

Menu schema

A menu schema describes a menu and each of its fields. Inkweaver uses it to build the menu’s interface, and to store and act on the values the reader chooses. For a walkthrough, see Menus and input.

MenuSchema describes a menu.

interface MenuSchema {
fields: Array<MenuField>;
labels?: Array<string>;
layer?: string;
name: MenuName;
scope?: Scope;
type: string;
}
FieldDescription
fieldsThe menu’s fields, in the order they’re shown.
labelsLabels that choose between variations in the menu’s artwork, the same way labels choose a scene’s variations.
layerA selector for the layer of the menu’s artwork that the interface is drawn over.
nameThe menu’s name.
scopeA scope to push when the menu opens, and release when it closes.
typeThe menu’s type.

If a menu has no artwork of its own, Inkweaver draws it on the artwork of the menu beneath it on the stack, along with the labels of every menu drawn on that artwork. That means an artist can make one PSD for every menu, and give each menu its own look with variation sets.

  • A menu’s labels replace any labels beneath it in the same variation set.
  • Labels that aren’t in any variation set don’t carry up to the next menu.
  • A variation set that no label chooses shows its first variation.

Inkweaver matches layer against the layers in the menu’s artwork, and draws the interface over the bounds of the layer it finds. Layer names match as classes, so a layer named UI matches .ui.

The layer has to be visible to be measured. To keep it from showing, leave it empty or set its opacity to zero. Don’t hide it.

name is keyof Menus. To declare a new menu, add its name to the Menus interface, then return a schema for that name from the buildMenuSchema command.

Example
declare module "@inkweaverdev/inkweaver-sdk" {
interface Menus {
journal: true;
}
}
decorateCommand("buildMenuSchema", async (context, next) => {
const result = await next(context);
switch (context.name) {
case "journal": {
return {
schema: {
...result.schema,
fields: [...result.schema.fields, journalOpacityField],
},
};
}
default: {
return result;
}
}
});

Every field in fields has these properties.

name identifies the field, and it must be unique within the menu.

name: string;

type is the field’s type. It decides how the field is shown and which other properties it has.

type: "bookmark" | "boolean" | "button" | "enum" | "number" | "separator" | "string";

locales holds the field’s text, with one version per locale. Inkweaver shows the version for the reader’s text language. If there isn’t one, it uses any version the field has, and then the field’s name.

locales: Record<string, string>;
Example
locales: {
"en-US": "Show puzzle hints",
"ja-JP": "パズルのヒントを表示",
}

boolean, number, string, and enum fields have a value function and a setValue function. The menu calls value after every change to state, settings, or view state, and shows the result right away. Keep value fast, and only read from state, settings, or view state.

A button field shows a button that runs an action.

{
name: string;
type: "button";
locales: Record<string, string>;
action: () => Promise<void>;
}

A bookmark field is a save slot that shows a bookmark and runs an action.

{
name: string;
type: "bookmark";
locales: Record<string, string>;
bookmark: null | Bookmark;
action: () => Promise<void>;
}
PropertyDescription
bookmarkThe bookmark in the slot, or null for an empty slot.
actionRuns when the reader chooses the slot.
Example
{
name: "slot-1",
type: "bookmark",
locales: { "en-US": "Slot 1" },
bookmark: bookmarks.find(({ id }) => id === "slot-1") ?? null,
action: () => saveToSlot("slot-1"),
}

A separator field is a visual break between groups of fields.

{
name: string;
type: "separator";
locales: Record<string, string>;
}

A boolean field shows a toggle.

{
name: string;
type: "boolean";
locales: Record<string, string>;
value: () => boolean;
setValue: (newValue: boolean) => void;
}

A number field shows a slider.

{
name: string;
type: "number";
locales: Record<string, string>;
minimum?: number;
maximum?: number;
step?: number;
value: () => number;
setValue: (newValue: number) => void;
}
Example
{
name: "journalOpacity",
type: "number",
minimum: 0.2,
maximum: 1,
step: 0.05,
locales: { "en-US": "Background opacity" },
value: () => getSettings().journalOpacity ?? 0.9,
setValue: (newValue) => updateSettings({ journalOpacity: newValue }),
}

A string field shows a text box.

{
name: string;
type: "string";
locales: Record<string, string>;
value: () => string;
setValue: (newValue: string) => void;
}

An enum field shows a drop-down list. Each option has a value, and its locales hold the text the reader sees.

{
name: string;
type: "enum";
locales: Record<string, string>;
options: Array<{
value: string;
locales: Record<string, string>;
}>;
value: () => string;
setValue: (newValue: string) => void;
}
Example
{
name: "textSpeed",
type: "enum",
locales: { "en-US": "Text speed" },
options: [
{ value: "slow", locales: { "en-US": "Slow" } },
{ value: "normal", locales: { "en-US": "Normal" } },
{ value: "instant", locales: { "en-US": "Instant" } },
],
value: () => getSettings().textSpeed ?? "normal",
setValue: (newValue) => updateSettings({ textSpeed: newValue }),
}

A binding’s inputs are strings. An input is one of:

  • One or more keys joined with +. The main key is a lowercase KeyboardEvent.key value, and the space bar is space. Modifiers can come in any order.
  • click, for a mouse click or a tap.
  • A gamepad button.

Write modifiers in alt, control, meta, shift order, then the key, joined with +.

Clicking an on-screen control, like a link or button, uses that control and doesn’t trigger any binding.

Examples
"escape";
"meta + l";
"shift + arrowright";
"click";

A gamepad input is written as gamepad:<index>, where <index> is the button’s number in the standard gamepad layout. Buttons are numbered by position, so gamepad:0 is the bottom face button on every controller: A on Xbox, Cross on PlayStation, and B on Nintendo.

A gamepad binding runs once per press, so holding the button doesn’t repeat it. Every connected controller works.

Examples
"gamepad:0"; // bottom face button
"gamepad:5"; // right bumper: RB, R1, or R
"gamepad:9"; // start: Menu, Options, or Plus