Beispiel #1
0
        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);
        }
Beispiel #2
0
        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);
            }
        }
Beispiel #3
0
        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;
        }