Skip to main content

Input

Elements respond to the mouse, to touch and to XR controllers through the application's ElementInput. It listens to the browser's input events, works out which element is under the pointer and fires events on that element's component. This page covers the events that every interactive element shares. Buttons add hover and press states on top of them, and scroll views use them to drag.

Enabling UI Input​

An element receives input when two things are true: the application has an ElementInput (see Setting Up), and the element has input enabled. Elements without input enabled are never hit, so enable it on the elements the user interacts with and leave it off on the rest, such as decorations and the labels on buttons. A click on a label then goes to the button below it.

card.element.useInput = true;

You can also pass useInput: true to addComponent('element', ...). The debug build of the engine logs a warning when an element enables input in an application that has no ElementInput.

Each surface creates a different set of input devices, which matters when UI and game input meet:

SurfaceInput devicesElement input
EngineThe ones you pass to AppOptionsYours to create, before the mouse and touch devices
EditorThe mouse, touch, keyboard and gamepad devices enabled in the INPUT settingsAlways, created before the other devices
Reactapp.mouse and app.touch. There is no keyboard deviceAlways, created before the other devices
Web Componentsapp.mouse and app.keyboard. There is no touch device, but element input handles touch itselfAlways, created before the other devices

Input Events​

These events are fired on the element component:

EventFired when
mouseenterThe pointer moves onto the element
mouseleaveThe pointer moves off the element
mousemoveThe pointer moves over the element. After a button is pressed on the element, it receives every move until the button is released
mousedownA mouse button is pressed over the element
mouseupA mouse button is released over the element, or anywhere after it was pressed on the element
mousewheelThe mouse wheel turns over the element
clickA mouse button, or a touch, is pressed and released over the same element
touchstartA touch starts on the element
touchmoveA touch that started on the element moves, wherever it goes
touchleaveA touch that started on the element moves off it, once per touch
touchendA touch that started on the element ends, wherever it ends
touchcancelA touch that started on the element is canceled by the browser
selectstart, selectend, selectmove, selectenter, selectleaveAn XR controller or hand points at the element and selects it. See UI in XR

Listening for Events​

Listen for events on the element component. The code that does it runs in a different place on each surface:

card.element.on('mouseenter', () => {
card.element.opacity = 1;
});
card.element.on('mouseleave', () => {
card.element.opacity = 0.6;
});

on returns an EventHandle. Call its off() method to stop listening.

Every handler receives an event object. event.element is the element the event was fired on, even when the handler belongs to one of its ancestors, and event.event is the browser event it came from:

Event objectFired forProperties
ElementMouseEventmouse*, and click from a mousex and y, the pointer position in CSS pixels from the top-left of the canvas; dx and dy, the movement since the last event; button; wheelDelta, which is -1, 0 or 1; ctrlKey, altKey, shiftKey and metaKey
ElementTouchEventtouch*, and click from a touchx and y of this touch; touch, the browser's touch; touches and changedTouches, as in the browser's touch event
ElementSelectEventselect*, and click from an XR selectinputSource, the controller or hand

All three also have element, camera (the camera the element was hit through) and event.

Event Bubbling​

An event is first fired on the element that was hit, then on its parent element, and so on up the hierarchy until it reaches an entity without an element. Ancestors receive the events whether or not they have input enabled, so one listener on a menu can handle the clicks on all of its items:

menu.element.on('click', (event) => {
console.log(`${event.element.entity.name} was clicked`);
});

Call event.stopPropagation() to stop an event going any further up.

Keeping UI Input from Reaching the Game​

Game code that reads the mouse or touch devices directly, for example to shoot when the player clicks, also sees clicks that land on the UI. stopPropagation() handles this as well. Besides stopping the bubbling, it stops the browser event itself, so browser listeners that would have run after the ElementInput's never receive it. The mouse and touch devices listen for the same browser events, so when the ElementInput is created before them, this keeps a press on the HUD away from app.mouse, including app.mouse.wasPressed():

// Presses on any input-enabled element in the HUD never reach app.mouse or app.touch
hud.element.on('mousedown', event => event.stopPropagation());
hud.element.on('touchstart', event => event.stopPropagation());

Leave input off on the HUD's own group element. Bubbling delivers its children's events to it anyway, and with input on, its whole rectangle would stop presses reaching the game.

The Editor, React and Web Components create the ElementInput first. In an Engine application, create it before the mouse and touch devices, as Setting Up does.

Which Element Gets the Event​

When elements overlap, only one receives an event. The ElementInput tests elements in this order and stops at the first hit:

  1. Cameras from the top down. Cameras are tried from the last one drawn to the first, so UI drawn on top wins. An element is only tested through a camera that renders one of its layers.
  2. Layers from the top down. Elements on a layer that is drawn later are tried first. This only matters when an interface uses more than one layer.
  3. Screen-space elements first, then elements on world-space screens, then elements with no screen.
  4. The element drawn on top first. Within each of those groups, the element with the highest draw order is tried first, which is usually the one lowest in the hierarchy. See Draw Order and Performance.

An element on a screen that is hit ends the search, even when it is farther from the camera than another hit. On overlapping world-space screens, the screen with the higher priority wins. Only elements without a screen are compared by distance, and the nearest wins.

The area that is tested is the element's rectangle, not the visible pixels of its image or the shapes of its glyphs. A button's hit padding grows it, and a mask clips it. Disabled entities are skipped.

Clicks and Dragging​

A click fires when the mouse button is released, or the touch ends, over the same element it was pressed on. Moving off the element and back before releasing still counts.

Once a mouse button is pressed on an element, that element receives every mousemove and the mouseup, wherever the pointer goes. A touch works the same way: touchmove and touchend go to the element the touch started on. This is what lets a slider or a scroll view keep dragging when the pointer leaves it.

Touch Screens​

After a tap, browsers send the page emulated mouse events, for pages that only handle the mouse. The ElementInput ignores the emulated click that follows a touch click on the same element, but the other emulated events still arrive: a tapped element also receives mouseenter, mousedown and mouseup. And if a tap hides the element it landed on, for example a button that closes its dialog, the emulated click lands on whatever was behind it. To stop the browser emulating mouse events, cancel the canvas's touchend events. This works on every surface:

app.graphicsDevice.canvas.addEventListener('touchend', (event) => {
event.preventDefault();
});

The ElementInput also cancels every touchmove over the canvas, so a touch that starts on the canvas does not scroll the page. Keep that in mind when the canvas is part of a longer page.

Pointer Lock​

While the pointer is locked, as in a first-person game, the ElementInput ignores mouse presses. Release the lock with app.mouse.disablePointerLock() before you show a menu, and lock it again when the menu closes.

Disabling UI Input​

  • To stop one element receiving input, turn useInput off. Disabled entities never receive input.
  • To leave a button visible but unresponsive, set its active property to false. See Buttons.
  • To pause all UI input, for example while a menu animates away, set app.elementInput.enabled to false. This property is not yet in the API reference.

See Also​