Example #1
0
    private int MeasureVanillaLine(TigFont font, TigTextStyle style, ReadOnlySpan <char> text)
    {
        if (text.IsEmpty)
        {
            return(0);
        }

        var result = 0;
        var length = text.Length;
        var glyphs = font.FontFace.Glyphs;

        for (var i = 0; i < length; i++)
        {
            var ch = text[i];

            // Skip @ characters if they are followed by a number between 0 and 9
            if (ch == '@' & i + 1 < length && text[i + 1] >= '0' && text[i + 1] <= '9')
            {
                i++;
                continue;
            }

            if (ch >= 0 && ch < 128 && char.IsWhiteSpace(ch))
            {
                if (ch != '\n')
                {
                    result += style.tracking;
                }
            }
            else
            {
                if (font.GetGlyphIdx(ch, out var glyphIdx))
                {
                    result += glyphs[glyphIdx].WidthLine + style.kerning;
                }
            }
        }

        return(result);
    }
    public void RenderRun(ReadOnlySpan <char> text,
                          int x,
                          int y,
                          Rectangle bounds,
                          TigTextStyle style,
                          TigFont font)
    {
        foreach (var state in _fileState)
        {
            state.GlyphCount = 0;
        }

        var fontFace = font.FontFace;

        for (var it = 0; it < text.Length; ++it)
        {
            var ch     = text[it];
            var nextCh = '\0';
            if (it + 1 < text.Length)
            {
                nextCh = text[it + 1];
            }

            // @0 to @9 select one of the text colors
            if (ch == '@' && char.IsDigit(nextCh))
            {
                ++it; // Skip the digit
                style.colorSlot = nextCh - '0';
                continue;
            }

            // Handle @t tabstop movement
            if (ch == '@' && nextCh == 't')
            {
                var tabWidth = style.tabStop - bounds.X;

                if (tabWidth > 0)
                {
                    ++it; // Skip the t

                    var tabCount = 1 + (x - bounds.X) / tabWidth;
                    x = bounds.X + tabCount * tabWidth;
                }

                continue;
            }

            if (!font.GetGlyphIdx(text[it], out var glyphIdx))
            {
                continue;
            }

            if (text[it] == ' ')
            {
                glyphIdx = '-' - '!';
            }

            if (glyphIdx >= fontFace.Glyphs.Length)
            {
                return; // Trying to render invalid character
            }

            if (ch >= 0 && ch < 128 && char.IsWhiteSpace(ch))
            {
                x += style.tracking;
                continue;
            }

            var glyph = fontFace.Glyphs[glyphIdx];

            // For some mysterious reason ToEE actually uses one pixel more to the left of the
            // Glyph than is specified in the font file. That area should be transparent, but
            // it means all rendered text is shifted one pixel more to the right than it should be.
            // NOTE: When using linear filtering, the adjacent char sometimes bleeds into the current one
            //       To avoid this, instead of actually rendering this 1px offset, we just offset the
            //       destination rectangle by (1,1) below.
            float u1 = glyph.Rectangle.X;
            float v1 = glyph.Rectangle.Y;
            var   u2 = u1 + glyph.Rectangle.Width;
            var   v2 = v1 + glyph.Rectangle.Height;

            var state = _fileState[glyph.FontArtIndex];

            // See big comment above for reasoning for the 1,1 offset
            var destRect = new Rectangle(
                x + 1,
                y + fontFace.BaseLine - glyph.BaseLineYOffset + 1,
                glyph.Rectangle.Width,
                glyph.Rectangle.Height
                );

            x += style.kerning + glyph.WidthLine;

            Trace.Assert(!style.flags.HasFlag((TigTextStyleFlag)0x1000));
            Trace.Assert(!style.flags.HasFlag((TigTextStyleFlag)0x2000));

            // Drop Shadow
            if (style.flags.HasFlag(TigTextStyleFlag.TTSF_DROP_SHADOW))
            {
                Trace.Assert(style.shadowColor.HasValue);
                var shadowVertexIdx = state.GlyphCount * 4;
                var shadowColor     = style.shadowColor.Value.topLeft;
                shadowColor.A = 255;

                // Top Left
                ref var sVertexTL = ref state.Vertices[shadowVertexIdx];
                sVertexTL.X       = destRect.X + 1.0f;
                sVertexTL.Y       = destRect.Y + 1.0f;
                sVertexTL.U       = u1;
                sVertexTL.V       = v1;
                sVertexTL.diffuse = shadowColor;

                // Top Right
                ref var sVertexTR = ref state.Vertices[shadowVertexIdx + 1];
                sVertexTR.X       = destRect.X + destRect.Width + 1.0f;
                sVertexTR.Y       = destRect.Y + 1.0f;
                sVertexTR.U       = u2;
                sVertexTR.V       = v1;
                sVertexTR.diffuse = shadowColor;

                // Bottom Right
                ref var sVertexBR = ref state.Vertices[shadowVertexIdx + 2];
Example #3
0
    internal bool MoveToNextRun(out LayoutRun nextRun)
    {
        switch (state)
        {
        default:
            throw new InvalidOperationException("Invalid state: " + state);

        case -1:
            nextRun = default;
            return(false);

        case 0:
            currentY = extents.Y;

            lastLine = false;

            // TODO: Check if this can even happen since we measure the text
            // if the width hasn't been constrained
            if (extents.Width == 0)
            {
                nextRun = new LayoutRun(
                    0,
                    text.Length,
                    extents.X,
                    extents.Y,
                    extents,
                    false
                    );
                state = -1;
                return(true);
            }

            // Is there only space for one line?
            if (!font.GetGlyphIdx('.', out var dotIdx))
            {
                throw new Exception("Font has no '.' character.");
            }

            ellipsisWidth = 3 * (style.kerning + glyphs[dotIdx].WidthLine);
            linePadding   = 0;
            if (extents.Y + 2 * font.FontFace.LargestHeight > extents.Y + extents.Height)
            {
                lastLine = true;
                if ((style.flags & TigTextStyleFlag.TTSF_TRUNCATE) != 0)
                {
                    linePadding = -ellipsisWidth;
                }
            }

            if (text.Length <= 0)
            {
                nextRun = default;
                state   = -1;
                return(false);
            }

            startOfWord = 0;
            state       = 1;
            goto case 1;

        // Iterate one more character run
        case 1:
            if (startOfWord >= text.Length)
            {
                state   = -1;
                nextRun = default;
                return(false);
            }

            (wordsOnLine, lineWidth) = TextLayouter.MeasureCharRun(
                text.Slice(startOfWord),
                style,
                extents,
                extents.Width,
                font,
                linePadding,
                lastLine);

            // There's just one word left and it wont fit. Remove restriction on width.
            if (wordsOnLine == 0 && (style.flags & TigTextStyleFlag.TTSF_TRUNCATE) == 0)
            {
                (wordsOnLine, lineWidth) = TextLayouter.MeasureCharRun(
                    text.Slice(startOfWord),
                    style,
                    extents,
                    9999999,
                    font,
                    linePadding,
                    lastLine);
            }

            currentX = 0;
            wordIdx  = 0;
            state    = 2;
            goto case 2;

        case 2:
            if (wordIdx >= wordsOnLine)
            {
                // Advance to next line
                currentY += font.FontFace.LargestHeight;
                if (currentY + 2 * font.FontFace.LargestHeight > extents.Y + extents.Height)
                {
                    lastLine = true;
                    if (style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))
                    {
                        linePadding = ellipsisWidth;
                    }
                }

                startOfWord++;
                state = 1;
                goto case 1;
            }

            var remainingSpace = extents.Width + linePadding - currentX;

            wordInfo = TextLayouter.ScanWord(text,
                                             startOfWord,
                                             text.Length,
                                             lastLine,
                                             font,
                                             style,
                                             remainingSpace);

            var lastIdx = wordInfo.lastIdx;
            wordWidth = wordInfo.Width;

            if (lastLine && (style.flags & TigTextStyleFlag.TTSF_TRUNCATE) != 0)
            {
                if (currentX + wordInfo.fullWidth > extents.Width)
                {
                    lastIdx = wordInfo.idxBeforePadding;
                }
                else
                {
                    if (!TextLayouter.HasMoreText(text.Slice(lastIdx), style.tabStop > 0))
                    {
                        wordInfo.drawEllipsis = false;
                        wordWidth             = wordInfo.fullWidth;
                    }
                }
            }

            startOfWord = lastIdx;
            if (startOfWord + 1 < text.Length && text[startOfWord] == '@' && text[startOfWord + 1] == 't')
            {
                // Extend the word with by the amount needed to move to the tabstop
                wordWidth += Math.Max(0, style.tabStop - (currentX + wordWidth));
                // Skip the "t" of "@t"
                startOfWord++;
            }
            else if (startOfWord < text.Length && text[startOfWord] >= 0 &&
                     char.IsWhiteSpace(text[startOfWord]))
            {
                wordWidth += style.tracking;
            }

            // This means this is not the last word in this line
            if (wordIdx + 1 < wordsOnLine)
            {
                startOfWord++;
            }

            // Draw the word
            var x = extents.X + currentX;
            if ((style.flags & TigTextStyleFlag.TTSF_CENTER) != 0)
            {
                x += (extents.Width - lineWidth) / 2;
            }

            if (wordInfo.firstIdx < 0 || lastIdx < 0)
            {
                Logger.Error("Bad firstIdx at LayoutAndDraw! {0}, {1}", wordInfo.firstIdx, lastIdx);
            }
            else if (lastIdx >= wordInfo.firstIdx)
            {
                nextRun = new LayoutRun(
                    wordInfo.firstIdx,
                    lastIdx,
                    x,
                    currentY,
                    extents,
                    false);
                state = 3;
                return(true);
            }

            state = 3;
            goto case 3;

        case 3:
            currentX += wordWidth;

            // We're on the last line, the word has been truncated, ellipsis needs to be drawn
            if (lastLine && style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE) &&
                wordInfo.drawEllipsis)
            {
                nextRun = new LayoutRun(
                    wordInfo.lastIdx,
                    wordInfo.lastIdx,
                    extents.X + currentX,
                    currentY,
                    extents,
                    true
                    );
                state = -1;
                return(true);
            }

            wordIdx++;
            state = 2;
            goto case 2;
        }
    }
Example #4
0
    private int CountLinesVanilla(int maxWidth, int maxLines, ReadOnlySpan <char> text, TigFont font,
                                  TigTextStyle style)
    {
        var length = text.Length;

        if (length <= 0)
        {
            return(1);
        }

        var lineWidth = 0;
        var lines     = 1;

        var glyphs = font.FontFace.Glyphs;

        var ch = '\0';

        for (var i = 0; i < length; i++)
        {
            var wordWidth = 0;

            // Measure the length of the current word
            for (; i < length; i++)
            {
                ch = text[i];
                if (ch == '’') // fix for this character that sometimes appears in vanilla
                {
                    ch = '\'';
                }
                // Skip @[0-9]
                if (ch == '@' & i + 1 < length && text[i + 1] >= '0' && text[i + 1] <= '9')
                {
                    i++;
                    continue;
                }


                if (ch < 255 && ch >= 0)
                {
                    if (char.IsWhiteSpace(ch))
                    {
                        break;
                    }
                }

                if (font.GetGlyphIdx(ch, out var glyphIdx))
                {
                    wordWidth += glyphs[glyphIdx].WidthLine + style.kerning;
                }
            }

            lineWidth += wordWidth;

            // If there's enough space in the maxWidth left and we're not at a newline
            // increase the linewidth and continue on.
            if (lineWidth <= maxWidth && ch != '\n')
            {
                if (ch < 255 && ch >= 0 && char.IsWhiteSpace(ch))
                {
                    lineWidth += style.tracking;
                }

                continue;
            }

            // We're either at a newline, or break the line here due to reaching the maxwidth
            lines++;

            // Reached the max number of lines . quit
            if (maxLines != 0 && lines >= maxLines)
            {
                break;
            }

            if (lineWidth <= maxWidth)
            {
                // We reached a normal line break
                lineWidth = 0;
            }
            else
            {
                // We're breaking the line, so we'll keep the current word
                // width as the initial length of the new line
                lineWidth = wordWidth;
            }

            // Continuation indent
            if (style.flags.HasFlag(TigTextStyleFlag.TTSF_CONTINUATION_INDENT))
            {
                lineWidth += 8 * style.tracking;
            }

            if (ch < 255 && ch >= 0 && char.IsWhiteSpace(ch))
            {
                if (ch != '\n')
                {
                    lineWidth += style.tracking;
                }
            }
        }

        return(lines);
    }
Example #5
0
    internal static Tuple <int, int> MeasureCharRun(ReadOnlySpan <char> text,
                                                    TigTextStyle style,
                                                    Rectangle extents,
                                                    int extentsWidth,
                                                    TigFont font,
                                                    int linePadding,
                                                    bool lastLine)
    {
        var lineWidth            = 0;
        var wordCountWithPadding = 0;
        var wordWidth            = 0;
        var wordCount            = 0;

        var glyphs = font.FontFace.Glyphs;

        // This seems to be special handling for the sequence "@t" and @0 - @9
        var index = 0;

        for (; index < text.Length; ++index)
        {
            var ch     = text[index];
            var nextCh = '\0';
            if (index + 1 < text.Length)
            {
                nextCh = text[index + 1];
            }

            // Handles @0 to @9
            if (ch == '@' & char.IsDigit(nextCh))
            {
                ++index; // Skip the number
            }
            else if (ch == '@' && nextCh == 't')
            {
                ++index; // Skip the t

                // Same handling as whitespaces, but variable sized increment rather than just adding tracking!
                if (lineWidth + wordWidth <= extentsWidth)
                {
                    wordCount++;
                    if (lineWidth + wordWidth <= extentsWidth + linePadding)
                    {
                        wordCountWithPadding++;
                    }

                    lineWidth += wordWidth;
                    wordWidth  = 0;

                    // Increase the line width such that it continues at the tab stop location,
                    // but do not move backwards (unsupported)
                    lineWidth += Math.Max(0, style.tabStop - lineWidth);
                }
                else
                {
                    // Stop if we have run out of space on this line
                    break;
                }
            }
            else if (ch == '\n')
            {
                if (lineWidth + wordWidth <= extentsWidth)
                {
                    wordCount++;
                    if (lineWidth + wordWidth <= extentsWidth + linePadding)
                    {
                        wordCountWithPadding++;
                    }

                    lineWidth += wordWidth;
                    wordWidth  = 0;
                }

                break;
            }
            else if (ch < 255 && ch > -1 && char.IsWhiteSpace(ch))
            {
                if (lineWidth + wordWidth <= extentsWidth)
                {
                    wordCount++;
                    if (lineWidth + wordWidth <= extentsWidth + linePadding)
                    {
                        wordCountWithPadding++;
                    }

                    lineWidth += wordWidth + style.tracking;
                    wordWidth  = 0;
                }
                else
                {
                    // Stop if we have run out of space on this line
                    break;
                }
            }
            else if (ch == '’') // special casing this m**********r
            {
                ch = '\'';
                if (font.GetGlyphIdx(ch, out var glyphIdx))
                {
                    wordWidth += style.kerning + glyphs[glyphIdx].WidthLine;
                }
            }
            else
            {
                if (font.GetGlyphIdx(ch, out var glyphIdx))
                {
                    wordWidth += style.kerning + glyphs[glyphIdx].WidthLine;
                }
            }
        }

        // Handle the last word, if we're at the end of the string
        if (index >= text.Length && wordWidth > 0)
        {
            if (lineWidth + wordWidth <= extentsWidth)
            {
                wordCount++;
                lineWidth += wordWidth;
                if (lineWidth + wordWidth <= extentsWidth + linePadding)
                {
                    wordCountWithPadding++;
                }
            }
            else if (style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))
            {
                // The word would actually not fit, but we're the last
                // thing in the string and we truncate with ...
                lineWidth += wordWidth;
                wordCount++;
                wordCountWithPadding++;
            }
        }

        // Ignore the padding if we'd not print ellipsis anyway
        if (!lastLine || index >= text.Length || !style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))
        {
            wordCountWithPadding = wordCount;
        }

        return(Tuple.Create(wordCountWithPadding, lineWidth));
    }
Example #6
0
    internal static ScanWordResult ScanWord(Span <char> text,
                                            int firstIdx,
                                            int textLength,
                                            bool lastLine,
                                            TigFont font,
                                            TigTextStyle style,
                                            int remainingSpace)
    {
        var result = new ScanWordResult();

        result.firstIdx = firstIdx;

        var glyphs = font.FontFace.Glyphs;

        var i = firstIdx;

        for (; i < textLength; i++)
        {
            var curCh  = text[i];
            var nextCh = '\0';
            if (i + 1 < textLength)
            {
                nextCh = text[i + 1];
            }

            if (curCh == '’')
            {
                curCh = text[i] = '\'';
            }

            // Simply skip @t without increasing the width
            if (curCh == '@' && char.IsDigit(nextCh))
            {
                i++; // Skip the number
                continue;
            }

            // @t will advance the width up to the next tabstop, but only if a tabstop has been specified
            if (curCh == '@' && nextCh == 't' && style.tabStop > 0)
            {
                break; // Treat it like whitespace
            }

            if (!font.GetGlyphIdx(curCh, out var glyphIdx))
            {
                Logger.Warn("Tried to display character {0} in text '{1}'", glyphIdx, new string(text));
                continue;
            }

            if (curCh == '\n')
            {
                if (lastLine && style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))
                {
                    result.drawEllipsis = true;
                }

                break;
            }

            if (curCh < 128 && curCh > 0 && char.IsWhiteSpace(curCh))
            {
                break;
            }

            if (style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))
            {
                result.fullWidth += glyphs[glyphIdx].WidthLine + style.kerning;
                if (result.fullWidth > remainingSpace)
                {
                    result.drawEllipsis = true;
                    continue;
                }

                result.idxBeforePadding = i;
            }

            result.Width += glyphs[glyphIdx].WidthLine + style.kerning;
        }

        result.lastIdx = i;
        return(result);
    }