Creating TextInput - Part1

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.

Catching keyboard input

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.

Handling keydown event

As 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 class

Warp 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.

Implementing TextInput component

As 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!

Quick demo

When added into our scene, the TextInput component may look like this: textinput_demo_1 There’re still a few things missing:

  • TextInput doesn’t loose focus when a player clicks outside of a component
  • Position of a text changes when writing due to invalid baseline
  • We cannot adjust text color for active / non-active component
  • TextInput doesn’t respect max length of a text

Those are however quite easy things to implement - we’ll get there in the next blog post.