Files
GDevelop/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts

392 lines
12 KiB
TypeScript

namespace gdjs {
const userFriendlyToHtmlInputTypes = {
text: 'text',
email: 'email',
password: 'password',
number: 'number',
'telephone number': 'tel',
url: 'url',
search: 'search',
};
const formatRgbAndOpacityToCssRgba = (
rgbColor: [float, float, float],
opacity: float
) => {
return (
'rgba(' +
rgbColor[0] +
',' +
rgbColor[1] +
',' +
rgbColor[2] +
',' +
opacity / 255 +
')'
);
};
class TextInputRuntimeObjectPixiRenderer {
private _object: gdjs.TextInputRuntimeObject;
private _input: HTMLInputElement | HTMLTextAreaElement | null = null;
private _instanceContainer: gdjs.RuntimeInstanceContainer;
private _runtimeGame: gdjs.RuntimeGame;
private _form: HTMLFormElement | null = null;
constructor(
runtimeObject: gdjs.TextInputRuntimeObject,
instanceContainer: gdjs.RuntimeInstanceContainer
) {
this._object = runtimeObject;
this._instanceContainer = instanceContainer;
this._runtimeGame = this._instanceContainer.getGame();
this._createElement();
}
_createElement() {
if (!!this._input)
throw new Error('Tried to recreate an input while it already exists.');
this._form = document.createElement('form');
const isTextArea = this._object.getInputType() === 'text area';
this._input = document.createElement(isTextArea ? 'textarea' : 'input');
this._form.style.border = '0px';
this._form.style.borderRadius = '0px';
this._form.style.backgroundColor = 'transparent';
this._form.style.position = 'absolute';
this._form.style.pointerEvents = 'auto'; // Element can be clicked/touched.
this._form.style.display = 'none'; // Hide while object is being set up.
this._form.style.boxSizing = 'border-box';
this._form.style.textAlign = this._object.getTextAlign();
this._input.autocomplete = 'off';
this._input.style.backgroundColor = 'white';
this._input.style.outline = 'none'; // Remove any style added by the browser to highlight the focused field in a form (:focus & :focus-visible modifiers).
this._input.style.resize = 'none'; // Prevent user from resizing the input when it's a text area.
this._input.style.border = '1px solid black';
this._input.style.boxSizing = 'border-box';
this._input.style.borderRadius = '0px'; // Remove any style added by the browser (Safari adds some radius for instance).
this._input.style.width = '100%';
this._input.style.height = '100%';
this._input.maxLength = this._object.getMaxLength();
this._input.style.padding = `${this._object
.getPaddingY()
.toFixed(2)}px ${this._object.getPaddingX().toFixed(2)}px`;
this._form.appendChild(this._input);
this._input.addEventListener('input', () => {
if (!this._input) return;
this._object.onRendererInputValueChanged(this._input.value);
});
this._input.addEventListener('touchstart', () => {
if (!this._input) return;
// Focus directly when touching the input on touchscreens.
if (document.activeElement !== this._input) this._input.focus();
});
this._form.addEventListener('submit', (event) => {
event.preventDefault();
this._object.onRendererFormSubmitted();
});
this.updateString();
this.updateFont();
this.updatePlaceholder();
this.updateOpacity();
this.updateInputType();
this.updateTextColor();
this.updateFillColorAndOpacity();
this.updateBorderColorAndOpacity();
this.updateBorderWidth();
this.updateDisabled();
this.updateReadOnly();
this.updateSpellCheck();
this.updateTextAlign();
this.updateMaxLength();
this.updatePadding();
this._runtimeGame
.getRenderer()
.getDomElementContainer()!
.appendChild(this._form);
}
_destroyElement() {
if (this._form) {
this._form.remove();
this._form = null;
}
if (this._input) {
this._input.remove();
this._input = null;
}
}
onScenePaused() {
// This is the only renderer that uses a DOM element. PixiJS renderers
// usually don't need to know if a scene is paused/resumed,
// because their renderers are then not used (not part of the other scene graphs).
// For this object, we need to remove the DOM element whenever it must
// be not rendered.
this._destroyElement();
}
onSceneResumed() {
// The input must have been destroyed when the scene was paused,
// in case it still exists, skip recreation.
if (!this._input) this._createElement();
}
onDestroy() {
this._destroyElement();
}
updatePreRender() {
if (!this._input || !this._form) return;
// Hide the input entirely if the object is hidden.
// Because this object is rendered as a DOM element (and not part of the PixiJS
// scene graph), we have to do this manually.
if (this._object.isHidden()) {
this._form.style.display = 'none';
return;
}
// Hide the input entirely if the layer is not visible.
// Because this object is rendered as a DOM element (and not part of the PixiJS
// scene graph), we have to do this manually.
{
let instanceContainer = this._instanceContainer;
let object: gdjs.RuntimeObject = this._object;
let hasParent = true;
do {
const layer = instanceContainer.getLayer(object.getLayer());
if (!layer.isVisible() || !object.isVisible()) {
this._form.style.display = 'none';
return;
}
// TODO Declare an interface to move up in the object tree.
if (
instanceContainer instanceof
gdjs.CustomRuntimeObjectInstanceContainer
) {
object = instanceContainer.getOwner();
instanceContainer = object.getInstanceContainer();
} else {
hasParent = false;
}
} while (hasParent);
}
const workingPoint: FloatPoint = gdjs.staticArray(
TextInputRuntimeObjectPixiRenderer.prototype.updatePreRender
) as FloatPoint;
const runtimeGame = this._instanceContainer.getGame();
const runtimeGameRenderer = runtimeGame.getRenderer();
const layer = this._instanceContainer.getLayer(this._object.getLayer());
const topLeftCanvasCoordinates = layer.convertInverseCoords(
this._object.x,
this._object.y,
0,
workingPoint
);
const canvasLeft = topLeftCanvasCoordinates[0];
const canvasTop = topLeftCanvasCoordinates[1];
const bottomRightCanvasCoordinates = layer.convertInverseCoords(
this._object.x + this._object.getWidth(),
this._object.y + this._object.getHeight(),
0,
workingPoint
);
const canvasRight = bottomRightCanvasCoordinates[0];
const canvasBottom = bottomRightCanvasCoordinates[1];
// Hide the input entirely if not visible at all.
const isOutsideCanvas =
canvasRight < 0 ||
canvasBottom < 0 ||
canvasLeft > runtimeGame.getGameResolutionWidth() ||
canvasTop > runtimeGame.getGameResolutionHeight();
if (isOutsideCanvas) {
this._form.style.display = 'none';
return;
}
// Position the input on the container on top of the canvas.
workingPoint[0] = canvasLeft;
workingPoint[1] = canvasTop;
runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
workingPoint,
workingPoint
);
const pageLeft = workingPoint[0];
const pageTop = workingPoint[1];
workingPoint[0] = canvasRight;
workingPoint[1] = canvasBottom;
runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
workingPoint,
workingPoint
);
const pageRight = workingPoint[0];
const pageBottom = workingPoint[1];
const widthInContainer = pageRight - pageLeft;
const heightInContainer = pageBottom - pageTop;
this._form.style.left = pageLeft + 'px';
this._form.style.top = pageTop + 'px';
this._form.style.width = widthInContainer + 'px';
this._form.style.height = heightInContainer + 'px';
this._form.style.transform =
'rotate3d(0,0,1,' + (this._object.getAngle() % 360) + 'deg)';
this._form.style.textAlign = this._object.getTextAlign();
this._input.style.padding = `${this._object
.getPaddingY()
.toFixed(2)}px ${this._object.getPaddingX().toFixed(2)}px`;
// Automatically adjust the font size to follow the game scale.
this._input.style.fontSize =
this._object.getFontSize() *
runtimeGameRenderer.getCanvasToDomElementContainerHeightScale() +
'px';
// Display after the object is positioned.
this._form.style.display = 'initial';
}
updateString() {
if (!this._input) return;
this._input.value = this._object.getString();
}
updatePlaceholder() {
if (!this._input) return;
this._input.placeholder = this._object.getPlaceholder();
}
updateFont() {
if (!this._input) return;
this._input.style.fontFamily = this._instanceContainer
.getGame()
.getFontManager()
.getFontFamily(this._object.getFontResourceName());
}
updateOpacity() {
if (!this._form) return;
this._form.style.opacity = (this._object.getOpacity() / 255).toFixed(3);
}
updateInputType() {
if (!this._input) return;
const isTextArea = this._input instanceof HTMLTextAreaElement;
const shouldBeTextArea = this._object.getInputType() === 'text area';
if (isTextArea !== shouldBeTextArea) {
this._destroyElement();
this._createElement();
}
const newType =
userFriendlyToHtmlInputTypes[this._object.getInputType()] || 'text';
this._input.setAttribute('type', newType);
}
updateTextColor() {
if (!this._input) return;
this._input.style.color = formatRgbAndOpacityToCssRgba(
this._object._getRawTextColor(),
255
);
}
updateFillColorAndOpacity() {
if (!this._input) return;
this._input.style.backgroundColor = formatRgbAndOpacityToCssRgba(
this._object._getRawFillColor(),
this._object.getFillOpacity()
);
}
updateBorderColorAndOpacity() {
if (!this._input) return;
this._input.style.borderColor = formatRgbAndOpacityToCssRgba(
this._object._getRawBorderColor(),
this._object.getBorderOpacity()
);
}
updateBorderWidth() {
if (!this._input) return;
this._input.style.borderWidth = this._object.getBorderWidth() + 'px';
}
updateDisabled() {
if (!this._input) return;
this._input.disabled = this._object.isDisabled();
}
updateReadOnly() {
if (!this._input) return;
this._input.readOnly = this._object.isReadOnly();
}
updateSpellCheck() {
if (!this._input) return;
this._input.spellcheck = this._object.isSpellCheckEnabled();
}
updateMaxLength() {
const input = this._input;
if (!input) return;
if (this._object.getMaxLength() <= 0) {
input.removeAttribute('maxLength');
return;
}
input.maxLength = this._object.getMaxLength();
}
updatePadding() {
if (!this._input) return;
this._input.style.padding = `${this._object
.getPaddingY()
.toFixed(2)}px ${this._object.getPaddingX().toFixed(2)}px`;
}
updateTextAlign() {
if (!this._input) return;
const newTextAlign = this._object.getTextAlign();
this._input.style.textAlign = newTextAlign;
}
isFocused() {
return this._input === document.activeElement;
}
focus() {
if (!this._input) return;
this._input.focus();
}
}
export const TextInputRuntimeObjectRenderer =
TextInputRuntimeObjectPixiRenderer;
export type TextInputRuntimeObjectRenderer =
TextInputRuntimeObjectPixiRenderer;
}