void paintBackground(Canvas canvas, PaintRecord record, Offset baseOffset) { if (record.style.background == null) { return; } var metrics = record.metrics; Rect rect = Rect.fromLTRB(0, metrics.ascent, record.runWidth, metrics.descent); rect = rect.shift(record.shiftedOffset(baseOffset)); canvas.drawRect(rect, record.style.background); }
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); } }
void paintDecorations(Canvas canvas, PaintRecord record, Offset baseOffset) { if (record.style.decoration == null || record.style.decoration == TextDecoration.none) { return; } if (record.style.decorationColor == null) { this._textPaint.color = record.style.color; } else { this._textPaint.color = record.style.decorationColor; } var width = record.runWidth; var metrics = record.metrics; float underLineThickness = metrics.underlineThickness ?? (record.style.fontSize / 14.0f); this._textPaint.style = PaintingStyle.stroke; this._textPaint.strokeWidth = underLineThickness; var recordOffset = record.shiftedOffset(baseOffset); var x = recordOffset.dx; var y = recordOffset.dy; int decorationCount = 1; switch (record.style.decorationStyle) { case TextDecorationStyle.doubleLine: decorationCount = 2; break; } var decoration = record.style.decoration; for (int i = 0; i < decorationCount; i++) { float yOffset = i * underLineThickness * kFloatDecorationSpacing; float yOffsetOriginal = yOffset; if (decoration != null && decoration.contains(TextDecoration.underline)) { // underline yOffset += metrics.underlinePosition ?? underLineThickness; canvas.drawLine(new Offset(x, y + yOffset), new Offset(x + width, y + yOffset), this._textPaint); yOffset = yOffsetOriginal; } if (decoration != null && decoration.contains(TextDecoration.overline)) { yOffset += metrics.ascent; canvas.drawLine(new Offset(x, y + yOffset), new Offset(x + width, y + yOffset), this._textPaint); yOffset = yOffsetOriginal; } if (decoration != null && decoration.contains(TextDecoration.lineThrough)) { yOffset += (decorationCount - 1.0f) * underLineThickness * kFloatDecorationSpacing / -2.0f; yOffset += metrics.strikeoutPosition ?? (metrics.fxHeight ?? 0) / -2.0f; canvas.drawLine(new Offset(x, y + yOffset), new Offset(x + width, y + yOffset), this._textPaint); yOffset = yOffsetOriginal; } } this._textPaint.style = PaintingStyle.fill; this._textPaint.strokeWidth = 0; }