UNPKG

@babylonjs/gui

Version:

Babylon.js GUI module =====================

1,025 lines 49.3 kB
import { __decorate } from "@babylonjs/core/tslib.es6.js"; import { Observable } from "@babylonjs/core/Misc/observable.js"; import { Control } from "./control.js"; import { ValueAndUnit } from "../valueAndUnit.js"; import { RegisterClass } from "@babylonjs/core/Misc/typeStore.js"; import { InputText } from "./inputText.js"; import { serialize } from "@babylonjs/core/Misc/decorators.js"; /** * Class used to create input text control */ export class InputTextArea extends InputText { /** Gets or sets a boolean indicating if the control can auto stretch its height to adapt to the text */ get autoStretchHeight() { return this._autoStretchHeight; } set autoStretchHeight(value) { if (this._autoStretchHeight === value) { return; } this._autoStretchHeight = value; this._markAsDirty(); } set height(value) { this.fixedRatioMasterIsWidth = false; if (this._height.toString(this._host) === value) { return; } if (this._height.fromString(value)) { this._markAsDirty(); } this._autoStretchHeight = false; } get maxHeight() { return this._maxHeight.toString(this._host); } /** Gets the maximum width allowed by the control in pixels */ get maxHeightInPixels() { return this._maxHeight.getValueInPixel(this._host, this._cachedParentMeasure.height); } set maxHeight(value) { if (this._maxHeight.toString(this._host) === value) { return; } if (this._maxHeight.fromString(value)) { this._markAsDirty(); } } /** * Creates a new InputTextArea * @param name defines the control name * @param text defines the text of the control */ constructor(name, text = "") { super(name); this.name = name; this._textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; this._textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; this._prevText = this.text; this._lineSpacing = new ValueAndUnit(0); this._maxHeight = new ValueAndUnit(1, ValueAndUnit.UNITMODE_PERCENTAGE, false); /** * An event triggered after the text was broken up into lines */ this.onLinesReadyObservable = new Observable(); this.text = text; this.isPointerBlocker = true; this.onLinesReadyObservable.add(() => this._updateCursorPosition()); this._highlightCursorInfo = { initialStartIndex: -1, initialRelativeStartIndex: -1, initialLineIndex: -1, }; this._cursorInfo = { globalStartIndex: 0, globalEndIndex: 0, relativeEndIndex: 0, relativeStartIndex: 0, currentLineIndex: 0, }; } _getTypeName() { return "InputTextArea"; } /** * Handles the keyboard event * @param evt Defines the KeyboardEvent */ processKeyboard(evt) { if (this.isReadOnly) { return; } // process pressed key this.alternativeProcessKey(evt.code, evt.key, evt); this.onKeyboardEventProcessedObservable.notifyObservers(evt); } /** * Process the last keyboard input * * @param code The ascii input number * @param key The key string representation * @param evt The keyboard event emits with input * @internal */ alternativeProcessKey(code, key, evt) { //return if clipboard event keys (i.e -ctr/cmd + c,v,x) if (evt && (evt.ctrlKey || evt.metaKey) && (key === "c" || key === "v" || key === "x")) { return; } // Specific cases switch (code) { case "Period": //SLASH if (evt && evt.shiftKey) { evt.preventDefault(); } break; case "Backspace": // BACKSPACE if (!this._isTextHighlightOn && this._cursorInfo.globalStartIndex > 0) { this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._cursorInfo.globalStartIndex--; } this._prevText = this._textWrapper.text; this._textWrapper.removePart(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex); this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; if (evt) { evt.preventDefault(); } this._blinkIsEven = false; this._isTextHighlightOn = false; this._textHasChanged(); break; case "Delete": // DELETE if (!this._isTextHighlightOn && this._cursorInfo.globalEndIndex < this.text.length) { this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex + 1; } this._prevText = this._textWrapper.text; this._textWrapper.removePart(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex); this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; if (evt) { evt.preventDefault(); } this._blinkIsEven = false; this._isTextHighlightOn = false; this._textHasChanged(); break; case "NumpadEnter": // NUMPAD ENTER case "Enter": // RETURN this._prevText = this._textWrapper.text; this._textWrapper.removePart(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex, "\n"); this._cursorInfo.globalStartIndex++; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._blinkIsEven = false; this._isTextHighlightOn = false; this._textHasChanged(); return; case "End": // END this._cursorInfo.globalStartIndex = this.text.length; this._blinkIsEven = false; this._isTextHighlightOn = false; this._markAsDirty(); return; case "Home": // HOME this._cursorInfo.globalStartIndex = 0; this._blinkIsEven = false; this._isTextHighlightOn = false; this._markAsDirty(); return; case "ArrowLeft": // LEFT this._markAsDirty(); if (evt && evt.shiftKey) { // shift + ctrl/cmd + <- if (evt.ctrlKey || evt.metaKey) { // Go to line's start by substract the relativeStartIndex to the globalStartIndex this._cursorInfo.globalStartIndex -= this._cursorInfo.relativeStartIndex; this._cursorInfo.globalEndIndex = this._highlightCursorInfo.initialStartIndex; } // store the starting point if (!this._isTextHighlightOn) { this._highlightCursorInfo.initialLineIndex = this._cursorInfo.currentLineIndex; this._highlightCursorInfo.initialStartIndex = this._cursorInfo.globalStartIndex; this._highlightCursorInfo.initialRelativeStartIndex = this._cursorInfo.relativeStartIndex; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._cursorInfo.globalStartIndex--; this._isTextHighlightOn = true; } else { if (this._cursorInfo.globalEndIndex > this._highlightCursorInfo.initialStartIndex) { this._cursorInfo.globalEndIndex--; } else { this._cursorInfo.globalStartIndex--; } } this._blinkIsEven = true; evt.preventDefault(); return; } if (this._isTextHighlightOn) { this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; } else if (evt && (evt.ctrlKey || evt.metaKey)) { // ctr + <- this._cursorInfo.globalStartIndex -= this._cursorInfo.relativeStartIndex; evt.preventDefault(); } else if (this._cursorInfo.globalStartIndex > 0) { this._cursorInfo.globalStartIndex--; } // update the cursor this._blinkIsEven = false; this._isTextHighlightOn = false; return; case "ArrowRight": // RIGHT this._markAsDirty(); if (evt && evt.shiftKey) { // shift + ctrl/cmd + -> if (evt.ctrlKey || evt.metaKey) { const rightDelta = this._lines[this._cursorInfo.currentLineIndex].text.length - this._cursorInfo.relativeEndIndex - 1; this._cursorInfo.globalEndIndex += rightDelta; this._cursorInfo.globalStartIndex = this._highlightCursorInfo.initialStartIndex; } // store the starting point if (!this._isTextHighlightOn) { this._highlightCursorInfo.initialLineIndex = this._cursorInfo.currentLineIndex; this._highlightCursorInfo.initialStartIndex = this._cursorInfo.globalStartIndex; this._highlightCursorInfo.initialRelativeStartIndex = this._cursorInfo.relativeStartIndex; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._cursorInfo.globalEndIndex++; this._isTextHighlightOn = true; } else { if (this._cursorInfo.globalStartIndex < this._highlightCursorInfo.initialStartIndex) { this._cursorInfo.globalStartIndex++; } else { this._cursorInfo.globalEndIndex++; } } this._blinkIsEven = true; evt.preventDefault(); return; } if (this._isTextHighlightOn) { this._cursorInfo.globalStartIndex = this._cursorInfo.globalEndIndex; } else if (evt && (evt.ctrlKey || evt.metaKey)) { //ctr + -> const rightDelta = this._lines[this._cursorInfo.currentLineIndex].text.length - this._cursorInfo.relativeEndIndex; this._cursorInfo.globalStartIndex += rightDelta; } else if (this._cursorInfo.globalStartIndex < this.text.length) { this._cursorInfo.globalStartIndex++; } // update the cursor this._blinkIsEven = false; this._isTextHighlightOn = false; return; case "ArrowUp": // UP // update the cursor this._blinkIsEven = false; if (evt) { if (evt.shiftKey) { if (!this._isTextHighlightOn) { this._highlightCursorInfo.initialLineIndex = this._cursorInfo.currentLineIndex; this._highlightCursorInfo.initialStartIndex = this._cursorInfo.globalStartIndex; this._highlightCursorInfo.initialRelativeStartIndex = this._cursorInfo.relativeStartIndex; } this._isTextHighlightOn = true; this._blinkIsEven = true; } else { this._isTextHighlightOn = false; } evt.preventDefault(); } if (this._cursorInfo.currentLineIndex === 0) { // First line this._cursorInfo.globalStartIndex = 0; } else { const currentLine = this._lines[this._cursorInfo.currentLineIndex]; const upperLine = this._lines[this._cursorInfo.currentLineIndex - 1]; let tmpIndex = 0; let relativeIndex = 0; if (!this._isTextHighlightOn || this._cursorInfo.currentLineIndex < this._highlightCursorInfo.initialLineIndex) { tmpIndex = this._cursorInfo.globalStartIndex; relativeIndex = this._cursorInfo.relativeStartIndex; } else { tmpIndex = this._cursorInfo.globalEndIndex; relativeIndex = this._cursorInfo.relativeEndIndex; } const currentText = currentLine.text.substring(0, relativeIndex); const currentWidth = this._contextForBreakLines.measureText(currentText).width; let upperWidth = 0; let previousWidth = 0; tmpIndex -= relativeIndex; // Start of current line tmpIndex -= upperLine.text.length + upperLine.lineEnding.length; // Start of upper line let upperLineRelativeIndex = 0; while (upperWidth < currentWidth && upperLineRelativeIndex < upperLine.text.length) { tmpIndex++; upperLineRelativeIndex++; previousWidth = Math.abs(currentWidth - upperWidth); upperWidth = this._contextForBreakLines.measureText(upperLine.text.substring(0, upperLineRelativeIndex)).width; } // Find closest move if (Math.abs(currentWidth - upperWidth) > previousWidth && upperLineRelativeIndex > 0) { tmpIndex--; } if (!this._isTextHighlightOn) { this._cursorInfo.globalStartIndex = tmpIndex; } else if (this._cursorInfo.currentLineIndex <= this._highlightCursorInfo.initialLineIndex) { this._cursorInfo.globalStartIndex = tmpIndex; this._cursorInfo.globalEndIndex = this._highlightCursorInfo.initialStartIndex; this._cursorInfo.relativeEndIndex = this._highlightCursorInfo.initialRelativeStartIndex; } else { this._cursorInfo.globalEndIndex = tmpIndex; } } this._markAsDirty(); return; case "ArrowDown": // DOWN // update the cursor this._blinkIsEven = false; if (evt) { if (evt.shiftKey) { if (!this._isTextHighlightOn) { this._highlightCursorInfo.initialLineIndex = this._cursorInfo.currentLineIndex; this._highlightCursorInfo.initialStartIndex = this._cursorInfo.globalStartIndex; this._highlightCursorInfo.initialRelativeStartIndex = this._cursorInfo.relativeStartIndex; } this._isTextHighlightOn = true; this._blinkIsEven = true; } else { this._isTextHighlightOn = false; } evt.preventDefault(); } if (this._cursorInfo.currentLineIndex === this._lines.length - 1) { // Last line this._cursorInfo.globalStartIndex = this.text.length; } else { const currentLine = this._lines[this._cursorInfo.currentLineIndex]; const underLine = this._lines[this._cursorInfo.currentLineIndex + 1]; let tmpIndex = 0; let relativeIndex = 0; if (!this._isTextHighlightOn || this._cursorInfo.currentLineIndex < this._highlightCursorInfo.initialLineIndex) { tmpIndex = this._cursorInfo.globalStartIndex; relativeIndex = this._cursorInfo.relativeStartIndex; } else { tmpIndex = this._cursorInfo.globalEndIndex; relativeIndex = this._cursorInfo.relativeEndIndex; } const currentText = currentLine.text.substring(0, relativeIndex); const currentWidth = this._contextForBreakLines.measureText(currentText).width; let underWidth = 0; let previousWidth = 0; tmpIndex += currentLine.text.length - relativeIndex + currentLine.lineEnding.length; // Start of current line let underLineRelativeIndex = 0; while (underWidth < currentWidth && underLineRelativeIndex < underLine.text.length) { tmpIndex++; underLineRelativeIndex++; previousWidth = Math.abs(currentWidth - underWidth); underWidth = this._contextForBreakLines.measureText(underLine.text.substring(0, underLineRelativeIndex)).width; } // Find closest move if (Math.abs(currentWidth - underWidth) > previousWidth && underLineRelativeIndex > 0) { tmpIndex--; } if (!this._isTextHighlightOn) { this._cursorInfo.globalStartIndex = tmpIndex; } else if (this._cursorInfo.currentLineIndex < this._highlightCursorInfo.initialLineIndex) { this._cursorInfo.globalStartIndex = tmpIndex; if (this._cursorInfo.globalStartIndex > this._cursorInfo.globalEndIndex) { this._cursorInfo.globalEndIndex += this._cursorInfo.globalStartIndex; this._cursorInfo.globalStartIndex = this._cursorInfo.globalEndIndex - this._cursorInfo.globalStartIndex; this._cursorInfo.globalEndIndex -= this._cursorInfo.globalStartIndex; } } else { this._cursorInfo.globalEndIndex = tmpIndex; this._cursorInfo.globalStartIndex = this._highlightCursorInfo.initialStartIndex; } } this._markAsDirty(); return; } // special case - select all. Use key instead of code to support all keyboard layouts if (key === "a" && evt && (evt.ctrlKey || evt.metaKey)) { this.selectAllText(); evt.preventDefault(); return; } // Printable characters if (key?.length === 1) { evt?.preventDefault(); this._currentKey = key; this.onBeforeKeyAddObservable.notifyObservers(this); key = this._currentKey; if (this._addKey) { this._isTextHighlightOn = false; this._blinkIsEven = false; this._prevText = this._textWrapper.text; this._textWrapper.removePart(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex, key); this._cursorInfo.globalStartIndex += key.length; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._textHasChanged(); } } } _parseLineWordWrap(line = "", width, context) { const lines = []; const words = line.split(" "); let lineWidth = 0; for (let n = 0; n < words.length; n++) { const testLine = n > 0 ? line + " " + words[n] : words[0]; const metrics = context.measureText(testLine); const testWidth = metrics.width; if (testWidth > width) { if (n > 0) { // Avoid first word duplication if of too long lineWidth = context.measureText(line).width; lines.push({ text: line, width: lineWidth, lineEnding: " " }); } line = words[n]; let flushedLine = ""; line.split("").map((char) => { if (context.measureText(flushedLine + char).width > width) { lines.push({ text: flushedLine, width: context.measureText(flushedLine).width, lineEnding: "" }); flushedLine = ""; } flushedLine += char; }); line = flushedLine; // Measure remaining characters lineWidth = context.measureText(line).width; } else { lineWidth = testWidth; line = testLine; } } lines.push({ text: line, width: lineWidth, lineEnding: " " }); return lines; } _breakLines(refWidth, context) { const lines = []; const _lines = (this.text || this.placeholderText).split("\n"); if (this.clipContent) { for (const _line of _lines) { lines.push(...this._parseLineWordWrap(_line, refWidth, context)); } } else { for (const _line of _lines) { lines.push(this._parseLine(_line, context)); } } lines[lines.length - 1].lineEnding = "\n"; return lines; } _parseLine(line = "", context) { return { text: line, width: context.measureText(line).width, lineEnding: " " }; } /** * Processing of child right before the parent measurement update * * @param parentMeasure The parent measure * @param context The rendering canvas * @internal */ _preMeasure(parentMeasure, context) { if (!this._fontOffset || this._wasDirty) { this._fontOffset = Control._GetFontOffset(context.font, this._host.getScene()?.getEngine()); } let text = this._beforeRenderText(this._textWrapper).text; // placeholder conditions and color setting if (!this.text && this._placeholderText) { text = this._placeholderText; } // measures the textlength -> this.measure.width this._textWidth = context.measureText(text).width; // we double up the margin width const marginWidth = this._margin.getValueInPixel(this._host, parentMeasure.width) * 2; if (this._autoStretchWidth) { const tmpLines = text.split("\n"); const longerString = tmpLines.reduce((acc, val) => { const valueLength = context.measureText(val).width; const accLength = context.measureText(acc).width; return valueLength > accLength ? val : acc; }, ""); const longerStringWidth = context.measureText(longerString).width; this.width = Math.min(this._maxWidth.getValueInPixel(this._host, parentMeasure.width), longerStringWidth + marginWidth) + "px"; this.autoStretchWidth = true; } this._availableWidth = this._width.getValueInPixel(this._host, parentMeasure.width) - marginWidth; // Prepare lines this._lines = this._breakLines(this._availableWidth, context); // can we find a cleaner implementation here? this._contextForBreakLines = context; if (this._autoStretchHeight) { const textHeight = this._lines.length * this._fontOffset.height; const totalHeight = textHeight + this._margin.getValueInPixel(this._host, parentMeasure.height) * 2; this.height = Math.min(this._maxHeight.getValueInPixel(this._host, parentMeasure.height), totalHeight) + "px"; this._autoStretchHeight = true; } this._availableHeight = this._height.getValueInPixel(this._host, parentMeasure.height) - marginWidth; if (this._isFocused) { this._cursorInfo.currentLineIndex = 0; let lineLength = this._lines[this._cursorInfo.currentLineIndex].text.length + this._lines[this._cursorInfo.currentLineIndex].lineEnding.length; let tmpLength = 0; while (tmpLength + lineLength <= this._cursorInfo.globalStartIndex) { tmpLength += lineLength; if (this._cursorInfo.currentLineIndex < this._lines.length - 1) { this._cursorInfo.currentLineIndex++; lineLength = this._lines[this._cursorInfo.currentLineIndex].text.length + this._lines[this._cursorInfo.currentLineIndex].lineEnding.length; } } } } _textHasChanged() { if (!this._prevText && this._textWrapper.text && this.placeholderText) { this._cursorInfo.currentLineIndex = 0; this._cursorInfo.globalStartIndex = 1; this._cursorInfo.globalEndIndex = 1; this._cursorInfo.relativeStartIndex = 1; this._cursorInfo.relativeEndIndex = 1; } super._textHasChanged(); } _computeScroll() { this._clipTextLeft = this._currentMeasure.left + this._margin.getValueInPixel(this._host, this._cachedParentMeasure.width); this._clipTextTop = this._currentMeasure.top + this._margin.getValueInPixel(this._host, this._cachedParentMeasure.height); if (this._isFocused && this._lines[this._cursorInfo.currentLineIndex].width > this._availableWidth) { const textLeft = this._clipTextLeft - this._lines[this._cursorInfo.currentLineIndex].width + this._availableWidth; if (!this._scrollLeft) { this._scrollLeft = textLeft; } } else { this._scrollLeft = this._clipTextLeft; } if (this._isFocused) { const selectedHeight = (this._cursorInfo.currentLineIndex + 1) * this._fontOffset.height; const textTop = this._clipTextTop - selectedHeight; if (!this._scrollTop) { this._scrollTop = textTop; } } else { this._scrollTop = this._clipTextTop; } } /** * Processing of child after the parent measurement update * * @internal */ _additionalProcessing() { // Flush the highlighted text each frame this.highlightedText = ""; this.onLinesReadyObservable.notifyObservers(this); } _drawText(text, textWidth, y, context) { const width = this._currentMeasure.width; let x = this._scrollLeft; switch (this._textHorizontalAlignment) { case Control.HORIZONTAL_ALIGNMENT_LEFT: x += 0; break; case Control.HORIZONTAL_ALIGNMENT_RIGHT: x += width - textWidth; break; case Control.HORIZONTAL_ALIGNMENT_CENTER: x += (width - textWidth) / 2; break; } if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) { context.shadowColor = this.shadowColor; context.shadowBlur = this.shadowBlur; context.shadowOffsetX = this.shadowOffsetX; context.shadowOffsetY = this.shadowOffsetY; } if (this.outlineWidth) { context.strokeText(text, this._currentMeasure.left + x, y); } context.fillText(text, x, y); } /** * Copy the text in the clipboard * * @param ev The clipboard event * @internal */ _onCopyText(ev) { this._isTextHighlightOn = false; //when write permission to clipbaord data is denied try { ev.clipboardData && ev.clipboardData.setData("text/plain", this._highlightedText); } catch { } //pass this._host.clipboardData = this._highlightedText; } /** * Cut the text and copy it in the clipboard * * @param ev The clipboard event * @internal */ _onCutText(ev) { if (!this._highlightedText) { return; } //when write permission to clipbaord data is denied try { ev.clipboardData && ev.clipboardData.setData("text/plain", this._highlightedText); } catch { } //pass this._host.clipboardData = this._highlightedText; this._prevText = this._textWrapper.text; this._textWrapper.removePart(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex); this._textHasChanged(); } /** * Paste the copied text from the clipboard * * @param ev The clipboard event * @internal */ _onPasteText(ev) { let data = ""; if (ev.clipboardData && ev.clipboardData.types.indexOf("text/plain") !== -1) { data = ev.clipboardData.getData("text/plain"); } else { //get the cached data; returns blank string by default data = this._host.clipboardData; } this._isTextHighlightOn = false; this._prevText = this._textWrapper.text; this._textWrapper.removePart(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex, data); const deltaIndex = data.length - (this._cursorInfo.globalEndIndex - this._cursorInfo.globalStartIndex); this._cursorInfo.globalStartIndex += deltaIndex; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._clickedCoordinateX = null; this._clickedCoordinateY = null; super._textHasChanged(); } _draw(context) { this._computeScroll(); this._scrollLeft = this._scrollLeft ?? 0; this._scrollTop = this._scrollTop ?? 0; context.save(); this._applyStates(context); if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) { context.shadowColor = this.shadowColor; context.shadowBlur = this.shadowBlur; context.shadowOffsetX = this.shadowOffsetX; context.shadowOffsetY = this.shadowOffsetY; } // Background if (this._isFocused) { if (this._focusedBackground) { context.fillStyle = this._isEnabled ? this._focusedBackground : this._disabledColor; context.fillRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height); } } else if (this._background) { context.fillStyle = this._isEnabled ? this._background : this._disabledColor; context.fillRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height); } if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) { context.shadowBlur = 0; context.shadowOffsetX = 0; context.shadowOffsetY = 0; } // sets the color of the rectangle (border if background available) if (this.color) { context.fillStyle = this.color; } const height = this._currentMeasure.height; const width = this._currentMeasure.width; let rootY = 0; switch (this._textVerticalAlignment) { case Control.VERTICAL_ALIGNMENT_TOP: rootY = this._fontOffset.ascent; break; case Control.VERTICAL_ALIGNMENT_BOTTOM: rootY = height - this._fontOffset.height * (this._lines.length - 1) - this._fontOffset.descent; break; case Control.VERTICAL_ALIGNMENT_CENTER: rootY = this._fontOffset.ascent + (height - this._fontOffset.height * this._lines.length) / 2; break; } context.save(); context.beginPath(); context.fillStyle = this.fontStyle; if (!this._textWrapper.text && this.placeholderText) { context.fillStyle = this._placeholderColor; } // here we define the visible reactangle to clip it in next line context.rect(this._clipTextLeft, this._clipTextTop, this._availableWidth + 2, this._availableHeight + 2); context.clip(); // Text rootY += this._scrollTop; for (let i = 0; i < this._lines.length; i++) { const line = this._lines[i]; if (i !== 0 && this._lineSpacing.internalValue !== 0) { if (this._lineSpacing.isPixel) { rootY += this._lineSpacing.getValue(this._host); } else { rootY = rootY + this._lineSpacing.getValue(this._host) * this._height.getValueInPixel(this._host, this._cachedParentMeasure.height); } } this._drawText(line.text, line.width, rootY, context); rootY += this._fontOffset.height; } context.restore(); // Cursor if (this._isFocused) { // Render cursor if (!this._blinkIsEven || this._isTextHighlightOn) { let cursorLeft = this._scrollLeft + context.measureText(this._lines[this._cursorInfo.currentLineIndex].text.substring(0, this._cursorInfo.relativeStartIndex)).width; if (cursorLeft < this._clipTextLeft) { this._scrollLeft += this._clipTextLeft - cursorLeft; cursorLeft = this._clipTextLeft; this._markAsDirty(); } else if (cursorLeft > this._clipTextLeft + this._availableWidth) { this._scrollLeft += this._clipTextLeft + this._availableWidth - cursorLeft; cursorLeft = this._clipTextLeft + this._availableWidth; this._markAsDirty(); } let cursorTop = this._scrollTop + this._cursorInfo.currentLineIndex * this._fontOffset.height; //cursorTop distance from top to cursor start if (cursorTop < this._clipTextTop) { this._scrollTop += this._clipTextTop - cursorTop; cursorTop = this._clipTextTop; this._markAsDirty(); } else if (cursorTop + this._fontOffset.height > this._clipTextTop + this._availableHeight && this._availableHeight > this._fontOffset.height) { this._scrollTop += this._clipTextTop + this._availableHeight - cursorTop - this._fontOffset.height; cursorTop = this._clipTextTop + this._availableHeight - this._fontOffset.height; this._markAsDirty(); } if (!this._isTextHighlightOn) { context.fillRect(cursorLeft, cursorTop, 2, this._fontOffset.height); } } this._resetBlinking(); //show the highlighted text if (this._isTextHighlightOn) { clearTimeout(this._blinkTimeout); this._highlightedText = this.text.substring(this._cursorInfo.globalStartIndex, this._cursorInfo.globalEndIndex); context.globalAlpha = this._highligherOpacity; context.fillStyle = this._textHighlightColor; const startLineIndex = Math.min(this._cursorInfo.currentLineIndex, this._highlightCursorInfo.initialLineIndex); const endLineIndex = Math.max(this._cursorInfo.currentLineIndex, this._highlightCursorInfo.initialLineIndex); let highlightRootY = this._scrollTop + startLineIndex * this._fontOffset.height; for (let i = startLineIndex; i <= endLineIndex; i++) { const line = this._lines[i]; let highlightRootX = this._scrollLeft; switch (this._textHorizontalAlignment) { case Control.HORIZONTAL_ALIGNMENT_LEFT: highlightRootX += 0; break; case Control.HORIZONTAL_ALIGNMENT_RIGHT: highlightRootX += width - line.width; break; case Control.HORIZONTAL_ALIGNMENT_CENTER: highlightRootX += (width - line.width) / 2; break; } const begin = i === startLineIndex ? this._cursorInfo.relativeStartIndex : 0; const end = i === endLineIndex ? this._cursorInfo.relativeEndIndex : line.text.length; const leftOffsetWidth = context.measureText(line.text.substring(0, begin)).width; const selectedText = line.text.substring(begin, end); const hightlightWidth = context.measureText(selectedText).width; context.fillRect(highlightRootX + leftOffsetWidth, highlightRootY, hightlightWidth, this._fontOffset.height); highlightRootY += this._fontOffset.height; } if (this._cursorInfo.globalEndIndex === this._cursorInfo.globalStartIndex) { this._resetBlinking(); } } } context.restore(); // Border if (this._thickness) { if (this._isFocused) { if (this.focusedColor) { context.strokeStyle = this.focusedColor; } } else { if (this.color) { context.strokeStyle = this.color; } } context.lineWidth = this._thickness; context.strokeRect(this._currentMeasure.left + this._thickness / 2, this._currentMeasure.top + this._thickness / 2, this._currentMeasure.width - this._thickness, this._currentMeasure.height - this._thickness); } } _resetBlinking() { clearTimeout(this._blinkTimeout); this._blinkTimeout = setTimeout(() => { this._blinkIsEven = !this._blinkIsEven; this._markAsDirty(); }, 500); } _onPointerDown(target, coordinates, pointerId, buttonIndex, pi) { if (!super._onPointerDown(target, coordinates, pointerId, buttonIndex, pi)) { return false; } if (this.isReadOnly) { return true; } this._clickedCoordinateX = coordinates.x; this._clickedCoordinateY = coordinates.y; this._isTextHighlightOn = false; this._highlightedText = ""; this._isPointerDown = true; this._host._capturingControl[pointerId] = this; if (this._host.focusedControl === this) { // Move cursor clearTimeout(this._blinkTimeout); this._markAsDirty(); return true; } if (!this._isEnabled) { return false; } this._host.focusedControl = this; return true; } // for textselection _onPointerMove(target, coordinates, pointerId, pi) { // Avoid Chromium-like beahavior when this event is fired right after onPointerDown if (pi.event.movementX === 0 && pi.event.movementY === 0) { return; } if (this._host.focusedControl === this && this._isPointerDown && !this.isReadOnly) { this._clickedCoordinateX = coordinates.x; this._clickedCoordinateY = coordinates.y; if (!this._isTextHighlightOn) { this._highlightCursorInfo.initialLineIndex = this._cursorInfo.currentLineIndex; this._highlightCursorInfo.initialStartIndex = this._cursorInfo.globalStartIndex; this._highlightCursorInfo.initialRelativeStartIndex = this._cursorInfo.relativeStartIndex; this._isTextHighlightOn = true; } this._markAsDirty(); } super._onPointerMove(target, coordinates, pointerId, pi); } /** * Apply the correct position of cursor according to current modification */ _updateCursorPosition() { if (!this._isFocused) { return; } if (!this._textWrapper.text && this.placeholderText) { this._cursorInfo.currentLineIndex = 0; this._cursorInfo.globalStartIndex = 0; this._cursorInfo.globalEndIndex = 0; this._cursorInfo.relativeStartIndex = 0; this._cursorInfo.relativeEndIndex = 0; } else { if (this._clickedCoordinateX && this._clickedCoordinateY) { if (!this._isTextHighlightOn) { this._cursorInfo = { globalStartIndex: 0, globalEndIndex: 0, relativeStartIndex: 0, relativeEndIndex: 0, currentLineIndex: 0, }; } let globalIndex = 0; let relativeIndex = 0; const lastClickedCoordinateY = this._clickedCoordinateY - this._scrollTop; const relativeCoordinateY = Math.floor(lastClickedCoordinateY / this._fontOffset.height); this._cursorInfo.currentLineIndex = Math.min(Math.max(relativeCoordinateY, 0), this._lines.length - 1); let currentSize = 0; const relativeXPosition = this._clickedCoordinateX - (this._scrollLeft ?? 0); let previousDist = 0; for (let index = 0; index < this._cursorInfo.currentLineIndex; index++) { const line = this._lines[index]; globalIndex += line.text.length + line.lineEnding.length; } while (currentSize < relativeXPosition && this._lines[this._cursorInfo.currentLineIndex].text.length > relativeIndex) { relativeIndex++; previousDist = Math.abs(relativeXPosition - currentSize); currentSize = this._contextForBreakLines.measureText(this._lines[this._cursorInfo.currentLineIndex].text.substring(0, relativeIndex)).width; } // Find closest move if (Math.abs(relativeXPosition - currentSize) > previousDist && relativeIndex > 0) { relativeIndex--; } globalIndex += relativeIndex; if (!this._isTextHighlightOn) { this._cursorInfo.globalStartIndex = globalIndex; this._cursorInfo.relativeStartIndex = relativeIndex; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; this._cursorInfo.relativeEndIndex = this._cursorInfo.relativeStartIndex; } else { if (globalIndex < this._highlightCursorInfo.initialStartIndex) { this._cursorInfo.globalStartIndex = globalIndex; this._cursorInfo.relativeStartIndex = relativeIndex; this._cursorInfo.globalEndIndex = this._highlightCursorInfo.initialStartIndex; this._cursorInfo.relativeEndIndex = this._highlightCursorInfo.initialRelativeStartIndex; } else { this._cursorInfo.globalStartIndex = this._highlightCursorInfo.initialStartIndex; this._cursorInfo.relativeStartIndex = this._highlightCursorInfo.initialRelativeStartIndex; this._cursorInfo.globalEndIndex = globalIndex; this._cursorInfo.relativeEndIndex = relativeIndex; } } // Avoid the caret during highlighting this._blinkIsEven = this._isTextHighlightOn; this._clickedCoordinateX = null; this._clickedCoordinateY = null; } else { // Standard behavior same as Current line is at least above the initial highlight index this._cursorInfo.relativeStartIndex = 0; this._cursorInfo.currentLineIndex = 0; let lineLength = this._lines[this._cursorInfo.currentLineIndex].text.length + this._lines[this._cursorInfo.currentLineIndex].lineEnding.length; let tmpLength = 0; while (tmpLength + lineLength <= this._cursorInfo.globalStartIndex) { tmpLength += lineLength; if (this._cursorInfo.currentLineIndex < this._lines.length - 1) { this._cursorInfo.currentLineIndex++; lineLength = this._lines[this._cursorInfo.currentLineIndex].text.length + this._lines[this._cursorInfo.currentLineIndex].lineEnding.length; } } this._cursorInfo.relativeStartIndex = this._cursorInfo.globalStartIndex - tmpLength; if (!this._isTextHighlightOn) { this._cursorInfo.relativeEndIndex = this._cursorInfo.relativeStartIndex; this._cursorInfo.globalEndIndex = this._cursorInfo.globalStartIndex; } else if (this._highlightCursorInfo.initialStartIndex !== -1 && this._cursorInfo.globalStartIndex >= this._highlightCursorInfo.initialStartIndex) { // Current line is at least below the initial highlight index while (tmpLength + lineLength <= this._cursorInfo.globalEndIndex) { tmpLength += lineLength; if (this._cursorInfo.currentLineIndex < this._lines.length - 1) { this._cursorInfo.currentLineIndex++; lineLength = this._lines[this._cursorInfo.currentLineIndex].text.length + this._lines[this._cursorInfo.currentLineIndex].lineEnding.length; } } this._cursorInfo.relativeEndIndex = this._cursorInfo.globalEndIndex - tmpLength; } } } } /** * Update all values of cursor information based on cursorIndex value * * @param offset The index to take care of * @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _updateValueFromCursorIndex(offset) { // Override to avoid parent behavior during _onPointerMove } /** * Select the word immediatly under the cursor on double click * * @param _evt Pointer informations of double click * @internal */ _processDblClick(_evt) { //pre-find the start and end index of the word under cursor, speeds up the rendering let moveLeft, moveRight; do { moveLeft = this._cursorInfo.globalStartIndex > 0 && this._textWrapper.isWord(this._cursorInfo.globalStartIndex - 1) ? --this._cursorInfo.globalStartIndex : 0; moveRight = this._cursorInfo.globalEndIndex < this._textWrapper.length && this._textWrapper.isWord(this._cursorInfo.globalEndIndex) ? ++this._cursorInfo.globalEndIndex : 0; } while (moveLeft || moveRight); this._highlightCursorInfo.initialLineIndex = this._cursorInfo.currentLineIndex; this._highlightCursorInfo.initialStartIndex = this._cursorInfo.globalStartIndex; this.onTextHighlightObservable.notifyObservers(this); this._isTextHighlightOn = true; this._blinkIsEven = true; this._markAsDirty(); } /** @internal */ selectAllText() { this._isTextHighlightOn = true; this._blinkIsEven = true; this._highlightCursorInfo = { initialStartIndex: 0, initialRelativeStartIndex: 0, initialLineIndex: 0, }; this._cursorInfo = { globalStartIndex: 0, globalEndIndex: this._textWrapper.length, relativeEndIndex: this._lines[this._lines.length - 1].text.length, relativeStartIndex: 0, currentLineIndex: this._lines.length - 1, }; this._markAsDirty(); } dispose() { super.dispose(); this.onLinesReadyObservable.clear(); } } __decorate([ serialize() ], InputTextArea.prototype, "autoStretchHeight", null); __decorate([ serialize() ], InputTextArea.prototype, "maxHeight", null); RegisterClass("BABYLON.GUI.InputTextArea", InputTextArea); //# sourceMappingURL=inputTextArea.js.map