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];
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; } }
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); }
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)); }
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); }