Skip to content

UI Interaction

Interactable

Marks a UI entity as interactive (receives pointer events). Required for all interactive widgets.

PropertyTypeDefaultDescription
enabledbooleantrueWhether interaction is active
blockRaycastbooleantrueBlock events from reaching elements behind
raycastTargetbooleantrueWhether this entity is a valid raycast hit target

UIInteraction

Automatically added by the plugin to entities with Interactable. Provides per-entity state:

PropertyTypeDescription
hoveredbooleanPointer is over this entity
pressedbooleanEntity is being pressed
justPressedbooleanPress started this frame
justReleasedbooleanPress released this frame

UIEvents

The UIEvents resource collects interaction events each frame:

import { defineSystem, addSystem, Res } from 'esengine';
import { UIEvents } from 'esengine';
addSystem(defineSystem(
[Res(UIEvents)],
(events) => {
for (const e of events.query('click')) {
console.log('Clicked:', e.entity);
}
}
));

Event Types

TypeDescriptionBubblesTriggered By
clickPointer released on same entity where pressedYesUIInteraction
pressPointer button pressed downYesUIInteraction
releasePointer button releasedYesUIInteraction
hover_enterPointer entered entity boundsNoUIInteraction
hover_exitPointer left entity boundsNoUIInteraction
focusElement received keyboard focus (click or Tab)NoFocusable
blurElement lost keyboard focusNoFocusable
submitEnter key pressed on single-line TextInputNoTextInput
changeValue changed on Slider, Toggle, TextInput, or DropdownNoWidget-specific
drag_startPointer moved beyond drag thresholdNoDraggable
drag_movePointer moved while draggingNoDraggable
drag_endPointer released after dragNoDraggable
scrollScrollView content scrolledNoScrollView

UIEvent Structure

Each event object contains:

PropertyTypeDescription
entityEntityThe entity that received the event
typestringEvent type (see table above)
targetEntityOriginal entity that triggered the event
currentTargetEntityEntity currently handling the event during bubbling

Querying Events

// Query all events of a specific type
for (const e of events.query('click')) {
console.log('Clicked:', e.entity);
}
// Check if a specific entity has a specific event
if (events.hasEvent(buttonEntity, 'click')) {
handleButton();
}
// Drain all events (returns and clears the queue)
const all = events.drain();

Callback Subscriptions

Instead of polling events each frame, you can subscribe to events with callbacks using .on(). This is ideal for one-off setup in startup systems:

import { defineSystem, addStartupSystem, Res, GetWorld } from 'esengine';
import { UIEvents, makeInteractable } from 'esengine';
addStartupSystem(defineSystem(
[Res(UIEvents), GetWorld()],
(events, world) => {
const button = findEntityByName(world, 'StartButton');
// Entity-scoped: only fires for this entity
const unsub = events.on(button, 'click', (e) => {
console.log('Start button clicked!');
});
// Global: fires for any entity with this event type
events.on('change', (e) => {
console.log('Value changed on:', e.entity);
});
// Call unsub() to remove the listener
}
));
OverloadDescription
events.on(entity, type, handler)Subscribe to events on a specific entity. Returns Unsubscribe function
events.on(type, handler)Subscribe to all events of a type globally. Returns Unsubscribe function

makeInteractable

Helper function to ensure an entity has the Interactable component with sensible defaults:

import { makeInteractable } from 'esengine';
makeInteractable(world, entity);
// Equivalent to adding Interactable with { enabled: true, blockRaycast: true, raycastTarget: true }

Use this when creating interactive entities from code — it avoids manually importing and inserting the Interactable component.

Event Bubbling

press, release, and click events bubble up the entity hierarchy. If a parent has Interactable with blockRaycast: true, bubbling stops there. hover_enter and hover_exit do not bubble.

Focusable

Enables keyboard focus and Tab navigation.

PropertyTypeDefaultDescription
tabIndexnumber0Focus order (lower = earlier)
isFocusedbooleanfalseCurrent focus state

Draggable

Enables drag-to-move on a UI element.

PropertyTypeDefaultDescription
enabledbooleantrueEnable dragging
dragThresholdnumber5Pixels before drag starts
lockXbooleanfalseLock horizontal movement
lockYbooleanfalseLock vertical movement
constraintMin{x, y} | nullnullMinimum position constraint
constraintMax{x, y} | nullnullMaximum position constraint

Draggable supports both Transform-based and UIRect-based (anchored) elements. When an entity has UIRect, dragging adjusts the layout offsets; otherwise it moves the Transform position directly.

When dragging is active, the engine adds a DragState component to the entity:

PropertyTypeDescription
isDraggingbooleanCurrently dragging
startWorldPos{x, y}Entity position when drag started
pointerStartWorld{x, y}Pointer position in world space when drag started
currentWorldPos{x, y}Current world position
deltaWorld{x, y}Movement this frame
totalDeltaWorld{x, y}Total movement since drag start

Default Tinting

When a Button or Toggle has no explicit transition, the UI system applies automatic state-based tinting to the entity’s Sprite or Image color:

StateEffect
NormalNo change
Hovered1.15x brightness
Pressed0.75x brightness
Disabled0.5x brightness, 0.6 alpha

This provides visual feedback without requiring explicit ColorTransition definitions.

Processing Pipeline

UI interaction systems run in a fixed order each frame:

ScheduleSystemPurpose
Last (prev frame)InputPluginClears one-frame input state
PreUpdateUIInteractionSystemHit test, hover, press, release, click
PreUpdateDragPluginDrag state tracking, drag events
UpdateButtonSystem, TogglePlugin, SliderPlugin, etc.Widget state updates

Next Steps