UI

Widgets

Ready-made UI widgets — radial menu, GML tooltip parser, currency display, segmented resource bars, radial progress, drag-and-drop, virtual list, modals, and toast notifications.

The src/view/ tree ships a set of self-contained widgets covering the most common HUD patterns. Each extends View and follows the link/unlink lifecycle described in UI toolkit.

Radial menu

RadialMenu (in src/view/elements/radial/RadialMenu.js) renders a donut-ring context menu as an SVG backdrop plus positioned RadialMenuElement children. Each slice is described by a RadialMenuElementDefinition.

import { RadialMenu } from "@woosh/meep-engine/src/view/elements/radial/RadialMenu.js";
import { RadialMenuElementDefinition } from "@woosh/meep-engine/src/view/elements/radial/RadialMenuElementDefinition.js";

const sword = new RadialMenuElementDefinition();
sword.name = "Attack";
sword.fill = "#c0392b";
sword.iconView = attackIconView;
sword.action = () => castAttack();

const menu = new RadialMenu([sword, spellDef, itemDef], {
    outerRadius: 130,
    innerRadius: 50,
    focusWidth: 20,
    backgroundColor: "rgba(0,0,0,0.6)"
});

menu.autoLayout();   // normalizes shares so items fill the ring, positions first item at top

Key RadialMenuElementDefinition fields:

FieldTypeDescription
sharenumberFraction of the full circle this slice occupies (0–1; auto-normalized by autoLayout)
offsetnumberClockwise start offset within the circle, normalized
fillCSS color stringBackground fill for the slice
iconViewViewIcon placed at the midpoint of the slice
namestringText label
actionfunctionCalled when the slice is activated via runSelected()
onSelected / onDeSelectedfunctionCalled when hover selection changes

menu.selectByAngle(radians) updates hover selection from a pointer angle (0 = up, clockwise). menu.runSelected() fires the action of every currently selected slice.

For ECS-integrated context menus, RadialContextMenu in src/engine/ecs/gui/menu/radial/RadialContextMenu.js wraps the same view with appear/disappear animations driven by AnimateAppearance and AnimateDisappearance.

GML tooltips

GML (Game Markup Language) is a lightweight markup language for tooltip text that supports inline references to game data and style spans. The engine is GMLEngine in src/view/tooltip/gml/GMLEngine.js.

A GML string uses [type:arg] tokens for references and [$className] / [/className] for style spans. The parser (parseTooltipString in src/view/tooltip/gml/parser/parseTooltipString.js) produces a token stream of four types:

Token typeValueEffect
Textplain stringRendered as a LabelView <span>
ReferenceTooltipReferenceValueDispatched to a registered GMLReferenceCompiler
StyleStartCSS class nameOpens a <span> with that class
StyleEndCSS class nameCloses the matching span

GMLEngine.compile(code) returns a View tree. GMLEngine.compileAsText(code) returns a plain string. GMLEngine.compile_localized(key, target, seed) looks up a localization key first, then compiles.

Compilers for custom reference types are registered per-engine:

import { GMLReferenceCompiler } from "@woosh/meep-engine/src/view/tooltip/gml/compiler/GMLReferenceCompiler.js";

const itemCompiler = new GMLReferenceCompiler();
itemCompiler.compile = (values, db, loc, gml) => {
    const itemId = values[0];
    const item = db.getItem(itemId);
    const view = new ItemTooltipView(item);
    return view;
};

engine.gui.gml.addReferenceCompilerVisual("item", itemCompiler);

DomTooltipManager (in src/view/tooltip/DomTooltipManager.js) and TooltipManager (in src/view/tooltip/TooltipManager.js) handle hover: TooltipManager tracks the cursor and shows a TooltipView at the pointer position when the hovered element registers a tip. The GMLEngine is wired into GUIEngine automatically at startup.

Currency display

CompositeCurrencyLabelView (in src/view/currency/CompositeCurrencyLabelView.js) renders an ObservedInteger as a multi-denomination label. A CurrencyDisplaySpec lists the denominations in descending order:

import { CompositeCurrencyLabelView } from "@woosh/meep-engine/src/view/currency/CompositeCurrencyLabelView.js";
import { CurrencyDisplaySpec } from "@woosh/meep-engine/src/view/currency/CurrencyDisplaySpec.js";

const goldSpec = new CurrencyDisplaySpec();
goldSpec.add("platinum", 100);  // 100 copper = 1 platinum
goldSpec.add("gold",      10);  // 10 copper = 1 gold
goldSpec.add("copper",     1);

const label = new CompositeCurrencyLabelView({
    value: player.gold,   // ObservedInteger
    spec: goldSpec
});

On each change the view rebuilds its children as one CurrencyDenominationLabelView per non-zero denomination, from most-significant to least. The root element carries CSS classes negative or positive depending on sign.

FantasyCurrencySpec in src/view/currency/FantasyCurrencySpec.js provides a sample three-tier fantasy denomination preset.

Segmented resource bar

SegmentedResourceBarView (in src/view/elements/progress/segmented/SegmentedResourceBarView.js) renders one or more stacked fill layers with canvas-drawn notch marks that scale automatically to the maximum value.

import { SegmentedResourceBarView } from "@woosh/meep-engine/src/view/elements/progress/segmented/SegmentedResourceBarView.js";

const bar = new SegmentedResourceBarView({
    values: [entity.health, entity.shield],  // ObservedInteger or Vector1 each
    max:    entity.maxHealth,
    classList: ["resource-bar"]
});

Each value gets its own fill div styled .value-0, .value-1, … via CSS. A .ghost div trails the previous fill for a smooth drain effect. Notch marks are drawn with Canvas2D on a CanvasView overlay; the mark density (minor at 2-unit, major at 10, 40, 200, 1000, 5000, 25000 steps) is selected automatically from RESOURCE_BAR_SEGMENTS based on the current max. A DomSizeObserver ensures the canvas redraws when the element resizes.

Highlight ranges can be added to bar.highlights (a List<NumericInterval>) and are rendered as .highlight overlays positioned proportionally.

Radial progress

RadialProgressView (in src/view/elements/radial/RadialProgressView.js) draws an SVG arc filling from 0 to the current fraction of a BoundedValue or a [value, max] array pair.

import RadialProgressView from "@woosh/meep-engine/src/view/elements/radial/RadialProgressView.js";

const progress = new RadialProgressView(
    entity.castProgress,  // BoundedValue
    { fill: "#e74c3c", thickness: 8 }
);
progress.size.set(64, 64);

The arc is a single SVG <path> generated by SVG.arcPath(r0, r1, a0, a1). thickness controls the ring width. Arc start is at 12 o’clock. The path and camera are rebuilt on size.onChanged.

Drag and drop

Three classes in src/view/common/dnd/ implement HTML5 drag and drop:

  • DragAndDropContext — owns the registry of draggable elements and drop targets in one scope.
  • Draggable — wraps a View; wires dragstart, dragend, drop, and related events. Fires on.dragInitialized and on.dragFinalized signals.
  • DropTarget — wraps a View; accepts drops with an optional validation(draggable) => boolean predicate. Fires on.added, on.removed, on.enter, on.exit, and on.invalidDrop signals. Automatically adds drop-target-hover-valid or drop-target-hover-invalid CSS classes during hover.
import { DragAndDropContext } from "@woosh/meep-engine/src/view/common/dnd/DragAndDropContext.js";

const dnd = new DragAndDropContext();

const draggable = dnd.addElement(itemView, parentTarget);

const slot = dnd.addTarget(slotView, "inventory", (d) => d.view.itemType === "weapon");
slot.on.added.add((draggable, previousParent) => {
    equip(draggable.view.item);
});

DragAndDropContext.removeElement(draggable) and removeTarget(view) clean up event listeners.

Virtual list view (ListView with cache)

ListView (in src/view/common/ListView.js) renders a reactive list, creating or recycling one view per item. With useCache: true it holds an LRU Cache keyed by item hash, so removed items’ views are reused when equivalent items are inserted, avoiding repeated construction for long or frequently-updated lists.

See UI toolkit for the full ListView API.

GUIEngine.createModal({ content, title, priority }) layers a full-screen ui-modal-overlay div over the scene and opens a SimpleWindow centred at anchor (0.5, 0.5). The overlay is its own entity; clicking it destroys the modal. Modals are queued in a ModalStack (in src/engine/ui/modal/ModalStack.js) sorted by priority — only one is active at a time; when it is destroyed the next-highest-priority modal activates.

GUIEngine.createModalConfirmation({ title, content, confirmationEnabled }) returns a Promise that resolves on confirm and rejects on cancel or dismiss.

GUIEngine.createAlert({ title, content, marks, priority }) presents a single “continue” button and resolves on dismiss.

ConfirmationDialogView (in src/view/elements/ConfirmationDialogView.js) is the underlying view: it wraps any content view with a row of ButtonView buttons, each mapped to a callback, with an optional ObservedBoolean enabled guard.

Toast notifications

ToastLogView (in src/view/elements/notify/ToastLogView.js) listens to a NotificationLog and plays each new notification as a slide-in/fade-out toast. Items slide in from the right (200 px) over 200 ms, stay for a configurable displayDuration (default 5 s), then fade out over 1 s.

NotificationManager (in src/engine/ui/notification/NotificationManager.js) manages named channels and multiple display bindings per channel:

import { NotificationManager } from "@woosh/meep-engine/src/engine/ui/notification/NotificationManager.js";
import ToastLogView from "@woosh/meep-engine/src/view/elements/notify/ToastLogView.js";

const nm = new NotificationManager();
nm.ecd = entityManager.dataset;

nm.createChannel("combat");
nm.addDisplay("combat", (log) => {
    const toast = new ToastLogView(log, { displayDuration: 4 });
    containerView.addChild(toast);
});

nm.addNotification(new Notification("Hit for 12 damage"), "combat");

addEmitterDisplay(channel, viewEmitter, grouping) is an alternative that routes each notification through a ViewEmitter for particle-style animated entries, with automatic ECS entity management per spawned view. nm.tick(timeDelta) must be called each frame to advance emitter animations.