Documentation
Browse documentation

DocumentationWrite a widget

The view

view.ocml describes what a PlayerVox OverCrow widget shows: a tree of elements, bound to the widget's state with {…} expressions. It looks like HTML or JSX, but it is neither: the set of elements is closed, there is no DOM and no script in the view. OverCrow lays out and paints the tree itself.

OCMLtemplates/list/view.ocml
<box class="checklist">
  <text class="title">{t("title")}</text>
  <list class="items" item-height="32">
    <for each={state.items} as="item" key={item.id}>
      <box class="row">
        <checkbox label={item.text} checked={item.done} on:change={setDone(item.id, event.value)}/>
        <text class={item.done ? "text done" : "text"}>{item.text}</text>
        <button class="remove" label={t("remove")} on:activate={remove(item.id)}>
          <icon name="x"/>
        </button>
      </box>
    </for>
  </list>
  <text class="summary">{t("remaining", { count: remaining(state.items) })}</text>
</box>

This is the checklist template: a title, a scrolling list that repeats a row per item, and a summary. The logic exports setDone, remove and remaining; t reads the messages of locales/.

The view is compiled when the widget is packaged: overcrow-widget check reports every mistake with its line and column, and an unknown element, attribute or event is an error, never ignored. The source file itself is not shipped.

Markup

  • The file is UTF-8, without a byte-order mark.
  • The document is a list of top-level nodes: <component> declarations and the children of the widget's root, which is a box.
  • Elements are lowercase: <name …>…</name> or <name …/>.
  • An attribute value is quoted text ("…" or '…'), an expression ({…}), or nothing, which means true: <button submit>. Attributes are separated by whitespace and never repeated.
  • Comments are <!-- … -->, without -- inside.
  • DOCTYPE, processing instructions, CDATA sections and namespaces are refused.

The entities are &amp; &lt; &gt; &quot; &apos; &lbrace; &rbrace; &nbsp;, and &#…; or &#x…; for any character that is not a control character. A literal { or } in a quoted value, and a lone } in text, are refused: write &lbrace; and &rbrace;.

Elements

Each element has a fixed content, a set of attributes and the events it can send. Containers (box, scroll, list, form, popover, button) hold other elements; text holds text and inline children; the others are leaves that PlayerVox OverCrow draws: icon, image, toggle, slider, field, chart, gauge…

ElementMeaning
boxGeneric container; flex or grid layout comes from style. The root of the view is a box.
scrollClipping container scrolled by the host with wheel, drag and keyboard.
listVertical scrolling list whose children are laid out and painted only when visible.
textBlock of wrapped text; its inline children flow with it (chat line with emotes).
spanStyled run of text inside text.
iconLucide icon tinted with color, sized by font-size unless width and height are set.
imageRaster image from the package, or an image handed by a service (an asset: handle); object-fit applies.
avatarRound image with a text fallback while the image is missing or failed.
badgeShort pill of text.
buttonActivatable control; its children (icon, text) form its content.
toggleOn/off switch.
checkboxCheck control (checklists).
sliderNumeric range control; dragging is handled by the host.
selectDrop-down choice; the open list is drawn by the host.
optionChoice of a select; its text is the visible label.
fieldSingle-line text input edited by the host, input methods (IME) included.
textareaMulti-line text input edited by the host, input methods (IME) included.
formGroups named controls. With intent, submitting sends their values to a write service directly from the host; the widget's logic never supplies that content.
elapsedDuration text advanced by the host from an anchor given by a service, so a running stopwatch needs no work from the logic. Styled like text; repainted only when the shown text changes, at most at 60 Hz, and never while hidden.
progressHorizontal progress bar.
gaugeRing or arc gauge.
chartLine, area, bar or sparkline chart drawn by the host.
seriesOne data series of a chart, coloured with color.
separatorThin rule between groups.
canvasSurface painted by the host from the latest list of commands given to draw.
popoverFloating panel anchored to an element, drawn above the content and clipped to the widget's frame (drop-downs, context menus).

Elements and attributes gives the attributes and events of each one.

Text

Text is allowed only in text, span, badge and option. A text holds either text or inline children (span, icon, image), not both: to style one word, put every run in a span.

OCMLwidgets/performance/view.ocml
      <text class={row.valueClass} label={row.name} tooltip={row.hint}><span>{row.value}</span><if test={row.unit != ""}><span class="unit">{row.unit}</span></if></text>

Whitespace follows the rule of JSX:

  • tabs become spaces;
  • a line break between two words becomes one space;
  • a line break next to a tag or an expression disappears with its indentation;
  • a run of whitespace next to a tag is dropped; one between two expressions on the same line is kept.

So a break before an expression removes the space before it: write you have {state.count} messages on one line to keep both spaces, or write &#32; where you need a space that the rule would remove.

Attributes

A quoted value is converted to the attribute's type and checked when the view is compiled:

TypeQuoted value
booleantrue or false; the bare attribute means true
integera decimal integer, without a leading zero
numbera decimal number, without an exponent
number listnumbers separated by spaces: values="12 18.5 30"
text, identifierthe text itself
imagea file of the package: src="assets/logo.png"

attr={expression} binds the attribute to an expression instead; PlayerVox OverCrow checks every value the widget sends. Values that exist only when the widget runs, such as an image handed by a service, must be bound.

Five attributes exist on every element:

AttributeTypeRequiredMeaning
classclass listnoStyle classes: distinct names separated by one space.
refstatic ref namenoName by which draw designates this canvas and by which an attribute such as the anchor of a popover designates this element. Static, unique in the view, and not allowed inside a for or a component body, where it would name several elements.
labeltext ≤ 256 bytesnoAccessible name; required on icon-only controls.
tooltiptext ≤ 256 bytesnoPlain text drawn by the host on hover or focus.
onsorted event namesnoEvents forwarded to the logic; written on:<event> in the view.

class is the hook of the style. label is the accessible name of the element: give one to every control whose content is an icon.

Names for nodes: ref

ref="name" gives a node a name. Two things use it: draw(name, commands) paints the canvas of that name, and a popover is placed against the node its anchor names.

OCMLdocs/content/examples/countdown/view.ocml
<box ref="panel" class="countdown" on:contextmenu={openMenu}>
OCMLdocs/content/examples/countdown/view.ocml
<popover anchor="panel" placement="pointer" open={state.menu} on:dismiss={closeMenu}>
  <button class="menu-row" on:activate={resetRounds}>
    <text class="menu-text">{t("reset-rounds")}</text>
  </button>
</popover>

A ref is always quoted, unique in the view, and refused inside a for or a component, where it would name several nodes.

Events

on:event={handler} asks PlayerVox OverCrow to send an event of the element to the logic. Without it, the widget does not hear about the event at all: a widget is not told about pointer moves or key presses it has not asked for.

A handler is written in one of two ways:

  • the name of a function exported by the logic, called with the event's detail: on:activate={increment};
  • a call, with the arguments you choose; in it, event is the event's detail: on:change={setDone(item.id, event.value)}.
OCMLtemplates/list/view.ocml
        <checkbox label={item.text} checked={item.done} on:change={setDone(item.id, event.value)}/>
        <text class={item.done ? "text done" : "text"}>{item.text}</text>
        <button class="remove" label={t("remove")} on:activate={remove(item.id)}>
          <icon name="x"/>
        </button>

The events are activate, contextmenu, wheel, keydown, input, change, submit, focus, blur, reachend, stick and dismiss; events describes each one and the detail it carries, and each element lists the ones it sends. Some of them are user actions, during which a widget may call a protected service; see calls that need a user action. Widgets receive input only in PlayerVox OverCrow's Interactive mode: in Passive mode, clicks go through them to the game.

Conditions, loops and components

SyntaxMeaning
{expr}Interpolates an expression into text or an attribute value.
<if test={expr}> … <else-if test={expr}> … <else>Conditional subtree.
<for each={expr} as="item" key={expr}>Repeats a subtree; key is mandatory and must be unique among siblings.
<component name="x" props="a b"> … <slot/> … </component>Local component with declared properties and one slot, used as <x a={…}>.
on:<event>={handler}Subscribes the element to one of its events and names the handler of the logic.

if, else-if, else

OCMLwidgets/stopwatch/view.ocml
        <if test={state.stopwatch.running}>
          <icon name="pause"/>
        </if>
        <else>
          <icon name="play"/>
        </else>

else-if and else follow an if directly; only whitespace and comments may separate them.

for

for repeats its children for each item of a list. each, as and key are all required, and the key must be unique among the repeated items: it is how PlayerVox OverCrow knows which row moved, appeared or went away.

OCMLwidgets/warframe-market/view.ocml
      <for each={state.results} as="item" key={item.slug}>
        <button class="result" label={item.name} on:activate={select(item.slug)}>
          <text class="result-name">{item.name}</text>
          <icon class="result-arrow" name="chevron-right"/>
        </button>
      </for>

For a long list, put the for in a list element: PlayerVox OverCrow lays out and paints only the rows that are visible.

Components

A component is a piece of view with a name and declared properties, to write once and use several times. It is local to the view.

OCMLdocs/content/examples/countdown/view.ocml
<component name="stat" props="name value">
  <box class="stat">
    <text class="stat-name">{name}</text>
    <text class="stat-value">{value}</text>
  </box>
</component>
OCMLdocs/content/examples/countdown/view.ocml
  <stat name={t("state")} value={stateText(state.running, state.remainingMs)}/>
  • Components are declared at the top level, once each, with a name that is not an element's.
  • The body sees only its own properties, not the names around the place where it is used. Pass what it needs.
  • A use passes declared properties only. A quoted property is text; bind it (value={5}) for any other type.
  • A body may contain one <slot/>, where the children of each use go.
  • A component cannot contain itself, and a use cannot take on: handlers: put them on elements inside the body.

if, for and components are transparent for the content rules: their children must suit the element around them.

Expressions

Expressions are a small, strict subset of JavaScript:

  • literals: numbers (0, 12, 1.5), strings, true, false, null, array and object literals;
  • names in scope and their members: a.b, a?.b, a[i], a?.[i];
  • operators: !, unary - and +, * / %, + -, < <= > >=, == != === !==, &&, ||, ??, and a ? b : c. ?? cannot be mixed with && or || without parentheses;
  • calls of functions exported by the logic, by their bare name: remaining(state.items). t(key, params) is one of them.

The names in scope are state, the as names of the enclosing fors, the properties of the enclosing component, and event in a handler. A name cannot hide another one.

There is no assignment, no arrow or function literal, no new, this, template literal, regular expression, method call or global: anything more than a simple computation belongs in a function of the logic. This is what keeps the view checkable: when a widget is packaged, every expression is compiled into a function of logic.js, and the view itself carries no code.

OCMLtemplates/list/view.ocml
        <text class={item.done ? "text done" : "text"}>{item.text}</text>

JavaScript reserved words, constructor, prototype, eval, undefined, globalThis, overcrow and any name starting with __ are refused.

When the view is wrong

Every problem is a diagnostic of overcrow-widget check, with a stable code:

CodeProblem
view.syntax, view.encoding, view.size, view.invalid_entityThe file is not well-formed markup.
view.unknown_element, view.invalid_parentAn element that does not exist, or in a place it cannot be.
view.unknown_attribute, view.duplicate_attribute, view.invalid_attribute, view.missing_attributeAn attribute that the element does not have, written twice, with a wrong value, or required and absent.
view.unknown_event, view.invalid_handlerAn event the element does not send (or put on a component use), or a handler that is neither a name nor a call.
view.expression_syntax, view.expression_too_long, view.expression_too_deep, view.expression_too_many_entriesAn expression outside the subset or its bounds.
view.reserved_name, view.unknown_nameA refused name, or one that is not in scope.
view.invalid_textText where the element takes none, or mixed with inline children.
view.invalid_component, view.recursive_component, view.invalid_slot, view.invalid_constructA component, slot, if or for that breaks its rules.
view.invalid_ref, view.unknown_refA ref that is not unique or is inside a for, or an anchor that names none.
view.missing_assetA static image that is not in assets/.
view.bound_valueA value on a control of a form bound to a write intent; see forms.
view.too_many_elements, view.too_many_components, view.too_many_children, view.too_many_expressions, view.too_deepA limit of the view is exceeded.
logic.missing_exportThe view calls a function that the logic does not export.