/// <summary> /// Measures the specified text string. Parameter bounds contains the bounds of the text.<br/> /// Measured values are returned in local coordinate space. /// </summary> /// <param name="bounds">Contains the bounds of the text when returned.</param> /// <returns>The horizontal advance of the measured text (i.e. where the next character should be drawn).</returns> public static float TextBounds(this Nvg nvg, Vector2D <float> pos, string @string, string end, out Rectangle <float> bounds) { bounds = default; Fontstash fons = nvg.fontManager.Fontstash; State state = nvg.stateStack.CurrentState; float scale = nvg.fontManager.GetFontScale() * nvg.pixelRatio.DevicePxRatio; float invscale = 1.0f / scale; if (state.FontId == Fontstash.INVALID) { return(0); } fons.SetSize(state.FontSize * scale); fons.SetSpacing(state.LetterSpacing * scale); fons.SetBlur(state.FontBlur * scale); fons.SetAlign((int)state.TextAlign); fons.SetFont(state.FontId); float width = fons.TextBounds(pos.X * scale, pos.Y * scale, @string, end, out float[] bs); if (bs != null) { fons.LineBounds(pos.Y * scale, out bs[1], out bs[3]); bounds = new Rectangle <float>() { Origin = new Vector2D <float>(bs[0] * invscale, bs[1] * invscale), Size = new Vector2D <float>((bs[2] - bs[0]) * invscale, (bs[3] - bs[1]) * invscale) }; } return(width * invscale); }
/// <summary> /// Calculates the glyph x positions of the specified text. Only the sub-string will be used.<br/> /// Measures values are returned in local coordinate space. /// </summary> public static int TextGlyphPositions(this Nvg nvg, Vector2D <float> pos, string @string, string end, out GlyphPosition[] positions, int maxRows) { positions = new GlyphPosition[maxRows]; Fontstash fons = nvg.fontManager.Fontstash; State state = nvg.stateStack.CurrentState; float scale = nvg.fontManager.GetFontScale() * nvg.pixelRatio.DevicePxRatio; float invscale = 1.0f / scale; FonsQuad q = new(); int npos = 0; if (state.FontId == Fontstash.INVALID) { return(0); } if (@string == end) { return(0); } fons.SetSize(state.FontSize * scale); fons.SetSpacing(state.LetterSpacing * scale); fons.SetBlur(state.FontBlur * scale); fons.SetAlign((int)state.TextAlign); fons.SetFont(state.FontId); fons.TextIterInit(out FonsTextIter iter, pos.X * scale, pos.Y * scale, @string, end, FonsGlyphBitmap.Optional); FonsTextIter prevIter = iter; while (fons.TextIterNext(ref iter, ref q)) { if (iter.prevGlyphIndex < 0 && nvg.fontManager.AllocTextAtlas()) { iter = prevIter; fons.TextIterNext(ref iter, ref q); } prevIter = iter; positions[npos++] = new GlyphPosition(iter.str, iter.x * invscale, MathF.Min(iter.x, q.x0) * invscale, MathF.Max(iter.nextx, q.x1) * invscale); if (npos >= maxRows) { return(npos); } } return(npos); }
public void Render(LevelSelectorStateRenderer renderer, Fontstash fons) { renderer.RenderQuad(_button.X, _button.Y, _button.Width, _button.Height, new Vector4(_colour, Hovering ? 1.0f : 0.75f)); float size = 32; float x = _button.X + size / 1.5f; float y = _button.Y + size * 1.5f; for (int i = 0; i < _data.bestRating; i++) { renderer.RenderTexturedQuad(_state.Star, x + i * size, y, size, -size); } renderer.PrepareLegacy(); fons.SetColour(0xFFFFFFFF); fons.SetFont(fons.GetFontByName("stdfont")); fons.SetSize(24.0f); fons.SetAlign((int)FonsAlign.Center | (int)FonsAlign.Middle); fons.DrawText(_button.X + _button.Width / 2.0f, _button.Y + _button.Height / 2.0f, _data.name); fons.SetSize(16.0f); fons.DrawText(_button.X + _button.Width / 2.0f, _button.Y + _button.Height / 2.0f + 35.0f, "Last Time: " + _data.bestTime.ToString()); renderer.EndLegacy(); }
/// <summary> /// Returns the vertical metrics based on the current text style.<br/> /// Measured values are returned in local coordinate space. /// </summary> public static void TextMetrics(this Nvg nvg, out float ascender, out float descender, out float lineh) { Fontstash fons = nvg.fontManager.Fontstash; State state = nvg.stateStack.CurrentState; float scale = nvg.fontManager.GetFontScale() * nvg.pixelRatio.DevicePxRatio; float invscale = 1.0f / scale; if (state.FontId == Fontstash.INVALID) { ascender = descender = lineh = -1.0f; return; } fons.SetSize(state.FontSize * scale); fons.SetSpacing(state.LetterSpacing * scale); fons.SetBlur(state.FontBlur * scale); fons.SetAlign((int)state.TextAlign); fons.SetFont(state.FontId); fons.VertMetrics(out ascender, out descender, out lineh); ascender *= invscale; descender *= invscale; lineh *= invscale; }
/// <summary> /// Breaks the specified text into lines. Only the sub-string will be used.<br/> /// White space is stripped at the beginning of the rows, the text is split at word boundaries or when new-line characters are encountered.<br/> /// Words longer than the max width are slit at nearest character (i.e. no hyphenation). /// </summary> public static int TextBreakLines(this Nvg nvg, string @string, string end, float breakRowWidth, out TextRow[] rows, int maxRows) { rows = new TextRow[maxRows]; Fontstash fons = nvg.fontManager.Fontstash; State state = nvg.stateStack.CurrentState; float scale = nvg.fontManager.GetFontScale() * nvg.pixelRatio.DevicePxRatio; float invscale = 1.0f / scale; FonsQuad q = new(); int nrows = 0; float rowStartX = 0.0f; float rowWidth = 0.0f; float rowMinX = 0.0f; float rowMaxX = 0.0f; string rowStart = null; string rowEnd = null; string wordStart = null; float wordStartX = 0.0f; float wordMinX = 0.0f; string breakEnd = null; float breakWidth = 0.0f; float breakMaxX = 0.0f; CodepointType type, pType = CodepointType.Space; uint pCodepoint = 0; if (maxRows == 0) { return(0); } if (state.FontId == Fontstash.INVALID) { return(0); } if (@string == end || @string.Length == 0) { return(0); } fons.SetSize(state.FontSize * scale); fons.SetSpacing(state.LetterSpacing * scale); fons.SetBlur(state.FontBlur * scale); fons.SetAlign((int)state.TextAlign); fons.SetFont(state.FontId); breakRowWidth *= scale; fons.TextIterInit(out FonsTextIter iter, 0, 0, @string, end, FonsGlyphBitmap.Optional); FonsTextIter prevIter = iter; while (fons.TextIterNext(ref iter, ref q)) { if (iter.prevGlyphIndex < 0 && nvg.fontManager.AllocTextAtlas()) { iter = prevIter; fons.TextIterNext(ref iter, ref q); } prevIter = iter; switch (iter.codepoint) { case 9: // \t case 11: // \v case 12: // \f case 32: // \space case 0x00a0: // NBSP type = CodepointType.Space; break; case 10: // \n type = pCodepoint == 13 ? CodepointType.Space : CodepointType.Newline; break; case 13: // \r type = pCodepoint == 10 ? CodepointType.Space : CodepointType.Newline; break; case 0x0085: // NEL type = CodepointType.Newline; break; default: if ((iter.codepoint >= 0x4E00 && iter.codepoint <= 0x9FFF) || (iter.codepoint >= 0x3000 && iter.codepoint <= 0x30FF) || (iter.codepoint >= 0xFF00 && iter.codepoint <= 0xFFEF) || (iter.codepoint >= 0x1100 && iter.codepoint <= 0x11FF) || (iter.codepoint >= 0x3130 && iter.codepoint <= 0x318F) || (iter.codepoint >= 0xAC00 && iter.codepoint <= 0xD7AF)) { type = CodepointType.CJKChar; } else { type = CodepointType.Char; } break; } if (type == CodepointType.Newline) { rows[nrows++] = new TextRow() { Start = rowStart ?? iter.str, End = rowEnd ?? iter.str, Width = rowWidth * invscale, MinX = rowMinX * invscale, MaxX = rowMaxX * invscale, Next = iter.next }; if (nrows >= maxRows) { return(nrows); } breakEnd = rowStart; breakWidth = 0.0f; breakMaxX = 0.0f; rowStart = null; rowEnd = null; rowWidth = 0.0f; rowMinX = rowMaxX = 0.0f; } else { if (rowStart == null) { if (type == CodepointType.Char || type == CodepointType.CJKChar) { rowStartX = iter.x; rowStart = iter.str; rowEnd = iter.next; rowWidth = iter.nextx - rowStartX; rowMinX = q.x0 - rowStartX; rowMaxX = q.x1 - rowStartX; wordStart = iter.str; wordStartX = iter.x; wordMinX = q.x0 - rowStartX; breakEnd = rowStart; breakWidth = 0.0f; breakMaxX = 0.0f; } } else { float nextWidth = iter.nextx - rowStartX; if (type == CodepointType.Char || type == CodepointType.CJKChar) { rowEnd = iter.next; rowWidth = iter.nextx - rowStartX; rowMaxX = q.x1 - rowStartX; } if (((pType == CodepointType.Char || pType == CodepointType.CJKChar) && type == CodepointType.Space) || type == CodepointType.CJKChar) { breakEnd = iter.str; breakWidth = rowWidth; breakMaxX = rowMaxX; } if ((pType == CodepointType.Space && (type == CodepointType.Char || type == CodepointType.CJKChar)) || type == CodepointType.CJKChar) { wordStart = iter.str; wordStartX = iter.x; wordMinX = q.x0; } if ((type == CodepointType.Char || type == CodepointType.CJKChar) && nextWidth > breakRowWidth) { if (breakEnd == rowStart) { rows[nrows++] = new TextRow() { Start = rowStart, End = iter.str, Width = rowWidth * invscale, MinX = rowMinX * invscale, MaxX = rowMaxX * invscale, Next = iter.str }; if (nrows >= maxRows) { return(nrows); } rowStartX = iter.x; rowStart = iter.str; rowEnd = iter.next; rowWidth = iter.nextx - rowStartX; rowMinX = q.x0 - rowStartX; rowMaxX = q.x1 - rowStartX; wordStart = iter.str; wordStartX = iter.x; wordMinX = q.x0 - rowStartX; } else { rows[nrows++] = new TextRow() { Start = rowStart, End = breakEnd, Width = breakWidth * invscale, MinX = rowMinX * invscale, MaxX = breakMaxX * invscale, Next = wordStart }; if (nrows >= maxRows) { return(nrows); } rowStartX = wordStartX; rowStart = wordStart; rowEnd = iter.next; rowWidth = iter.nextx - rowStartX; rowMinX = wordMinX - rowStartX; rowMaxX = q.x1 - rowStartX; } breakEnd = rowStart; breakWidth = 0.0f; breakMaxX = 0.0f; } } } pCodepoint = iter.codepoint; pType = type; } if (rowStart != null) { rows[nrows++] = new TextRow() { Start = rowStart, End = rowEnd, Width = rowWidth * invscale, MinX = rowMinX * invscale, MaxX = rowMaxX * invscale, Next = end }; } return(nrows); }
/// <summary> /// Measures the specified multi-text string.<br/> /// Measured values are returned in local space. /// </summary> /// <param name="bounds">Contains the bounds box of the multi-text when returned.</param> public static void TextBoxBounds(this Nvg nvg, Vector2D <float> pos, float breakRowWidth, string @string, string end, out Rectangle <float> bounds) { Fontstash fons = nvg.fontManager.Fontstash; State state = nvg.stateStack.CurrentState; float scale = nvg.fontManager.GetFontScale() * nvg.pixelRatio.DevicePxRatio; float invscale = 1.0f / scale; int nrows; Align oldAlign = state.TextAlign; Align hAlign = state.TextAlign & (Align.Left | Align.Centre | Align.Right); Align vAlign = state.TextAlign & (Align.Top | Align.Middle | Align.Bottom | Align.Baseline); if (state.FontId == Fontstash.INVALID) { bounds = default; return; } nvg.TextMetrics(out _, out _, out float lineh); state.TextAlign = Align.Left | vAlign; float minX = pos.X, maxX = pos.X; float minY = pos.Y, maxY = pos.Y; fons.SetSize(state.FontSize * scale); fons.SetSpacing(state.LetterSpacing * scale); fons.SetBlur(state.FontBlur * scale); fons.SetAlign((int)state.TextAlign); fons.SetFont(state.FontId); fons.LineBounds(0, out float rMinY, out float rMaxY); rMinY *= invscale; rMaxY *= invscale; while ((nrows = TextBreakLines(nvg, @string, end, breakRowWidth, out TextRow[] rows, 2)) != 0) { for (uint i = 0; i < nrows; i++) { float rMinX, rMaxX; float dx = 0.0f; if (hAlign.HasFlag(Align.Left)) { dx = 0.0f; } else if (hAlign.HasFlag(Align.Centre)) { dx = breakRowWidth * 0.5f - rows[i].Width * 0.5f; } else if (hAlign.HasFlag(Align.Right)) { dx = breakRowWidth - rows[i].Width; } rMinX = pos.X + rows[i].MinX + dx; rMaxX = pos.X + rows[i].MaxX + dx; minX = MathF.Min(minX, rMinX); maxX = MathF.Max(maxX, rMaxX); minY = MathF.Min(minY, pos.Y + rMinY); maxY = MathF.Max(maxY, pos.Y + rMaxY); pos.Y += lineh * state.LineHeight; } @string = rows[nrows - 1].Next; } state.TextAlign = oldAlign; bounds = new Rectangle <float>(new Vector2D <float>(minX, minY), new Vector2D <float>(maxX, maxY) - new Vector2D <float>(minX, minY)); }
/// <summary> /// Draws text string at specified location. Only the sub-string up to the end is drawn. /// </summary> public static float Text(this Nvg nvg, Vector2D <float> pos, string @string, string end) { Fontstash fons = nvg.fontManager.Fontstash; State state = nvg.stateStack.CurrentState; FonsQuad q = new(); float scale = nvg.fontManager.GetFontScale() * nvg.pixelRatio.DevicePxRatio; float invscale = 1.0f / scale; bool isFlipped = Maths.IsTransformFlipped(state.Transform); if (state.FontId == Fontstash.INVALID) { return(pos.X); } fons.SetSize(state.FontSize * scale); fons.SetSpacing(state.LetterSpacing * scale); fons.SetBlur(state.FontBlur * scale); fons.SetAlign((int)state.TextAlign); fons.SetFont(state.FontId); List <Vertex> vertices = new(); fons.TextIterInit(out FonsTextIter iter, pos.X * scale, pos.Y * scale, @string, end, FonsGlyphBitmap.Requiered); FonsTextIter prevIter = iter; while (fons.TextIterNext(ref iter, ref q)) { Vector2D <float>[] c = new Vector2D <float> [4]; if (iter.prevGlyphIndex == -1) { if (vertices.Count != 0) { nvg.fontManager.RenderText(vertices); vertices.Clear(); } if (!nvg.fontManager.AllocTextAtlas()) // no memory { break; } iter = prevIter; _ = fons.TextIterNext(ref iter, ref q); if (iter.prevGlyphIndex == -1) { break; } } prevIter = iter; if (isFlipped) { float tmp = q.y0; q.y0 = q.y1; q.y1 = tmp; tmp = q.t0; q.t0 = q.t1; q.t1 = tmp; } c[0] = nvg.TransformPoint(state.Transform, q.x0 * invscale, q.y0 * invscale); c[1] = nvg.TransformPoint(state.Transform, q.x1 * invscale, q.y0 * invscale); c[2] = nvg.TransformPoint(state.Transform, q.x1 * invscale, q.y1 * invscale); c[3] = nvg.TransformPoint(state.Transform, q.x0 * invscale, q.y1 * invscale); vertices.Add(new Vertex(c[0], q.s0, q.t0)); vertices.Add(new Vertex(c[2], q.s1, q.t1)); vertices.Add(new Vertex(c[1], q.s1, q.t0)); vertices.Add(new Vertex(c[0], q.s0, q.t0)); vertices.Add(new Vertex(c[3], q.s0, q.t1)); vertices.Add(new Vertex(c[2], q.s1, q.t1)); } nvg.fontManager.FlushTextTexture(); nvg.fontManager.RenderText(vertices); return(iter.nextx / scale); }