Skip to content

Keyboard, input and drag

onKey fires on every component that carries it while the interface is open:

src/entry.ts2
<text x={0} y={22} w="fill" h={16} text={`${amount}*`} font={Fonts.PLAIN}
align="center" valign="middle" shadow color={Colors.WHITE}
onKey={(event) => {
if (event.keyCode === KEY_ENTER) {
setWithdraw(amount);
setAmount(0);
return;
}
if (event.keyCode === KEY_BACKSPACE) {
setAmount(amount / 10);
return;
}
if (event.keyChar >= CHAR_ZERO && event.keyChar <= CHAR_NINE && amount < 100000) {
setAmount(amount * 10 + (event.keyChar - CHAR_ZERO));
}
}} />

event.keyCode is the client’s own code for the key, not a character code: Escape is 13, Enter 84, Backspace 85. event.keyChar is the typed character’s code, and is what a digit or letter is read from. Name the codes once:

src/entry.ts2
/** The client's key codes for the keys this prompt understands. */
const KEY_ENTER = 84;
const KEY_BACKSPACE = 85;
/** The character codes of '0' and '9'. */
const CHAR_ZERO = 48;
const CHAR_NINE = 57;

For “Escape closes it”, put a hotkey on an option instead (Keys on options).

Keep what is typed in a cell and draw the cell:

src/entry.ts2
export const Committed = (): Component => {
const [amount] = useState(withdrawAmount);

Interface state has the three kinds of cell.

A key handler builds numbers, not text; for free text, use an <input>.

<input> is a text field. focus gives it the caret when the interface opens:

src/chat.ts2
<input x={4} y="center" w={{ fill: 8 }} h={16} font={Fonts.PLAIN}
text="selling rune scimitar" valign="middle" shadow
color={Colors.YELLOW} promptColor={Colors.DIM}
selectColor={Colors.WHITE} selectBgColor={0x3d5a8a}
lineWidthLimit={40} focus />
Prop
textwhat the field starts with
focusthe field has the caret when the interface opens
promptColor, selectColor, selectBgColorthe caret, the selected text, and its background
lineWidthLimit, lineCountLimit, lineWrapWidthhow much may be typed, and where it wraps

What the player types goes to the server; the interface cannot read it back. <input> has the full prop list.

draggable takes a ref to the container the drag is measured in. The component cannot leave that box, and every position the drag reports is relative to it:

src/slider.ts2
<layer ref={zone} x={ZONE_INSET} y={0} w={ZONE_W} h={PIECE}
onClick={(event) => { setLevel(Math.clamp(0, 100, (event.mouseX - PIECE / 2) * 100 / TRAVEL)); }}>
<sprite x={level * TRAVEL / 100} y={0} w={PIECE} h={PIECE} sprite={Slider.THUMB}
draggable={zone} dragMoves dragDeadZone={0} dragDeadTime={0}
onDrag={(event) => { setLevel(Math.clamp(0, 100, event.mouseX * 100 / TRAVEL)); }} />
</layer>
Prop
dragMovesthe component stays where its props put it; nothing is carried under the pointer
dragDeadZonehow many pixels the pointer moves before a press becomes a drag
dragDeadTimehow many cycles it is held before a press becomes a drag

Without dragMoves, the client carries a copy of the component under the pointer, as it does an item in the backpack. A slider thumb wants dragMoves.

onDrag fires as the pointer moves, and event.mouseX is the dragged component’s left edge inside the container. Write it to a cell and draw from the cell:

src/slider.ts2
<text x="right" y="center" w={36} h={14} text={`${level}%`} font={Fonts.PLAIN}
align="right" valign="middle" shadow color={Colors.YELLOW} />

dragTarget marks a component as somewhere a drag may end. On release, onDragComplete fires on the dragged component, and event.dragTarget is the target it landed on, or -1 for nothing:

src/slider.ts2
<sprite x="center" y="center" w={36} h={32} item={left} itemCountMode="never" draggable={row} dragTarget
onDragComplete={(event) => { if (event.dragTarget !== -1) { setFirst(1 - first); } }} />

A drop between the item cells of a backpack or bank reaches the server on its own.