private int MeasureVanillaParagraph(TigFont font, TigTextStyle style, ReadOnlySpan <char> text) { Span <char> tempText = stackalloc char[text.Length + 1]; text.CopyTo(tempText); tempText[text.Length] = '\n'; var maxLineLen = 0; Span <char> textRest = tempText; var nextNewline = textRest.IndexOf('\n'); while (nextNewline != -1) { var currentLine = textRest.Slice(0, nextNewline); textRest = textRest.Slice(nextNewline + 1); var lineLen = MeasureVanillaLine(font, style, currentLine); if (lineLen > maxLineLen) { maxLineLen = lineLen; } nextNewline = textRest.IndexOf('\n'); } return(maxLineLen); }
public CharUiMainWidget(CharUiParams uiParams) : base(uiParams.CharUiMainWindow) { _normalBackground = new WidgetImage("art/interface/char_ui/main_window.img"); _ironmanBackground = new WidgetImage("art/interface/char_ui/ironman_main_window.img"); SetSize(_normalBackground.GetPreferredSize()); _translation = Tig.FS.ReadMesFile("mes/0_char_ui_text.mes"); var pcCreationMes = Tig.FS.ReadMesFile("mes/pc_creation.mes"); var rerollsLabel = pcCreationMes[10001]; _pointBuyLabel = pcCreationMes[10015]; var attributeModeStyle = new TigTextStyle(new ColorRect(new PackedLinearColorA(0xFF5A7390))); attributeModeStyle.kerning = 1; attributeModeStyle.tracking = 3; var attributeCountStyle = attributeModeStyle.Copy(); attributeCountStyle.textColor = new ColorRect(PackedLinearColorA.White); _attributeRollModeLabel = new WidgetText(rerollsLabel, "char-ui-attribute-mode"); _attributeRollCountLabel = new WidgetText("", "char-ui-attribute-rerolls"); }
// TODO I believe this function measures how many characters will fit into the current line given the bounds. public int MeasureLineWrap(TigFont font, TigTextStyle style, ReadOnlySpan <char> text, Rectangle bounds) { if (bounds.Width == 0) { return(text.Length); } Span <char> textCopy = stackalloc char[text.Length]; text.CopyTo(textCopy); var iterator = new LayoutRunIterator(textCopy, font, bounds, style); int endOfLine = 0; while (iterator.MoveToNextRun(out var run)) { if (run.Y <= bounds.Y) { endOfLine = run.End; } else { break; } } return(endOfLine); }
private void LayoutAndDrawVanilla( Span <char> text, TigFont font, ref Rectangle extents, TigTextStyle style) { var extentsWidth = extents.Width; var extentsHeight = extents.Height; if (extentsWidth == 0) { var metrics = new TigFontMetrics(); metrics.width = extents.Width; metrics.height = extents.Height; Tig.Fonts.Measure(style, text, ref metrics); extents.Width = metrics.width; extents.Height = metrics.height; extentsWidth = metrics.width; extentsHeight = metrics.height; } if ((style.flags & (TigTextStyleFlag.TTSF_BACKGROUND | TigTextStyleFlag.TTSF_BORDER)) != 0) { var rect = new Rectangle( extents.X, extents.Y, Math.Max(extentsWidth, extents.Width), Math.Max(extentsHeight, extents.Height) ); DrawBackgroundOrOutline(rect, style); } var iterator = new LayoutRunIterator(text, font, extents, style); while (iterator.MoveToNextRun(out var run)) { if (run.Truncated) { _renderer.RenderRun( "...", run.X, run.Y, run.Bounds, style, font); } else { _renderer.RenderRun( text.Slice(run.Start, run.End - run.Start), run.X, run.Y, run.Bounds, style, font); } } }
private void DrawBackgroundOrOutline(Rectangle rect, TigTextStyle style) { float left = rect.X; float top = rect.Y; var right = left + rect.Width; var bottom = top + rect.Height; left -= 3; top -= 3; right += 3; bottom += 3; if (style.flags.HasFlag(TigTextStyleFlag.TTSF_BACKGROUND)) { Span <Vertex2d> corners = stackalloc Vertex2d[4]; corners[0].pos = new Vector4(left, top, 0.5f, 1); corners[1].pos = new Vector4(right, top, 0.5f, 1); corners[2].pos = new Vector4(right, bottom, 0.5f, 1); corners[3].pos = new Vector4(left, bottom, 0.5f, 1); if (style.bgColor.HasValue) { var bgColor = style.bgColor.Value; corners[0].diffuse = bgColor.topLeft; corners[1].diffuse = bgColor.topRight; corners[2].diffuse = bgColor.bottomRight; corners[3].diffuse = bgColor.bottomLeft; } else { foreach (ref var corner in corners) { corner.diffuse = PackedLinearColorA.White; } } corners[0].uv = Vector2.Zero; corners[1].uv = Vector2.Zero; corners[2].uv = Vector2.Zero; corners[3].uv = Vector2.Zero; // Draw an untexture rectangle _shapeRenderer.DrawRectangle(corners, null); } if (style.flags.HasFlag(TigTextStyleFlag.TTSF_BORDER)) { var topLeft = new Vector2(left - 1, top - 1); var bottomRight = new Vector2(right + 1, bottom + 1); _shapeRenderer.DrawRectangleOutline( topLeft, bottomRight, PackedLinearColorA.Black ); } }
internal LayoutRunIterator(Span <char> text, TigFont font, Rectangle extents, TigTextStyle style) : this() { this.text = text; this.font = font; this.extents = extents; this.style = style; glyphs = font.FontFace.Glyphs; state = 0; }
private void MeasureVanilla(TigFont font, TigTextStyle style, ReadOnlySpan <char> text, ref TigFontMetrics metrics) { if (metrics.width == 0 && text.Contains('\n')) { metrics.width = MeasureVanillaParagraph(font, style, text); } var largestHeight = font.FontFace.LargestHeight; if (metrics.width == 0) { metrics.width = MeasureVanillaLine(font, style, text); metrics.height = largestHeight; metrics.lines = 1; metrics.lineheight = largestHeight; return; } metrics.lines = 1; // Default if (metrics.height != 0) { var maxLines = metrics.height / largestHeight; if (!(style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))) { maxLines++; } if (maxLines != 1) { metrics.lines = CountLinesVanilla(metrics.width, maxLines, text, font, style); } } else { if (!(style.flags.HasFlag(TigTextStyleFlag.TTSF_TRUNCATE))) { metrics.lines = CountLinesVanilla(metrics.width, 0, text, font, style); } } if (metrics.height == 0) { metrics.height = metrics.lines * largestHeight; metrics.height -= -(font.FontFace.BaseLine - largestHeight); } metrics.lineheight = largestHeight; }
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];
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); }
public void Measure(TigFont font, TigTextStyle style, ReadOnlySpan <char> text, ref TigFontMetrics metrics) { // use the old font drawing algorithm MeasureVanilla(font, style, text, ref metrics); }
public void LayoutAndDraw(ReadOnlySpan <char> text, TigFont font, ref Rectangle extents, TigTextStyle style) { if (text.Length == 0) { return; } // Get the base text format and check if we should render using the new or old algorithms // Make the text mutable since vanilla drawing might change escape characters // within the text span. Span <char> mutableText = stackalloc char[text.Length]; text.CopyTo(mutableText); // use the old font drawing algorithm LayoutAndDrawVanilla(mutableText, font, ref extents, style); }
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); }