api.ui
UiApi
Everything a plugin puts on the screen: the status line, toasts, dialogs, floating
panels, confirm / alert / prompt / progress, a map tool that owns the pointer,
a passive overlay drawn over the map, and picking an area or a tile.
- openDialogs
- status
- statusText
- toast
- saveFile
- dialog
- panel
- fileType
- mapView
- statusItem
- mapButton
- dialogSlot
- preferencesPage
- mapTool
- overlay
- pickFiles
- pickArea
- pickTile
- pickObject
- loadImage
- readClipboardImage
- confirm
- alert
- prompt
- progress
- tooltip
- el
- widgets
- open
- ask
- repaint
Members
openDialogs
openDialogs(): string[];
The dialogs open now, bottom to top, by id: the built-in ones by their DialogId, a
plugin's as "pluginDialog". What a shared map shows others ("in Player Settings").
toast
toast(toast: { kind?: Toast["kind"]; title: string; detail?: string; ttl?: number; }): void;
A short notice over the map that leaves by itself — how Save reports; ttl 0 keeps it until dismissed.
api.ui.toast({ kind: "ok", title: "Export finished", detail: "12 triggers written." });Try itsaveFile
saveFile(data: Blob | Uint8Array, fileName: string): Promise<{ route: "picker" | "download"; fileName: string; } | null>;
Write a file to disk the way the editor's own exports do: into the file the browser's save dialog picks, or as a download where there is no dialog. Resolves with where it went, or null when the user dismissed the dialog.
dialog
dialog(spec: DialogSpec): DialogHandle;
panel
panel(spec: PanelSpec): PanelHandle;
Open a panel: floating over the map, docked at the right with the built-in panels, or
in the strip under the map (spec.dock). As many as you like; each closes with the plugin.
fileType
fileType(spec: FileTypeSpec): Disposable;
Claim a kind of file: dropped on the editor or picked in File ▸ Open, a file with one of
spec.extensions is handed to spec.open instead of being opened as a map. A plugin
that claims what another already has is not given it (the first stays); none can claim
the editor's own .scm, .scx or .chk. Ends with the plugin, or with dispose.
api.ui.fileType({ name: "StarCraft replay", extensions: [".rep"], open: (files) => loadReplays(files) });
mapView
mapView(spec: MapViewSpec): MapViewHandle;
Another way to look at the maps you choose, in the map's place, with a switch over the
map between it and the map itself (see MapViewSpec). A map shows one view at a time;
each map keeps the one it was left on. Ends with the plugin, or with dispose.
const game = api.ui.mapView({ title: "Game", mount: (el) => { el.append(frame); } });
// A map just opened is the one in front.
if (await api.document.open(map, "replay.scx", { viewOnly: { label: "Replay" } })) game.show(true);
statusItem
statusItem(spec: StatusItemSpec): StatusItemHandle;
A cell of your own in the status bar. One line of text beside the plugin's icon, a
spinner while busy, a click. Keep the handle and set it as things move; it
leaves with remove() or the plugin.
mapButton
mapButton(spec: MapButtonSpec): MapButtonHandle;
A button on the map, in the row at its bottom-right corner beside the tileset, size
and zoom — for something that belongs to the map being worked on, such as a shared
map's chat. Keep it to what matters while the map is in view; everything else goes
in a menu or the status bar. It leaves with remove() or the plugin.
dialogSlot
dialogSlot(dialog: DialogSlotId, spec: DialogSlotSpec): Disposable;
Add to a built-in dialog. mount runs in a row at the left of the dialog's footer each
time that dialog opens, with the dialog's working-copy fields to read and fill
(SlottedDialogId lists which dialogs and fields) — or to another plugin's dialog,
by the id its DialogSpec.slot offers. Returns the registration; it leaves with
dispose() or the plugin.
// A button in Map Properties that tidies the name in the form. The user still presses OK.
api.ui.dialogSlot("mapProperties", {
mount(body, dlg) {
body.append(api.ui.widgets.button("Tidy name", { onClick: () => {
dlg.fields.name.set(dlg.fields.name.get().trim().replace(/\s+/g, " "));
} }));
},
});Try itpreferencesPage
preferencesPage(spec: PreferencesPageSpec): Disposable;
A page of the plugin's own in Edit ▸ Preferences, under Plugins — see
PreferencesPageSpec. ui.open("preferences", { page: "plugin:<id>" }) opens it.
Returns the registration; it leaves with dispose() or the plugin.
mapTool
mapTool(spec: MapToolSpec): MapToolHandle;
Take over the pointer on the map until stop(), Esc, a right-click, a map change
or another tool. One tool runs at a time — starting one stops the previous — and a
pickArea / pickTile in progress is served first.
overlay
overlay(spec: OverlaySpec): OverlayHandle;
A picture over the map the user can switch on and off (View ▸ Overlays, the Layers panel) and that stays while they work on any layer. It draws at every repaint and hears the pointer, but never takes it. As many as you like; they go with the plugin.
pickFiles
pickFiles(options?: PickFilesOptions): Promise<File[]>;
The browser's file picker; resolves with an empty list on cancel.
pickArea
pickArea(options?: PickOptions): Promise<Rect | null>;
Let the user drag a rectangle on the map. The viewport switches to a crosshair, draws
the marquee and takes the gesture ahead of the active layer's tools; resolves with
the tile rect (exclusive x1 / y1), or null when the user pressed Esc or
right-clicked, no map is open, the map was replaced, or the plugin was disabled.
Only one pick runs at a time — a new one cancels the previous. A modal dialog
covers the map, so close yours first and reopen it with the result.
pickTile
pickTile(options?: PickOptions): Promise<{ x: number; y: number; } | null>;
As pickArea, for a single click: the tile under it.
pickObject
pickObject(options?: PickObjectOptions): Promise<PickedObject | null>;
As pickTile, for an object: the user clicks a unit or a location. The viewport
outlines and names what is under the pointer as it moves; a click on nothing keeps
picking. Resolves with the kind and the record's index, or null as pickArea does.
const picked = await api.ui.pickObject({ prompt: "Click a unit", kinds: ["unit"] });
const unit = picked && api.document.scenario()?.units[picked.index];
if (unit) await api.ui.alert(`${api.names.unit(unit.unitId)}, owned by ${api.names.player(unit.owner)}`);Try itloadImage
loadImage(source: Blob | string): Promise<ImageBitmap>;
Decode a picture: a File / Blob, a data: URL, or an http(s) URL (fetched, and
when the site refuses cross-origin reads, loaded through an <img> with
crossOrigin — a site that allows neither rejects with a message saying so).
readClipboardImage
readClipboardImage(): Promise<Blob | null>;
The image on the system clipboard, if the browser lets the page read it (a permission prompt may appear); null otherwise.
confirm
confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
Ask a yes/no question; resolves false on Cancel, Escape or the ×.
alert
alert(message: string, options?: ConfirmOptions): Promise<void>;
Say something with a single OK.
prompt
prompt(message: string, options?: PromptOptions): Promise<string | null>;
Ask for a line of text; null when the user cancelled.
const name = await api.ui.prompt("Name the map", { value: api.document.info()?.name, confirmLabel: "Rename" });
if (name) api.document.update("Rename", (tx) => tx.properties({ name }));Try itprogress
progress(label: string, options?: ProgressOptions): ProgressHandle;
A progress bar over the map for long work, with an optional Cancel.
const triggers = api.triggers.list();
const job = api.ui.progress("Reading triggers", { cancellable: true });
try {
for (let i = 0; i < triggers.length && !job.cancelled(); i++) {
job.report(i / triggers.length, `Trigger ${i + 1} of ${triggers.length}`);
await new Promise((next) => setTimeout(next, 20)); // the slow part of your work goes here
}
} finally {
job.done();
}Try ittooltip
tooltip(target: HTMLElement, content: string | ((at: TooltipPointer) => HTMLElement | string | null), options?: TooltipOptions): TooltipHandle;
A tooltip in the editor's own style over an element of yours, in place of the
browser's title. content is a line of text, or a function asked for what to show
at the pointer — rows, bars, pictures, built with el and the widgets — which is how
a chart says what is under the pointer: it is asked again as the pointer moves, and
answering null hides the tip there. The editor places it beside the pointer, keeps it
on screen and takes it away when the pointer leaves, the element leaves the page, or
the plugin stops. refresh() asks again where the pointer stands (a picture arrived,
the data moved). Remove the element's title, or both show.
// A chart that reads out the value under the pointer.
api.ui.tooltip(canvas, (at) => {
const i = Math.floor((at.offsetX / canvas.clientWidth) * values.length);
return values[i] === undefined ? null : api.ui.el("div", {}, api.ui.el("b", {}, `${values[i]}`), " minerals");
});
el
el<K extends keyof HTMLElementTagNameMap>(tag: K, props?: Record<string, unknown>, ...children: WidgetChild[]): HTMLElementTagNameMap[K];
el("div", { className: "row" }, …): the DOM helper the widgets are built from.
open
open(dialog: DialogId, payload?: Record<string, unknown>): void;
Open a built-in dialog (fire and forget; ask waits for one that answers).
ask
ask(dialog: DialogId, payload?: Record<string, unknown>): Promise<boolean>;
Open a built-in dialog that answers — "saveAs", "confirmClose", "newMap" — and
resolve true when it went through, false when it was dismissed.
repaint
repaint(): void;
Ask the viewport to repaint (a transaction does this by itself). Raises no event.
Types
Declarations only this group names.
Toastinterface
interface Toast
A short notice over the map that leaves by itself — what a save says when it is done, since the status bar line is easy to miss and the menubar dot only stops glowing.
id: number | |
|---|---|
kind: "ok" | "info" | "warn" | "error" | |
title: string | |
detail?: string | |
ttl: number | Milliseconds before it leaves on its own; 0 keeps it until dismissed. |
action?: { label: string; run: () => void; } | One button beside the text, for a notice that is really a question — the update check's "Download". Pressing it dismisses the toast as well as running this. |
PluginInfointerface
interface PluginInfo
id: string | Manifest id (or slug of the name). |
|---|---|
name: string | |
source: string | The spec the plugin was installed from ( |
version?: string | |
icon?: PluginIcon | The manifest's |
DialogSizetype
type DialogSize = "sm" | "md" | "lg" | "xl" | "full";
DialogButtoninterface
interface DialogButton
label: string | |
|---|---|
primary?: boolean | |
run?: (dialog: DialogHandle) => void | boolean | Promise<void | boolean> | Return |
closes?: boolean | Close after |
DialogTransferinterface
interface DialogTransfer
What a paste or a drop brought into a dialog.
files: File[] | |
|---|---|
text: string |
DialogSpecinterface
interface DialogSpec
title: string | |
|---|---|
size?: DialogSize | |
tall?: boolean | |
mount(body: HTMLElement, dialog: DialogHandle): void | (() => void) | Fill |
buttons?: DialogButton[] | Footer buttons, left to right; a single Close when omitted. An empty list leaves the
dialog without a footer — for a dialog that is a workspace of its own, with its own
controls, closed by the title strip's × ( |
flush?: boolean | The body runs edge to edge, without the dialog's padding: for content that draws its own frame (an editor with its own bars). |
onPaste?: (transfer: DialogTransfer, dialog: DialogHandle) => void | Ctrl+V anywhere in the dialog (while it is the topmost one). Files come from the
clipboard's items — a screenshot pastes as one |
onDrop?: (transfer: DialogTransfer, dialog: DialogHandle) => void | Something dropped onto the dialog body. |
keepOpenOnEscape?: (target: EventTarget | null) => boolean | Escape normally closes the dialog. Answer true to keep it open for this press — when the key was meant for something inside the dialog that handles Escape itself (a code editor dismissing its own popups), judged by the element it landed on. |
slot?: DialogSlotOffer | Offer a slot of this dialog's own, so other plugins can add to it with |
const name = api.ui.widgets.text({ value: api.document.info()?.name ?? "" });
api.ui.dialog({
title: "Rename the map",
size: "sm",
mount: (body) => body.append(api.ui.widgets.form([{ label: "Name", field: name }])),
buttons: [
{ label: "Cancel" },
{ label: "Rename", primary: true, run: () => { api.document.update("Rename", (tx) => tx.properties({ name: name.value })); } },
],
});Try itDialogSlotOfferinterface
interface DialogSlotOffer
A slot a plugin dialog offers to other plugins; see DialogSpec.slot.
id: string | |
|---|---|
payload?: Record<string, unknown> | |
fields?: Record<string, DialogField> |
DialogHandleinterface
interface DialogHandle
close(): void | |
|---|---|
isOpen(): boolean | Whether the dialog is still on screen. |
setTitle(title: string): void | Change the title strip's text. |
setBusy(label: string | false): void | Say the dialog is working: a ring and |
PanelSpecinterface
interface PanelSpec
A panel floats over the map and does not block it: the user keeps drawing, scrolling
and using hotkeys while it is open (except while typing in one of its fields). It
is dragged by its title bar and closed with the × or close().
title: string | |
|---|---|
width?: number | In CSS pixels; 260 by default. The panel is as tall as its content, up to the map's height. Ignored when docked. |
height?: number | Floating: a height in CSS pixels instead of the content's own; the body then scrolls, or fills, as its content asks. Bottom dock: the strip's starting height (160 by default), which the user can then drag. |
resizable?: boolean | Floating only: a grip at the bottom-right corner lets the user resize the panel, and
the size is remembered with the position for the session. The body is a column flex
box, so a root element with |
dock?: "float" | "right" | "bottom" | Where the panel lives. |
grow?: boolean | A docked panel that takes the dock's spare height (a transcript, a long list); off by default. |
flush?: boolean | Floating only: the body runs edge to edge, without the panel's padding — for content that draws its own frame. |
maximizable?: boolean | Floating only: a button in the title strip (and a double-click on it) makes the panel
fill the map's area, and puts it back. For a window of charts or tables read at length
beside a map that is still there underneath. |
mount(body: HTMLElement, panel: PanelHandle): void | (() => void) | Fill |
onClose?: () => void | The panel closed, whichever way. |
// A unit count that follows the map, docked under the built-in panels.
api.ui.panel({
title: "Unit count",
dock: "right",
mount(body) {
const show = () => { body.textContent = `${api.document.scenario()?.units.length ?? 0} units`; };
show();
const sub = api.events.on("units", show);
return () => sub.dispose();
},
});Try itFileTypeSpecinterface
interface FileTypeSpec
A kind of file a plugin opens (ui.fileType): replays, a mod's own data. A file whose
name ends in one of extensions, dropped on the editor or picked in File ▸ Open, goes
to open instead of being read as a map. Several dropped at once arrive together.
name: string | What the files are, for File ▸ Open's list of types ("StarCraft replay"). |
|---|---|
extensions: string[] | With the dot, any case: |
open(files: File[]): void | Promise<void> |
MapViewSpecinterface
interface MapViewSpec
Another way to look at a map, shown where the map is drawn (ui.mapView): a replay's
game played out, a 3D view, a picture of something else entirely. The editor gives the
view one element for its whole life and never moves it — an iframe in it is not loaded
again when the view is hidden and shown — and lays it over the map, rulers and all.
title: string | What the switch over the map calls it ("Game"). The map itself is "Map". |
|---|---|
mount(element: HTMLElement): void | (() => void) | Called once, with the element the view lives in. Fill it as you like; it is as large as
the map's area and in the page from the first time the view is shown. Return a function
to be called when the view goes (the plugin stops, or |
onShow?(look: MapViewCamera): void | The view is shown in the map's place, where the map was looking: |
minimap?: MapViewMinimapSpec | What the minimap shows while the view is shown. Without it the minimap shows the map, and its box and presses are the map's own view's (hidden behind this one). |
MapViewCamerainterface
interface MapViewCamera
Where a view looks, in map pixels (32 to a tile): the point in the middle of what it shows, and how much of the map that is.
x: number | |
|---|---|
y: number | |
width: number | |
height: number |
MapViewMinimapSpecinterface
interface MapViewMinimapSpec
A map view's own minimap (MapViewSpec.minimap): the minimap draws the map's ground, then
draw over it in place of the map's units, start locations and fog, and its box where the
view looks (MapViewHandle.setCamera).
draw?(ctx: CanvasRenderingContext2D, minimap: MinimapView): void | Draw over the map's ground, each time the minimap's picture is made while the view is shown. |
|---|---|
press?(x: number, y: number): void | A press on the minimap, and each move of a drag from it, at a map pixel: look there. Without it the minimap does not move anything while the view is shown. |
// A replay's units as dots, and the minimap moving the game's camera.
const view = api.ui.mapView({
title: "Replay",
mount: (el) => el.append(frame),
minimap: {
draw(ctx, m) {
for (const u of units) {
ctx.fillStyle = u.colour;
ctx.fillRect(m.x(u.x) - 1, m.y(u.y) - 1, Math.max(2, u.width * m.scale), Math.max(2, u.height * m.scale));
}
},
press: (x, y) => game.lookAt(x, y),
},
});
game.onUnits(() => view.redrawMinimap());
game.onCamera((c) => view.setCamera(c));
MinimapViewinterface
interface MinimapView
Map pixels to minimap canvas pixels, for MapViewMinimapSpec.draw.
scale: number | Minimap canvas pixels per map pixel. |
|---|---|
x(px: number): number | A map pixel's x on the minimap's canvas. |
y(py: number): number | |
size: number | The canvas's size in pixels, each side (the map is centred in it, letterboxed). |
MapViewHandleinterface
interface MapViewHandle
offer(on: boolean, documentId?: number): void | Offer the view on a map (by |
|---|---|
show(on: boolean, documentId?: number): void | Show the view in that map's place (offering it there if it was not), or the map again. |
isShown(): boolean | Whether the view is what the map in front shows now. |
onChange(listener: (shown: boolean) => void): Disposable | Whether the view is shown changed for the map in front: the person used the switch,
|
setCamera(camera: MapViewCamera): void | Where the view looks now, in map pixels; call it whenever that changes. The minimap draws its box there while the view is shown, and the map, shown again, opens on the same place at the same scale. |
redrawMinimap(): void | Make the minimap's picture again ( |
PanelHandleinterface
interface PanelHandle
close(): void | |
|---|---|
isOpen(): boolean | |
setTitle(title: string): void | |
maximize(on?: boolean): void | A |
isMaximized(): boolean | Whether it fills the map's area now. |
StatusItemSpecinterface
interface StatusItemSpec
One cell in the status bar (ui.statusItem): a line of text with the plugin's icon,
a spinner while busy, and a click. It is how a plugin that works in the background
stays visible without a panel — "AI · working 12 s", "3 problems", "Synced".
text: string | |
|---|---|
title?: string | The tooltip. |
busy?: boolean | Show a spinner in the cell. |
warn?: boolean | Paint the cell as a warning. |
onClick?: () => void |
const count = () => api.query.validate().filter((i) => i.level !== "info").length;
const cell = api.ui.statusItem({ text: `${count()} problems`, onClick: () => api.ui.open("validateMap") });
api.events.on("commit", () => {
const n = count();
cell.set({ text: `${n} problems`, warn: n > 0 });
});Try itStatusItemHandleinterface
interface StatusItemHandle
set(patch: Partial<StatusItemSpec>): void | Change any of the fields; the cell redraws. |
|---|---|
remove(): void | |
isShown(): boolean |
MapButtonSpecinterface
interface MapButtonSpec
A button on the map itself (ui.mapButton), in the row at its bottom-right corner.
label: string | A word or two; the button is as wide as it. |
|---|---|
title?: string | The tooltip. |
badge?: number | string | null | A count or a short mark beside the label (unread messages); nothing when absent, null, 0 or "". |
active?: boolean | Drawn pressed: what the button opens is open. |
onClick: () => void |
let notes = 3;
const button = api.ui.mapButton({
label: "Notes",
badge: notes,
onClick: () => { notes = 0; button.set({ badge: null }); },
});Try itMapButtonHandleinterface
interface MapButtonHandle
set(patch: Partial<MapButtonSpec>): void | Change any of the fields; the button redraws. |
|---|---|
remove(): void | |
isShown(): boolean |
SlottedDialogIdtype
type SlottedDialogId = "mapProperties" | "triggerEditor" | "stringEditor" | "playerSettings" | "missionBriefing";
The dialogs a plugin may add to (ui.dialogSlot), and the fields each one exposes to
the slot. A field is the dialog's working copy — what the person sees in the form,
not yet applied to the map — so a slot's button can fill it in and leave OK to them.
mapProperties:name,description.triggerEditor,missionBriefing:selected, the selected row's index in the list as text (""for none). Setting it selects that row. The index is into the dialog's working copy, which matchestriggers.list()until the person inserts or removes rows. Andmodified,"1"while that working copy has changes not yet applied (read only):close()discards them, so ask before closing when it is set.stringEditor,playerSettings: no fields, a slot only.
A plugin's own dialog can offer a slot too (DialogSpec.slot), under an id of its
choosing — "trigedit.text" is the Text Trigger Editor's, lending text with
payload.briefing — and another plugin adds to it with the same dialogSlot call.
DialogSlotIdtype
type DialogSlotId = SlottedDialogId | (string & {});
A slot id: one of the editor's dialogs, or the id a plugin dialog offers ("<plugin>.<name>" by convention).
DialogFieldinterface
interface DialogField
A form field a dialog lends to a slot: read it, set it, hear it change.
get(): string | |
|---|---|
set(value: string): void |
DialogSlotHostinterface
interface DialogSlotHost
What a slot's mount is handed: which dialog, its payload, its fields, and a way to close it.
readonly dialog: DialogSlotId | |
|---|---|
readonly payload: Readonly<Record<string, unknown>> | |
readonly fields: Readonly<Record<string, DialogField>> | |
close(): void |
DialogSlotSpecinterface
interface DialogSlotSpec
Something a plugin mounts inside a built-in dialog — a button in Map Properties
that suggests a name, one in the Text Trigger Editor that explains the text. The slot
is a row at the left of the dialog's footer; fill it with the widgets, and it looks like
part of the dialog. mount runs every time the dialog opens, with a cleanup on close.
mount(body: HTMLElement, host: DialogSlotHost): void | (() => void) |
|---|
PreferencesPageSpecinterface
interface PreferencesPageSpec
A page of the plugin's own in Edit ▸ Preferences, listed under Plugins by the plugin's
name — where a user looks for a setting, instead of a menu item per plugin. One page per
plugin; registering again replaces it. mount runs the first time the page is shown
while the dialog is open and the page then stays mounted until the dialog closes, so
what the user changed is still there when OK or Apply calls apply; the cleanup runs
on close. A page with no apply writes its settings as they change (api.storage),
which is the simpler shape when nothing needs undoing on Cancel.
mount(body: HTMLElement, page: PreferencesPageHost): void | (() => void) | Fill |
|---|---|
apply?(): void | Called on OK and Apply while the dialog is open and the page has been shown. |
reset?(): void | Called by Reset to defaults while the page is the one showing. |
const settings = api.storage.get("settings", { dock: "float" });
let pending: (() => void) | null = null;
api.ui.preferencesPage({
mount(body) {
const w = api.ui.widgets;
const dock = w.select([{ value: "float", label: "Floating" }, { value: "right", label: "Docked" }], { value: settings.dock });
body.append(w.form([{ label: "Panel", field: dock }]), w.hint("Where the panel opens."));
pending = () => api.storage.set("settings", { dock: dock.value });
},
// OK and Apply write it; Cancel leaves the stored value alone.
apply: () => pending?.(),
});Try itPreferencesPageHostinterface
interface PreferencesPageHost
What a preferences page's mount is handed.
readonly plugin: PluginInfo | |
|---|---|
close(): void | Close the Preferences dialog. |
MapPointerinterface
interface MapPointer
The pointer over the map, in the map's own units. A tile is 32 × 32 pixels.
px: number | Map pixels. Kept inside the map while a button is held, as the built-in brushes do. |
|---|---|
py: number | |
tx: number | The tile under the pointer. |
ty: number | |
inMap: boolean | False once the pointer has left the map with no button held. |
down: boolean | Whether the primary button is held. |
shift: boolean | |
ctrl: boolean | |
alt: boolean |
MapViewinterface
interface MapView
Map pixels to canvas pixels, for a tool's draw.
zoom: number | |
|---|---|
tilePx: number | Canvas pixels per tile. |
x(px: number): number | A map pixel's x on the canvas. |
y(py: number): number | |
visible: Rect | The tiles on screen. |
MapToolStopReasontype
type MapToolStopReason =
/** `handle.stop()`. */
"stopped"
/** Esc or a right-click, and `onCancel` did not keep the tool. */
| "cancelled"
/** The map was closed or replaced. */
| "document"
/** Another tool started. */
| "replaced"
/** The plugin was disabled. */
| "disabled";
MapToolSpecinterface
interface MapToolSpec
A tool owns the pointer over the map: the viewport hands it every press, move and release ahead of the active layer's own tools, hides the layer's brush ghost, and lets it draw an overlay. The map stays visible and scrollable, and a panel can stay open beside it — which is how a plugin gets a drawing mode of its own.
name: string | Shown in the viewport's HUD and the status bar while the tool runs. |
|---|---|
hint?: string | After the name: |
cursor?: string | CSS cursor over the map; |
onDown?(p: MapPointer): void | |
onMove?(p: MapPointer): void | Every pointer move over the map, button held or not, and once with |
onUp?(p: MapPointer): void | |
onCancel?(): boolean | void | Esc or a right-click. Return |
draw?(ctx: CanvasRenderingContext2D, view: MapView): void | Draw over the map, after everything else, each time the viewport repaints. |
onStop?(reason: MapToolStopReason): void | The tool is no longer running, for whatever reason (once). |
// Click to drop a marine, with a ring under the pointer. Esc or a right-click ends the tool.
let at: { px: number; py: number } | null = null;
const tool = api.ui.mapTool({
name: "Drop marines",
hint: "click to place, Esc to stop",
onMove(p) { at = p.inMap ? p : null; tool.redraw(); },
onDown(p) { api.document.edit("Drop a marine", (tx) => { tx.placeUnit(0, 0, p.px, p.py); }); },
draw(ctx, view) {
if (!at) return;
ctx.strokeStyle = "#ffd24a";
ctx.beginPath();
ctx.arc(view.x(at.px), view.y(at.py), view.tilePx / 2, 0, Math.PI * 2);
ctx.stroke();
},
});Try itMapToolHandleinterface
interface MapToolHandle
stop(): void | |
|---|---|
isActive(): boolean | |
redraw(): void | Repaint the viewport — and so call |
OverlayAbovetype
type OverlayAbove =
/** Over the terrain and grid, under doodad footprints, units, sprites and locations (the default). */
"terrain"
/** Over units, sprites and locations, under fog of war. */
| "objects"
/** Over everything but a running map tool's own drawing. */
| "everything";
Where in the viewport's paint pass an overlay draws.
OverlaySpecinterface
interface OverlaySpec
An overlay is a picture drawn over the map that the user can turn on and off: it
is listed under View ▸ Overlays and in the Layers panel, stays while the user works
on any layer, and never takes the pointer — clicks go to the active layer's tools
as usual. It hears the pointer through onHover, which is how a readout follows the
mouse while the user places units. Register one at activation and keep the handle;
it goes away with the plugin.
name: string | Shown in View ▸ Overlays and the Layers panel. Unique per plugin. In English, like a menu label: shown through the plugin's registered catalogues. |
|---|---|
visible?: boolean | Start visible; true by default. What the user last set for this name wins for the session. |
above?: OverlayAbove | Where the picture goes in the paint pass; |
draw(ctx: CanvasRenderingContext2D, view: MapView): void | Draw in canvas pixels through |
onHover?(p: MapPointer | null): void | The pointer over the map while the overlay is visible, on every layer and while a
map tool runs; |
onToggle?(visible: boolean): void | The overlay was shown or hidden, by the user in the chrome or by your handle. |
// A ring twelve tiles wide round every start location, listed under View ▸ Overlays.
const overlay = api.ui.overlay({
name: "Start rings",
above: "objects",
draw(ctx, view) {
ctx.strokeStyle = "#ffd24a";
ctx.lineWidth = 2;
for (const start of api.query.startLocations()) {
ctx.beginPath();
ctx.arc(view.x(start.x), view.y(start.y), 12 * view.tilePx, 0, Math.PI * 2);
ctx.stroke();
}
},
});
api.events.on("units", () => overlay.redraw());Try itOverlayHandleinterface
interface OverlayHandle
show(): void | |
|---|---|
hide(): void | |
toggle(): void | |
isVisible(): boolean | |
redraw(): void | Repaint the viewport — and so call |
remove(): void | Take the overlay out of the chrome for good; |
PickOptionsinterface
interface PickOptions
prompt?: string | Shown in the viewport's HUD while the user picks; also the status line. |
|---|
PickObjectOptionsinterface
interface PickObjectOptions
kinds?: PickObjectKind[] | Which kinds count; every kind when omitted. Under the pointer, a unit wins over a location. |
|---|
PickedObjectinterface
interface PickedObject
What the user picked: the record's index in document.scenario().units or .locations.
kind: PickObjectKind | |
|---|---|
index: number |
PickFilesOptionsinterface
interface PickFilesOptions
accept?: string |
|
|---|---|
multiple?: boolean |
TooltipPointerinterface
interface TooltipPointer
Where the pointer is, for a tooltip's content.
x: number | In the page, CSS pixels ( |
|---|---|
y: number | |
offsetX: number | From the target's top-left corner, CSS pixels. |
offsetY: number | |
pressed: boolean | Whether a button is held — a drag across a chart. |
TooltipOptionsinterface
interface TooltipOptions
delay?: number | Milliseconds the pointer rests before the tip shows. 500 for text, as the editor's own tooltips; 0 for a function, which reads out what is under the pointer and should keep up with it. |
|---|---|
maxWidth?: number | A width the tip does not grow past, CSS pixels; 320 by default. |
TooltipHandleinterface
interface TooltipHandle
refresh(): void | Ask |
|---|---|
hide(): void | Take the tip away until the pointer next moves over the target. |
ConfirmOptionsinterface
interface ConfirmOptions
title?: string | |
|---|---|
confirmLabel?: string | The primary button; |
cancelLabel?: string | |
danger?: boolean | Paint the primary button as a destructive action. |
PromptOptionsinterface
interface PromptOptions
value?: string | |
|---|---|
placeholder?: string | |
multiline?: boolean | A multi-line field. |
ProgressOptionsinterface
interface ProgressOptions
title?: string | |
|---|---|
cancellable?: boolean | Show a Cancel button; |
ProgressHandleinterface
interface ProgressHandle
A progress panel over the map. It does not block the editor — the user can still
scroll and look — so a plugin doing long work should report often and check
cancelled() in its loop.
report(fraction: number, text?: string): void | How far along (0…1), and optionally a line under the bar. |
|---|---|
cancelled(): boolean | True once the user pressed Cancel or closed the panel. Poll it inside a loop. |
readonly signal: AbortSignal | The same answer as a signal, for work that takes one: aborted when the user
cancels, so |
done(): void | |
isOpen(): boolean |
WidgetChildtype
type WidgetChild = Node | string | number | null | undefined | false;
Anything the widget builders accept as a child: a node, text, or nothing.
ButtonOptionsinterface
interface ButtonOptions
primary?: boolean | |
|---|---|
danger?: boolean | |
ghost?: boolean | |
busy?: boolean | Start off waiting: a ring in front of the label, and the button disabled. |
onClick?: (event: MouseEvent) => void |
ButtonElementtype
type ButtonElement = HTMLButtonElement & {
/**
* A ring in front of the label while `busy`, and the button disabled — so the press
* that started the work cannot be repeated, and says why it cannot.
*/
setBusy(busy: boolean): void;
};
The <button>, with the one call that puts it in and out of its waiting state.
CheckboxOptionsinterface
interface CheckboxOptions
value?: boolean | |
|---|---|
radio?: boolean | A radio button instead; give the group a |
name?: string | |
onChange?: (value: boolean) => void |
CheckboxElementtype
type CheckboxElement = HTMLLabelElement & {
input: HTMLInputElement;
};
The <label> a checkbox is, with its <input> on it so you can read or set the value.
TextFieldOptionsinterface
interface TextFieldOptions
value?: string | |
|---|---|
placeholder?: string | |
password?: boolean | |
onChange?: (value: string) => void |
NumberFieldOptionsinterface
interface NumberFieldOptions
value?: number | |
|---|---|
min?: number | |
max?: number | |
step?: number | |
onChange?: (value: number) => void |
SelectOptioninterface
interface SelectOption
value: string | number | |
|---|---|
label: string | |
disabled?: boolean |
SegmentedElementtype
type SegmentedElement = HTMLElement & {
get(): string;
/** Pick `value` without calling `onChange`. */
set(value: string | number): void;
};
widgets.segmented: the value picked, and a way to pick another.
TabIteminterface
interface TabItem
value: string | |
|---|---|
label: string | |
disabled?: boolean | |
mount(body: HTMLElement): void | Fill the tab's panel, the first time it is shown. What it starts ends with the panel or dialog the tabs are in. |
TabsOptionsinterface
interface TabsOptions
value?: string | The tab shown first; the first that is not disabled otherwise. |
|---|---|
onChange?: (value: string) => void | |
compact?: boolean | Smaller tabs, for a narrow panel. |
className?: string |
TabsElementtype
type TabsElement = HTMLElement & {
get(): string;
/** Show the tab `value` (mounting it the first time) without calling `onChange`. */
set(value: string): void;
/** The panel of a tab that has been shown, or null. */
panel(value: string): HTMLElement | null;
};
widgets.tabs: the tab shown, and a way to show another.
SelectOptionsinterface
interface SelectOptions
value?: string | number | |
|---|---|
onChange?: (value: string) => void |
ListIteminterface
interface ListItem
label: string | |
|---|---|
value: T | |
hint?: string | Dimmed text at the end of the row. |
index?: number | A number in the row's gutter, as the editor's own lists show indices. |
title?: string |
ListOptionsinterface
interface ListOptions
selected?: number | Which row starts selected. |
|---|---|
height?: number | Maximum height in pixels; the list scrolls past it. |
className?: string | |
onPick?: (value: T, index: number) => void |
SpinnerOptionsinterface
interface SpinnerOptions
size?: "sm" | "md" | "lg" |
|
|---|---|
label?: string | Text beside the ring. Without it you get the bare ring, to put beside your own. |
ProgressBarOptionsinterface
interface ProgressBarOptions
value?: number | null | How far along to start: 0…1, or null (the default) for the sliding bar that means "no idea how long". |
|---|---|
label?: string | A line under the bar — what is being waited for, or how much of it is done. |
percent?: boolean | The percentage at the end of the bar; on for a bar that knows how far along it is. |
width?: number | Width in CSS pixels; it fills the width it is given by default. |
ProgressBarElementtype
type ProgressBarElement = HTMLElement & {
/**
* How far along: 0…1, or null for the sliding bar. `label` replaces the line under it.
* Cheap to call often — it repaints only when the bar actually moves.
*/
set(value: number | null, label?: string): void;
};
StatusLineOptionsinterface
interface StatusLineOptions
text?: string | What it says before anything has happened. |
|---|
StatusLineElementtype
type StatusLineElement = HTMLElement & {
/**
* A finished line. `kind` colours it; nothing means plain. A `Node` in place of the
* text is for a line that carries more than words — a failure with a link to the
* settings that would fix it.
*/
set(text: string | Node, kind?: "ok" | "warn" | "error"): void;
/** Waiting, with no idea how long: a ring and the text. */
busy(text: string): void;
/** Waiting with a share done (0…1), or null for the sliding bar. */
progress(text: string, value: number | null): void;
/**
* A Cancel beside the line that calls `stop`; null takes it away. `label` is the
* button's word — "Stop" for a request that is running, since Cancel in a dialog
* means leaving it.
*/
cancel(stop: (() => void) | null, label?: string): void;
/** Back to an empty line. */
clear(): void;
};
The line along the bottom of a dialog: what happened, what is happening, how far along it is, and the Cancel that stops it — in one place, so a dialog has one voice. It is a live region, so a screen reader hears the outcome (but not every byte of a download: the bar's own movement is not announced).
SkeletonOptionsinterface
interface SkeletonOptions
width?: string | CSS width, |
|---|---|
height?: number | Height in CSS pixels: 10 for a line, 44 for a block. |
lines?: number | How many lines, stacked; 1 by default. The last is drawn short, as text ends short. |
block?: boolean | A block — the place a thumbnail or a picture will take — rather than a line of text. |
BusyOptionsinterface
interface BusyOptions
label?: string | What is being waited for, over the covered box. |
|---|---|
dim?: boolean | Dim what is already there rather than leaving it at full strength; true by default. |
BusyHandleinterface
interface BusyHandle
set(label: string): void | Change the label without lifting the cover. |
|---|---|
done(): void | Uncover the box. Safe to call twice. |
StepsOptionsinterface
interface StepsOptions
tail?: number | While the list is |
|---|
StepOptionsinterface
interface StepOptions
icon?: string | A character in front of the label, in the list's own vocabulary: ✎ for a change, ▸ for a read. |
|---|---|
title?: string | The row's tooltip. |
running?: boolean | Begin running (a ring) rather than pending (a hollow mark). |
StepHandletype
type StepHandle = {
readonly element: HTMLElement;
/** Rename the row (a step named before its arguments were known), and its tooltip. */
set(label: string, title?: string): void;
start(detail?: string): void;
done(detail?: string): void;
fail(detail?: string): void;
skip(detail?: string): void;
/** Change the detail and nothing else — a clock ticking beside a running step. */
detail(text: string): void;
/** Something under the label — a thumbnail, a line of output. */
append(node: Node): void;
};
One row of a steps list. Each state takes the detail shown dim after the label.
StepsElementtype
type StepsElement = HTMLElement & {
add(label: string, options?: StepOptions): StepHandle;
/**
* A line between rows that is not a step — an assistant's words between its tool
* calls. `at` puts it before the row at that index; without it, at the end.
*/
note(content: string | Node, at?: number): HTMLElement;
/** Rows and notes so far, for `note`'s `at`. */
size(): number;
/** Steps so far (rows, not notes). */
count(): number;
/** Whether work is running: while it is, the `tail` window applies. */
running(on: boolean): void;
};
A list of steps: one row per unit of work, each pending, running, done, failed or skipped, with a detail after its label and a mark at its end. What a builder shows while it builds, and what a tool-using assistant shows while it works.
FoldOptionsinterface
interface FoldOptions
text?: string | The line to begin with. |
|---|---|
open?: boolean | Open to begin with. |
busy?: boolean | Start with a ring for a mark. |
FoldElementtype
type FoldElement = HTMLDetailsElement & {
/** Where the content goes. */
readonly body: HTMLElement;
/** The line: text, or text and nodes. */
set(...content: (string | Node)[]): void;
/** The mark in front of the line: `"busy"` for a ring, a character (✓, ✗) in a colour, or null for none. */
mark(sym: string | null, kind?: "ok" | "warn" | "error"): void;
/** A control at the end of the line — an Undo — whose click does not fold the block; null takes it away. */
action(node: HTMLElement | null): void;
};
A block that folds to one line: a mark, the line, a control at its end, and a body. For work worth a line when it is done and a list while it runs — one turn of an assistant, a pass over the map — and for anything long that most readers skip, such as a model's reasoning.
WidgetsApiinterface
interface WidgetsApi
Buttons, fields, forms and lists in the editor's own styles, as plain DOM. A plugin
dialog built from these looks like a built-in one; el is the escape hatch for
anything they do not cover.
button(label: string, options?: ButtonOptions): ButtonElement | |
|---|---|
checkbox(label: string, options?: CheckboxOptions): CheckboxElement | |
text(options?: TextFieldOptions): HTMLInputElement | |
number(options?: NumberFieldOptions): HTMLInputElement | |
select(items: SelectOption[], options?: SelectOptions): HTMLSelectElement | |
segmented(items: SelectOption[], options?: SelectOptions): SegmentedElement | Buttons side by side of which one is on: a choice of a few short options seen at once
("Both", "Minerals", "Gas"), where a |
tabs(items: TabItem[], options?: TabsOptions): TabsElement | Tabs in the editor's style: a row of tabs over a panel a tab. A tab's |
form(rows: (FormRow | null | undefined | false)[]): HTMLElement | A two-column grid of labelled fields. |
group(title: string, ...children: WidgetChild[]): HTMLElement | A titled box (a |
row(...children: WidgetChild[]): HTMLDivElement | |
column(...children: WidgetChild[]): HTMLDivElement | |
hint(text: string): HTMLElement | Small dimmed explanatory text. |
separator(): HTMLElement | |
list<T>(items: ListItem<T>[], options?: ListOptions<T>): HTMLElement | |
spinner(options?: SpinnerOptions): HTMLElement | A turning ring: on its own to put beside your own text, or with a |
progressBar(options?: ProgressBarOptions): ProgressBarElement | A bar, with the percentage and a line under it. |
statusLine(options?: StatusLineOptions): StatusLineElement | The dialog's bottom line: |
skeleton(options?: SkeletonOptions): HTMLElement | A grey stand-in for content that has not arrived, in the shape it will take. Rows of these in the list you are about to fill say more than an empty box does, and the list does not jump when the answer lands. |
busy(target: HTMLElement, options?: BusyOptions | string): BusyHandle | Cover a box while what is in it is being replaced: dimmed, deaf to clicks, with a
ring and a label over it. |
steps(options?: StepsOptions): StepsElement | A list of steps, each pending → running → done / failed / skipped, with a detail. |
fold(options?: FoldOptions): FoldElement | A block folded to one line — a mark, a line that can change, a control at its end — over a body. |
Seen in
- Hello Worldthe smallest complete plugin: one menu item and one dialog, to copy from
- Paintmap tools, a floating panel, and painting with the active layer's brush
- Walkabilitya read-only overlay drawn over the map
- Melee Wizardplacing units with the editor's own placement checks
- Terrain from Imagean image turned into terrain, one transaction
- scmscx.comopening a map fetched from elsewhere
- TrigEditthe text trigger format, and a dialog that offers a slot to other plugins
- Aftermatha replay drawn over its map with an overlay
- scmjs.devan account, shared maps, docked panels and dialog slots