In the previous blog posts we discussed containers and buttons, which are rather static components. Once they’re rendered, they can change their state, but that’s limited to background color and borders. Those components are unable to accept input from a player (besides reacting to mouse cursor movements), so are quite simple in implementation. TextInput
however is a completely different story - it must react to all the same events as Container
or Button
, but additionally it must be aware of a keyboard input. Let’s start working on implementation.
Keyboard events are a little bit different than mouse events in terms of triggers and listeners. While mouse cursor interacts directly with rendered components (via Canvas
element), keyboard events are integrated with specific HTML elements:
Keyboard events are only generated by <input>, <textarea>, <summary> and anything with the contentEditable or tabindex attribute. If not caught, they bubble up the DOM tree until they reach Document.
You can read the whole reference here.
We’ll leverage that behavior by introducing a new registration options for listeners in Renderer
:
registerDocumentCallback(e, callback) {
document.addEventListener(e, callback);
}
Now we’re able to register event lister for our scene:
uiRenderer.registerDocumentCallback('keydown', (e) => {
mainMenu.onKeyDown(e);
});
From now on, keydown
event will be propagated for the whole UI components tree:
/**
* @description Handles the key down event.
* @date 1/1/2024 - 6:42:39 PM
*
* @param {*} event
*/
onKeyDown(event) {
if (this.#components.length > 0) {
this.#components.forEach(component => component.onKeyDown(event));
}
}
Let’s try to handle that event properly.
keydown
eventAs each UI component in Warp inherits from `UIObject`` class, we’ll introduce a default implementation of an event handler:
/**
* @description Handles the key down event. This is a base implementation and acts as a fallback.
* @date 1/2/2024 - 2:04:56 PM
*
* @param {*} event
*/
onKeyDown(event) {
if (this.isHidden === false) {
if (typeof this.children !== 'undefined' && this.children.length > 0) {
this.children.forEach(component => component.onKeyDown(event));
}
}
}
This already secures us from errors caused by missing implementation of onKeyDown(event)
method. This is however an implementation, which does literally nothing - we need to start handling the event accordingly to an input type.
InputObject
classWarp introduces an InputObject
class, which acts as a base class for all UI components, which accept input (it could be checbox, radio button, textarea and so on). This class defines one important property - a value stored by a component:
/**
* @description Returns the value of the input object
* @date 1/1/2024 - 6:20:08 PM
*
* @readonly
* @type {*}
*/
get value() {
return this.#value;
}
/**
* @description Sets the value of the input object
*/
set value(value) {
this.__notifyPropChanged('value', this.#value, value);
this.#value = value;
this.markDirty();
}
We’ll use it in TextInput
component to store text entered by a player.
TextInput
componentAs everything is set, we can start implementation of TextInput
. The most important thing is handling keydown
event. Here’s how it’s done currently:
onKeyDown(event) {
switch (event.key) {
case 'Backspace':
if (this.value.length > 0) {
this.value = this.value.substring(0, this.value.length - 1);
}
break;
case 'Enter':
case 'Shift':
case 'Control':
case 'Alt':
case 'CapsLock':
case 'Tab':
case 'ArrowLeft':
case 'ArrowRight':
case 'ArrowUp':
case 'ArrowDown':
break;
case 'Escape':
this.isActive = false;
break;
default:
this.value += event.key;
break;
}
}
As you can see, we’re ignoring an input for most of the special keys (like arrows, Alt, Control), have a dedicated logic for Backspace (so we can remove characters from the stored value) and Escape (to remove focus from component) and, by default, append an input to the stored value. Second important thing is rendering a component with stored text:
render(context) {
super.render(context);
if (this.canRender()) {
let text = this.value;
if (this.isActive === true) {
text += '_';
}
context.font = this.#font;
const textPos = this.#getTextPosition(context, text);
context.fillStyle = this.textColor;
context.fillText(text, textPos.x, textPos.y);
}
this.isDirty = false;
}
Let’s check the result!
When added into our scene, the TextInput
component may look like this:
There’re still a few things missing:
TextInput
doesn’t loose focus when a player clicks outside of a componentTextInput
doesn’t respect max length of a textThose are however quite easy things to implement - we’ll get there in the next blog post.