void layoutEmoji(string text, TextStyle style, Font font, int start, int count) { for (int i = 0; i < count; i++) { char c = text[i]; float x = this._advance; if (EmojiUtils.isSingleCharNonEmptyEmoji(c) || char.IsHighSurrogate(c)) { float letterSpace = style.letterSpacing; float letterSpaceHalfLeft = letterSpace * 0.5f; float letterSpaceHalfRight = letterSpace - letterSpaceHalfLeft; x += letterSpaceHalfLeft; this._advances[i] += letterSpaceHalfLeft; var metrics = FontMetrics.fromFont(font, style.UnityFontSize); var minX = x; var maxX = metrics.descent - metrics.ascent + x; var minY = metrics.ascent; var maxY = metrics.descent; if (this._bounds.width <= 0 || this._bounds.height <= 0) { this._bounds = UnityEngine.Rect.MinMaxRect( minX, minY, maxX, maxY); } else { if (minX < this._bounds.x) { this._bounds.x = minX; } if (minY < this._bounds.y) { this._bounds.y = minY; } if (maxX > this._bounds.xMax) { this._bounds.xMax = maxX; } if (maxY > this._bounds.yMax) { this._bounds.yMax = maxY; } } this._positions[i] = x; float advance = style.fontSize; x += advance; this._advances[i] += advance; this._advances[i] += letterSpaceHalfRight; x += letterSpaceHalfRight; } else { this._advances[i] = 0; this._positions[i] = x; } this._advance = x; } }
public uiMeshMesh resolveMesh() { if (this._resolved) { return(this._mesh); } this._resolved = true; var style = this.textBlob.Value.style; var text = this.textBlob.Value.text; var key = MeshKey.create(this.textBlob.Value.instanceId, this.scale); var fontInfo = FontManager.instance.getOrCreate(style.fontFamily, style.fontWeight, style.fontStyle); var font = fontInfo.font; _meshes.TryGetValue(key, out var meshInfo); if (meshInfo != null && meshInfo.textureVersion == fontInfo.textureVersion) { ObjectPool <MeshKey> .release(key); meshInfo.touch(); this._mesh = meshInfo.mesh.transform(this.matrix); return(this._mesh); } // Handling Emoji char startingChar = text[this.textBlob.Value.textOffset]; if (char.IsHighSurrogate(startingChar) || EmojiUtils.isSingleCharEmoji(startingChar)) { var vert = ObjectPool <uiList <Vector3> > .alloc(); var tri = ObjectPool <uiList <int> > .alloc(); var uvCoord = ObjectPool <uiList <Vector2> > .alloc(); var metrics = FontMetrics.fromFont(font, style.UnityFontSize); var minMaxRect = EmojiUtils.getMinMaxRect(style.fontSize, metrics.ascent, metrics.descent); var minX = minMaxRect.left; var maxX = minMaxRect.right; var minY = minMaxRect.top; var maxY = minMaxRect.bottom; for (int i = 0; i < this.textBlob.Value.textSize; i++) { char a = text[this.textBlob.Value.textOffset + i]; int code = a; if (char.IsHighSurrogate(a)) { D.assert(i + 1 < this.textBlob.Value.textSize); D.assert(this.textBlob.Value.textOffset + i + 1 < this.textBlob.Value.text.Length); char b = text[this.textBlob.Value.textOffset + i + 1]; D.assert(char.IsLowSurrogate(b)); code = char.ConvertToUtf32(a, b); } else if (char.IsLowSurrogate(a) || EmojiUtils.isEmptyEmoji(a)) { continue; } var uvRect = EmojiUtils.getUVRect(code); var positionX = this.textBlob.Value.getPositionX(i); int baseIndex = vert.Count; vert.Add(new Vector3(positionX + minX, minY, 0)); vert.Add(new Vector3(positionX + maxX, minY, 0)); vert.Add(new Vector3(positionX + maxX, maxY, 0)); vert.Add(new Vector3(positionX + minX, maxY, 0)); tri.Add(baseIndex); tri.Add(baseIndex + 1); tri.Add(baseIndex + 2); tri.Add(baseIndex); tri.Add(baseIndex + 2); tri.Add(baseIndex + 3); uvCoord.Add(uvRect.bottomLeft.toVector()); uvCoord.Add(uvRect.bottomRight.toVector()); uvCoord.Add(uvRect.topRight.toVector()); uvCoord.Add(uvRect.topLeft.toVector()); if (char.IsHighSurrogate(a)) { i++; } } uiMeshMesh meshMesh = uiMeshMesh.create(null, vert, tri, uvCoord); if (_meshes.ContainsKey(key)) { ObjectPool <MeshInfo> .release(_meshes[key]); _meshes.Remove(key); } _meshes[key] = MeshInfo.create(key, meshMesh, 0); this._mesh = meshMesh.transform(this.matrix); return(this._mesh); } var length = this.textBlob.Value.textSize; var fontSizeToLoad = Mathf.CeilToInt(style.UnityFontSize * this.scale); var vertices = ObjectPool <uiList <Vector3> > .alloc(); vertices.SetCapacity(length * 4); var triangles = ObjectPool <uiList <int> > .alloc(); triangles.SetCapacity(length * 6); var uv = ObjectPool <uiList <Vector2> > .alloc(); uv.SetCapacity(length * 4); for (int charIndex = 0; charIndex < length; ++charIndex) { var ch = text[charIndex + this.textBlob.Value.textOffset]; // first char as origin for mesh position var positionX = this.textBlob.Value.getPositionX(charIndex); if (LayoutUtils.isWordSpace(ch) || LayoutUtils.isLineEndSpace(ch) || ch == '\t') { continue; } if (fontSizeToLoad == 0) { continue; } font.getGlyphInfo(ch, out var glyphInfo, fontSizeToLoad, style.UnityFontStyle); var minX = glyphInfo.minX / this.scale; var maxX = glyphInfo.maxX / this.scale; var minY = -glyphInfo.maxY / this.scale; var maxY = -glyphInfo.minY / this.scale; var baseIndex = vertices.Count; vertices.Add(new Vector3(positionX + minX, minY, 0)); vertices.Add(new Vector3(positionX + maxX, minY, 0)); vertices.Add(new Vector3(positionX + maxX, maxY, 0)); vertices.Add(new Vector3(positionX + minX, maxY, 0)); triangles.Add(baseIndex); triangles.Add(baseIndex + 1); triangles.Add(baseIndex + 2); triangles.Add(baseIndex); triangles.Add(baseIndex + 2); triangles.Add(baseIndex + 3); uv.Add(glyphInfo.uvTopLeft); uv.Add(glyphInfo.uvTopRight); uv.Add(glyphInfo.uvBottomRight); uv.Add(glyphInfo.uvBottomLeft); } if (vertices.Count == 0) { this._mesh = null; ObjectPool <uiList <Vector3> > .release(vertices); ObjectPool <uiList <Vector2> > .release(uv); ObjectPool <uiList <int> > .release(triangles); ObjectPool <MeshKey> .release(key); return(null); } uiMeshMesh mesh = vertices.Count > 0 ? uiMeshMesh.create(null, vertices, triangles, uv) : null; if (_meshes.ContainsKey(key)) { ObjectPool <MeshInfo> .release(_meshes[key]); _meshes.Remove(key); } _meshes[key] = MeshInfo.create(key, mesh, fontInfo.textureVersion); this._mesh = mesh.transform(this.matrix); return(this._mesh); }
public void layout(ParagraphConstraints constraints) { if (!this._needsLayout && this._width == constraints.width) { return; } this._tabStops.setFont( FontManager.instance.getOrCreate( this._paragraphStyle.fontFamily ?? TextStyle.kDefaultFontFamily, this._paragraphStyle.fontWeight ?? TextStyle.kDefaultFontWeight, this._paragraphStyle.fontStyle ?? TextStyle.kDefaultfontStyle).font, (int)(this._paragraphStyle.fontSize ?? TextStyle.kDefaultFontSize)); this._needsLayout = false; this._width = Mathf.Floor(constraints.width); int lineStyleRunsCount = this._computeLineBreak(); if (this._glyphLines == null || this._glyphLines.Length < this._lineRangeCount) { this._glyphLines = new GlyphLine[LayoutUtils.minPowerOfTwo(this._lineRangeCount)]; } if (this._lineHeights == null || this._lineHeights.Length < this._lineRangeCount) { this._lineHeights = new float[LayoutUtils.minPowerOfTwo(this._lineRangeCount)]; } if (this._paintRecords == null || this._paintRecords.Length < lineStyleRunsCount) { this._paintRecords = new PaintRecord[LayoutUtils.minPowerOfTwo(lineStyleRunsCount)]; } this._paintRecordsCount = 0; if (this._codeUnitRuns == null || this._codeUnitRuns.Length < lineStyleRunsCount) { this._codeUnitRuns = new CodeUnitRun[LayoutUtils.minPowerOfTwo(lineStyleRunsCount)]; } this._codeUnitRunsCount = 0; int styleMaxLines = this._paragraphStyle.maxLines ?? int.MaxValue; this._didExceedMaxLines = this._lineRangeCount > styleMaxLines; var lineLimit = Mathf.Min(styleMaxLines, this._lineRangeCount); int styleRunIndex = 0; float yOffset = 0; float preMaxDescent = 0; float maxWordWidth = 0; TextBlobBuilder builder = new TextBlobBuilder(); int ellipsizedLength = this._text.Length + (this._paragraphStyle.ellipsis?.Length ?? 0); // All text blobs share a single position buffer, which is big enough taking ellipsis into consideration if (this._textBlobPositionXs == null || this._textBlobPositionXs.Length < ellipsizedLength) { this._textBlobPositionXs = new float[LayoutUtils.minPowerOfTwo(ellipsizedLength)]; } builder.setPositionXs(this._textBlobPositionXs); // this._glyphLines and this._codeUnitRuns will refer to this array for glyph positions if (this._glyphPositions == null || this._glyphPositions.Length < ellipsizedLength) { this._glyphPositions = new GlyphPosition[LayoutUtils.minPowerOfTwo(ellipsizedLength)]; } // Pointer to the _glyphPositions array, to keep track of where the next glyph is stored int pGlyphPositions = 0; // Compute max(NumberOfWords(line) for line in lines), to determine the size of word buffers int maxWordCount = this._computeMaxWordCount(); if (_wordsBuffer == null || _wordsBuffer.Length < maxWordCount) { _wordsBuffer = new Range <int> [maxWordCount < 4 ? 4: maxWordCount]; } // Iterate through line ranges for (int lineNumber = 0; lineNumber < lineLimit; ++lineNumber) { var lineRange = this._lineRanges[lineNumber]; int wordIndex = 0; float runXOffset = 0; float justifyXOffset = 0; // Break the line into words if justification should be applied. bool justifyLine = this._paragraphStyle.textAlign == TextAlign.justify && lineNumber != lineLimit - 1 && !lineRange.hardBreak && // Do not apply justify if ellipsis should be added, or the ellipsis may be pushed // out of the border. // This is still not taken care of in the flutter engine. !(this._paragraphStyle.ellipsized() && this._paragraphStyle.maxLines == null); int wordCount = this._findWords(lineRange.start, lineRange.end, _wordsBuffer); float wordGapWidth = !(justifyLine && wordCount > 1) ? 0 : (this._width - this._lineWidths[lineNumber]) / (wordCount - 1); // Count the number of style runs, and compute the character number of the longest run by the way int lineStyleRunCount = this._countLineStyleRuns(lineRange, styleRunIndex, out int maxTextCount); string ellipsis = this._paragraphStyle.ellipsis; bool hardBreak = lineRange.hardBreak; if (!string.IsNullOrEmpty(ellipsis) && !hardBreak && !this._width.isInfinite() && (lineNumber == lineLimit - 1 || this._paragraphStyle.maxLines == null)) { maxTextCount += ellipsis.Length; } // Allocate the advances and positions to store the layout result // TODO: find a way to compute the maxTextCount for the entire paragraph, so that this allocation // happens only once if (_advancesBuffer == null || _advancesBuffer.Length < maxTextCount) { _advancesBuffer = new float[LayoutUtils.minPowerOfTwo(maxTextCount)]; } if (_positionsBuffer == null || _positionsBuffer.Length < maxTextCount) { _positionsBuffer = new float[LayoutUtils.minPowerOfTwo(maxTextCount)]; } // Keep of the position in _glyphPositions before evaluating this line int glyphPositionLineStart = pGlyphPositions; if (lineStyleRunCount != 0) { // Exclude trailing whitespace from right-justified lines so the last // visible character in the line will be flush with the right margin. int lineEndIndex = this._paragraphStyle.textAlign == TextAlign.right || this._paragraphStyle.textAlign == TextAlign.center ? lineRange.endExcludingWhitespace : lineRange.end; int lineStyleRunIndex = 0; // Instead of computing all lineStyleRuns at once and store into an array and iterate through them, // compute each lineStyleRun and deal with it on the fly, to save the storage for the runs while (styleRunIndex < this._runs.size) { var styleRun = this._runs.getRun(styleRunIndex); // Compute the intersection between current style run intersects and the line int start = Mathf.Max(styleRun.start, lineRange.start); int end = Mathf.Min(styleRun.end, lineEndIndex); // Make sure that each run is not empty if (start < end) { var style = styleRun.style; string text = this._text; int textStart = start; int textEnd = end; int textCount = textEnd - textStart; // Keep track of the pointer to _glyphPositions in the start of this run int glyphPositionStyleRunStart = pGlyphPositions; // Ellipsize the text if ellipsis string is set, and this is the last lineStyleRun of // the current line, and this is the last line or max line is not set if (!string.IsNullOrEmpty(ellipsis) && !hardBreak && !this._width.isInfinite() && lineStyleRunIndex == lineStyleRunCount - 1 && (lineNumber == lineLimit - 1 || this._paragraphStyle.maxLines == null)) { float ellipsisWidth = Layout.measureText(ellipsis, style); // Find the minimum number of characters to truncate, so that the truncated text // appended with ellipsis is within the constraints of line width int truncateCount = Layout.computeTruncateCount(runXOffset, text, textStart, textCount, style, this._width - ellipsisWidth, this._tabStops); // If all the positions have not changed, use the cached ellipsized text // else update the cache if (!(this._ellipsizedText != null && this._ellipsizedLength == textStart + textCount - truncateCount && this._ellipsizedText.Length == this._ellipsizedLength + ellipsis.Length && this._ellipsizedText.EndsWith(ellipsis))) { this._ellipsizedText = text.Substring(0, textStart + textCount - truncateCount) + ellipsis; this._ellipsizedLength = this._ellipsizedText.Length - ellipsis.Length; } text = this._ellipsizedText; textCount = text.Length - textStart; D.assert(textCount != 0); if (this._paragraphStyle.maxLines == null) { lineLimit = lineNumber + 1; this._didExceedMaxLines = true; } } float advance = Layout.doLayout(runXOffset, text, textStart, textCount, style, _advancesBuffer, _positionsBuffer, this._tabStops, out var bounds); builder.allocRunPos(style, text, textStart, textCount); builder.setBounds(bounds); // Update the max width of the words // Fill in the glyph positions, and the positions of the text blob builder float wordStartPosition = float.NaN; for (int glyphIndex = 0; glyphIndex < textCount; ++glyphIndex) { float glyphXOffset = _positionsBuffer[glyphIndex] + justifyXOffset; float glyphAdvance = _advancesBuffer[glyphIndex]; builder.setPositionX(glyphIndex, glyphXOffset); this._glyphPositions[pGlyphPositions++] = new GlyphPosition(runXOffset + glyphXOffset, glyphAdvance, textStart + glyphIndex); if (wordIndex < wordCount) { Range <int> word = _wordsBuffer[wordIndex]; // Run into the start of current word, record the start position of this word if (word.start == start + glyphIndex) { wordStartPosition = runXOffset + glyphXOffset; } // Run into the end of current word if (word.end == start + glyphIndex + 1) { if (justifyLine) { justifyXOffset += wordGapWidth; } // Update the current word wordIndex++; // If the start position of this word has been recorded, calculate the // width of this word, and update the entire word if (!float.IsNaN(wordStartPosition)) { maxWordWidth = Mathf.Max(maxWordWidth, this._glyphPositions[pGlyphPositions - 1].xPos.end - wordStartPosition); wordStartPosition = float.NaN; } } } } // Create paint record var font = FontManager.instance.getOrCreate(style.fontFamily, style.fontWeight, style.fontStyle).font; var metrics = FontMetrics.fromFont(font, style.UnityFontSize); PaintRecord paintRecord = new PaintRecord(style, runXOffset, 0, builder.make(), metrics, advance); this._paintRecords[this._paintRecordsCount++] = paintRecord; runXOffset += advance; // Create code unit run this._codeUnitRuns[this._codeUnitRunsCount++] = new CodeUnitRun( new Range <int>(start, end), new Range <float>(this._glyphPositions[glyphPositionStyleRunStart].xPos.start, this._glyphPositions[pGlyphPositions - 1].xPos.end), lineNumber, TextDirection.ltr, glyphPositionStyleRunStart, textCount); lineStyleRunIndex++; } if (styleRun.end >= lineEndIndex) { break; } styleRunIndex++; } } float maxLineSpacing = 0; float maxDescent = 0; void updateLineMetrics(FontMetrics metrics, float styleHeight) { float lineSpacing = lineNumber == 0 ? -metrics.ascent * styleHeight : (-metrics.ascent + metrics.leading) * styleHeight; if (lineSpacing > maxLineSpacing) { maxLineSpacing = lineSpacing; if (lineNumber == 0) { this._alphabeticBaseline = lineSpacing; this._ideographicBaseline = (metrics.underlinePosition ?? 0.0f - metrics.ascent) * styleHeight; } } float descent = metrics.descent * styleHeight; maxDescent = Mathf.Max(descent, maxDescent); } if (lineStyleRunCount != 0) { for (int i = 0; i < lineStyleRunCount; i++) { var paintRecord = this._paintRecords[this._paintRecordsCount - i - 1]; updateLineMetrics(paintRecord.metrics, paintRecord.style.height); } } else { var defaultFont = FontManager.instance.getOrCreate( this._paragraphStyle.fontFamily ?? TextStyle.kDefaultFontFamily, this._paragraphStyle.fontWeight ?? TextStyle.kDefaultFontWeight, this._paragraphStyle.fontStyle ?? TextStyle.kDefaultfontStyle).font; var metrics = FontMetrics.fromFont(defaultFont, (int)(this._paragraphStyle.fontSize ?? TextStyle.kDefaultFontSize)); updateLineMetrics(metrics, this._paragraphStyle.height ?? TextStyle.kDefaultHeight); } this._lineHeights[lineNumber] = ((lineNumber == 0 ? 0 : this._lineHeights[lineNumber - 1]) + Mathf.Round(maxLineSpacing + maxDescent)); yOffset += Mathf.Round(maxLineSpacing + preMaxDescent); preMaxDescent = maxDescent; float lineXOffset = this.getLineXOffset(runXOffset); int count = pGlyphPositions - glyphPositionLineStart; if (lineXOffset != 0 && this._glyphPositions != null) { for (int i = 0; i < count; ++i) { this._glyphPositions[this._glyphPositions.Length - i - 1].shiftSelf(lineXOffset); } } this._glyphLines[lineNumber] = new GlyphLine(glyphPositionLineStart, count); for (int i = 0; i < lineStyleRunCount; i++) { var paintRecord = this._paintRecords[this._paintRecordsCount - 1 - i]; paintRecord.shift(lineXOffset, yOffset); this._paintRecords[this._paintRecordsCount - 1 - i] = paintRecord; } } this._lineCount = lineLimit; // min intrinsic width := minimum width this paragraph has to take, which equals the maximum word width if (this._paragraphStyle.maxLines == 1 || (this._paragraphStyle.maxLines == null && this._paragraphStyle.ellipsized())) { this._minIntrinsicWidth = this.maxIntrinsicWidth; } else { this._minIntrinsicWidth = Mathf.Min(maxWordWidth, this.maxIntrinsicWidth); } }