diff --git a/sources/engine/Stride.Graphics/SpriteBatch.cs b/sources/engine/Stride.Graphics/SpriteBatch.cs index 48a03f50cc..9fd3b0e924 100644 --- a/sources/engine/Stride.Graphics/SpriteBatch.cs +++ b/sources/engine/Stride.Graphics/SpriteBatch.cs @@ -508,7 +508,7 @@ private void DrawString(SpriteFont spriteFont, ref SpriteFont.StringProxy text, drawCommand.Position.X /= resolutionRatio.X; drawCommand.Position.Y /= resolutionRatio.Y; - spriteFont.InternalDraw(commandList, ref text, ref drawCommand, alignment); + spriteFont.InternalDraw(commandList, text, ref drawCommand, alignment); } internal unsafe void DrawSprite(Texture texture, ref RectangleF destination, bool scaleDestination, ref RectangleF? sourceRectangle, Color4 color, Color4 colorAdd, diff --git a/sources/engine/Stride.Graphics/SpriteFont.cs b/sources/engine/Stride.Graphics/SpriteFont.cs index 319f6de04b..1e45b54132 100644 --- a/sources/engine/Stride.Graphics/SpriteFont.cs +++ b/sources/engine/Stride.Graphics/SpriteFont.cs @@ -2,11 +2,13 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; +using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using Stride.Core; +using Stride.Core.Annotations; using Stride.Core.Diagnostics; using Stride.Core.Mathematics; using Stride.Core.Serialization; @@ -60,16 +62,6 @@ public class SpriteFont : ComponentBase protected SwizzleMode swizzle; private FontSystem fontSystem; - private readonly GlyphAction internalDrawGlyphAction; - private readonly GlyphAction internalUIDrawGlyphAction; - private readonly GlyphAction measureStringGlyphAction; - - protected internal SpriteFont() - { - internalDrawGlyphAction = InternalDrawGlyph; - internalUIDrawGlyphAction = InternalUIDrawGlyph; - measureStringGlyphAction = MeasureStringGlyph; - } /// /// Gets the textures containing the font character data. @@ -144,11 +136,6 @@ protected override void Destroy() // unregister itself from its managing system FontSystem.AllocatedSpriteFonts.Remove(this); } - - public interface IFontManager - { - void New(); - } /// /// Get the value of the extra line spacing for the given font size. @@ -199,16 +186,21 @@ public float GetTotalLineSpacing(float fontSize) return GetExtraLineSpacing(fontSize) + GetFontDefaultLineSpacing(fontSize); } - internal void InternalDraw(CommandList commandList, ref StringProxy text, ref InternalDrawCommand drawCommand, TextAlignment alignment) + internal void InternalDraw(CommandList commandList, in StringProxy text, ref InternalDrawCommand drawCommand, TextAlignment alignment) { // If the text is mirrored, offset the start position accordingly. if (drawCommand.SpriteEffects != SpriteEffects.None) { - drawCommand.Origin -= MeasureString(ref text, ref drawCommand.FontSize) * AxisIsMirroredTable[(int)drawCommand.SpriteEffects & 3]; + drawCommand.Origin -= MeasureString(text, drawCommand.FontSize) * AxisIsMirroredTable[(int)drawCommand.SpriteEffects & 3]; } + (TextAlignment alignment, Vector2 textboxSize)? scanOption = alignment == TextAlignment.Left ? null : (alignment, MeasureString(text, drawCommand.FontSize)); + // Draw each character in turn. - ForEachGlyph(commandList, ref text, ref drawCommand.FontSize, internalDrawGlyphAction, ref drawCommand, alignment, true); + foreach (var glyphInfo in new GlyphEnumerator(commandList, text, drawCommand.FontSize, true, 0, text.Length, this, scanOption)) + { + InternalDrawGlyph(ref drawCommand, in drawCommand.FontSize, glyphInfo); + } } /// @@ -226,14 +218,14 @@ internal virtual void PreGenerateGlyphs(ref StringProxy text, ref Vector2 size) { } - internal void InternalDrawGlyph(ref InternalDrawCommand parameters, in Vector2 fontSize, in Glyph glyph, float x, float y, float nextx, ref Vector2 auxiliaryScaling) + internal void InternalDrawGlyph(ref InternalDrawCommand parameters, in Vector2 fontSize, in GlyphPosition glyphPosition) { - if (char.IsWhiteSpace((char)glyph.Character) || glyph.Subrect.Width == 0 || glyph.Subrect.Height == 0) + if (char.IsWhiteSpace((char)glyphPosition.Glyph.Character) || glyphPosition.Glyph.Subrect.Width == 0 || glyphPosition.Glyph.Subrect.Height == 0) return; var spriteEffects = parameters.SpriteEffects; - var offset = new Vector2(x, y + GetBaseOffsetY(fontSize.Y) + glyph.Offset.Y); + var offset = new Vector2(glyphPosition.X, glyphPosition.Y + GetBaseOffsetY(fontSize.Y) + glyphPosition.Glyph.Offset.Y); Vector2.Modulate(ref offset, ref AxisDirectionTable[(int)spriteEffects & 3], out offset); Vector2.Add(ref offset, ref parameters.Origin, out offset); offset.X = MathF.Round(offset.X); @@ -242,39 +234,41 @@ internal void InternalDrawGlyph(ref InternalDrawCommand parameters, in Vector2 f if (spriteEffects != SpriteEffects.None) { // For mirrored characters, specify bottom and/or right instead of top left. - var glyphRect = new Vector2(glyph.Subrect.Right - glyph.Subrect.Left, glyph.Subrect.Top - glyph.Subrect.Bottom); + var glyphRect = new Vector2(glyphPosition.Glyph.Subrect.Right - glyphPosition.Glyph.Subrect.Left, glyphPosition.Glyph.Subrect.Top - glyphPosition.Glyph.Subrect.Bottom); Vector2.Modulate(ref glyphRect, ref AxisIsMirroredTable[(int)spriteEffects & 3], out offset); } var destination = new RectangleF(parameters.Position.X, parameters.Position.Y, parameters.Scale.X, parameters.Scale.Y); - RectangleF? sourceRectangle = glyph.Subrect; - parameters.SpriteBatch.DrawSprite(Textures[glyph.BitmapIndex], ref destination, true, ref sourceRectangle, parameters.Color, new Color4(0, 0, 0, 0), parameters.Rotation, ref offset, spriteEffects, ImageOrientation.AsIs, parameters.Depth, swizzle, true); + RectangleF? sourceRectangle = glyphPosition.Glyph.Subrect; + parameters.SpriteBatch.DrawSprite(Textures[glyphPosition.Glyph.BitmapIndex], ref destination, true, ref sourceRectangle, parameters.Color, new Color4(0, 0, 0, 0), parameters.Rotation, ref offset, spriteEffects, ImageOrientation.AsIs, parameters.Depth, swizzle, true); } - internal void InternalUIDraw(CommandList commandList, ref StringProxy text, ref InternalUIDrawCommand drawCommand) + internal void InternalUIDraw(CommandList commandList, in StringProxy text, ref InternalUIDrawCommand drawCommand, in Vector2 actualFontSize) { // We don't want to have letters with non uniform ratio - var requestedFontSize = new Vector2(drawCommand.RequestedFontSize * drawCommand.RealVirtualResolutionRatio.Y); var textBoxSize = drawCommand.TextBoxSize * drawCommand.RealVirtualResolutionRatio; - ForEachGlyph(commandList, ref text, ref requestedFontSize, internalUIDrawGlyphAction, ref drawCommand, drawCommand.Alignment, true, textBoxSize); + foreach (var glyphInfo in new GlyphEnumerator(commandList, text, actualFontSize, true, 0, text.Length, this, (drawCommand.Alignment, textBoxSize))) + { + InternalUIDrawGlyph(ref drawCommand, in actualFontSize, glyphInfo); + } } - internal void InternalUIDrawGlyph(ref InternalUIDrawCommand parameters, in Vector2 requestedFontSize, in Glyph glyph, float x, float y, float nextx, ref Vector2 auxiliaryScaling) + internal void InternalUIDrawGlyph(ref InternalUIDrawCommand parameters, in Vector2 requestedFontSize, in GlyphPosition glyphPosition) { - if (char.IsWhiteSpace((char)glyph.Character)) + if (char.IsWhiteSpace((char)glyphPosition.Glyph.Character)) return; var realVirtualResolutionRatio = requestedFontSize / parameters.RequestedFontSize; // Skip items with null size var elementSize = new Vector2( - auxiliaryScaling.X * glyph.Subrect.Width / realVirtualResolutionRatio.X, - auxiliaryScaling.Y * glyph.Subrect.Height / realVirtualResolutionRatio.Y); + glyphPosition.AuxiliaryScaling.X * glyphPosition.Glyph.Subrect.Width / realVirtualResolutionRatio.X, + glyphPosition.AuxiliaryScaling.Y * glyphPosition.Glyph.Subrect.Height / realVirtualResolutionRatio.Y); if (elementSize.LengthSquared() < MathUtil.ZeroTolerance) return; - var xShift = x; - var yShift = y + (GetBaseOffsetY(requestedFontSize.Y) + glyph.Offset.Y * auxiliaryScaling.Y); + var xShift = glyphPosition.X; + var yShift = glyphPosition.Y + (GetBaseOffsetY(requestedFontSize.Y) + glyphPosition.Glyph.Offset.Y * glyphPosition.AuxiliaryScaling.Y); if (parameters.SnapText) { xShift = MathF.Round(xShift); @@ -299,8 +293,32 @@ internal void InternalUIDrawGlyph(ref InternalUIDrawCommand parameters, in Vecto worldMatrix.M23 *= elementSize.Y; worldMatrix.M24 *= elementSize.Y; - RectangleF sourceRectangle = glyph.Subrect; - parameters.Batch.DrawCharacter(Textures[glyph.BitmapIndex], in worldMatrix, in sourceRectangle, in parameters.Color, parameters.DepthBias, swizzle); + RectangleF sourceRectangle = glyphPosition.Glyph.Subrect; + parameters.Batch.DrawCharacter(Textures[glyphPosition.Glyph.BitmapIndex], in worldMatrix, in sourceRectangle, in parameters.Color, parameters.DepthBias, swizzle); + } + + public int IndexInString([NotNull] string text, in Vector2 fontSize, Vector2 pointOnText, (TextAlignment text, Vector2 boxSize)? scanOption) + { + pointOnText.Y -= GetTotalLineSpacing(fontSize.Y); // Characters go from 0->+Y downwards, + var proxy = new StringProxy(text, text.Length); + (int index, float score, float x) = (0, float.PositiveInfinity, 0); + foreach (var glyphInfo in new GlyphEnumerator(null, proxy, fontSize, false, 0, text.Length, this, scanOption)) + { + var sqrd = Vector2.DistanceSquared(new Vector2(glyphInfo.X, glyphInfo.Y), pointOnText); + if (sqrd < score) + { + index = glyphInfo.Index; + score = sqrd; + x = glyphInfo.X * 0.5f + glyphInfo.NextX * 0.5f; + } + } + + if (index == text.Length - 1 && x < pointOnText.X) + { + return text.Length; + } + + return index; } /// @@ -308,7 +326,7 @@ internal void InternalUIDrawGlyph(ref InternalUIDrawCommand parameters, in Vecto /// /// The string to measure. /// Vector2. - public Vector2 MeasureString(string text) + public Vector2 MeasureString([NotNull] string text) { var fontSize = new Vector2(Size, Size); return MeasureString(text, fontSize, text.Length); @@ -319,7 +337,7 @@ public Vector2 MeasureString(string text) /// /// The string to measure. /// Vector2. - public Vector2 MeasureString(StringBuilder text) + public Vector2 MeasureString([NotNull] StringBuilder text) { var fontSize = new Vector2(Size, Size); return MeasureString(text, fontSize, text.Length); @@ -331,7 +349,7 @@ public Vector2 MeasureString(StringBuilder text) /// The string to measure. /// The size of the font (ignored in the case of static fonts) /// Vector2. - public Vector2 MeasureString(string text, float fontSize) + public Vector2 MeasureString([NotNull] string text, float fontSize) { return MeasureString(text, new Vector2(fontSize, fontSize), text.Length); } @@ -342,7 +360,7 @@ public Vector2 MeasureString(string text, float fontSize) /// The string to measure. /// The size of the font (ignored in the case of static fonts) /// Vector2. - public Vector2 MeasureString(StringBuilder text, float fontSize) + public Vector2 MeasureString([NotNull] StringBuilder text, float fontSize) { return MeasureString(text, new Vector2(fontSize, fontSize), text.Length); } @@ -353,7 +371,7 @@ public Vector2 MeasureString(StringBuilder text, float fontSize) /// The string to measure. /// The size of the font (ignored in the case of static fonts) /// Vector2. - public Vector2 MeasureString(string text, Vector2 fontSize) + public Vector2 MeasureString([NotNull] string text, Vector2 fontSize) { return MeasureString(text, ref fontSize, text.Length); } @@ -364,7 +382,7 @@ public Vector2 MeasureString(string text, Vector2 fontSize) /// The string to measure. /// The size of the font (ignored in the case of static fonts) /// Vector2. - public Vector2 MeasureString(StringBuilder text, Vector2 fontSize) + public Vector2 MeasureString([NotNull] StringBuilder text, Vector2 fontSize) { return MeasureString(text, ref fontSize, text.Length); } @@ -375,7 +393,7 @@ public Vector2 MeasureString(StringBuilder text, Vector2 fontSize) /// The string to measure. /// The size of the font (ignored in the case of static fonts) /// Vector2. - public Vector2 MeasureString(string text, ref Vector2 fontSize) + public Vector2 MeasureString([NotNull] string text, ref Vector2 fontSize) { return MeasureString(text, ref fontSize, text.Length); } @@ -386,7 +404,7 @@ public Vector2 MeasureString(string text, ref Vector2 fontSize) /// The string to measure. /// The size of the font (ignored in the case of static fonts) /// Vector2. - public Vector2 MeasureString(StringBuilder text, ref Vector2 fontSize) + public Vector2 MeasureString([NotNull] StringBuilder text, ref Vector2 fontSize) { return MeasureString(text, ref fontSize, text.Length); } @@ -398,7 +416,7 @@ public Vector2 MeasureString(StringBuilder text, ref Vector2 fontSize) /// The size of the font (ignored in the case of static fonts) /// The length of the string to measure /// Vector2. - public Vector2 MeasureString(string text, Vector2 fontSize, int length) + public Vector2 MeasureString([NotNull] string text, Vector2 fontSize, int length) { return MeasureString(text, ref fontSize, length); } @@ -410,7 +428,7 @@ public Vector2 MeasureString(string text, Vector2 fontSize, int length) /// The size of the font (ignored in the case of static fonts) /// The length of the string to measure /// Vector2. - public Vector2 MeasureString(StringBuilder text, Vector2 fontSize, int length) + public Vector2 MeasureString([NotNull] StringBuilder text, Vector2 fontSize, int length) { return MeasureString(text, ref fontSize, length); } @@ -422,13 +440,13 @@ public Vector2 MeasureString(StringBuilder text, Vector2 fontSize, int length) /// The size of the font (ignored in the case of static fonts) /// The length of the string to measure /// Vector2. - public Vector2 MeasureString(string text, ref Vector2 fontSize, int length) + public Vector2 MeasureString([NotNull] string text, ref Vector2 fontSize, int length) { if (text == null) throw new ArgumentNullException(nameof(text)); var proxy = new StringProxy(text, length); - return MeasureString(ref proxy, ref fontSize); + return MeasureString(proxy, fontSize); } /// @@ -438,19 +456,22 @@ public Vector2 MeasureString(string text, ref Vector2 fontSize, int length) /// The size of the font (ignored in the case of static fonts) /// The length of the string to measure /// Vector2. - public Vector2 MeasureString(StringBuilder text, ref Vector2 fontSize, int length) + public Vector2 MeasureString([NotNull] StringBuilder text, ref Vector2 fontSize, int length) { if (text == null) throw new ArgumentNullException(nameof(text)); var proxy = new StringProxy(text, length); - return MeasureString(ref proxy, ref fontSize); + return MeasureString(proxy, fontSize); } - internal Vector2 MeasureString(ref StringProxy text, ref Vector2 size) + internal Vector2 MeasureString(in StringProxy text, in Vector2 size) { var result = Vector2.Zero; - ForEachGlyph(null, ref text, ref size, measureStringGlyphAction, ref result, TextAlignment.Left, false); // text size is independent from the text alignment + foreach (var glyphInfo in new GlyphEnumerator(null, text, size, false, 0, text.Length, this)) + { + MeasureStringGlyph(ref result, in size, glyphInfo); + } return result; } @@ -464,6 +485,30 @@ public virtual bool IsCharPresent(char c) return false; } + internal void TypeSpecificRatios(float requestedFontSize, ref bool snapText, ref Vector2 realVirtualResolutionRatio, out Vector2 actualFontSize) + { + if (FontType == SpriteFontType.SDF) + { + snapText = false; + float scaling = requestedFontSize / Size; + realVirtualResolutionRatio = 1 / new Vector2(scaling, scaling); + } + if (FontType == SpriteFontType.Static) + { + realVirtualResolutionRatio = Vector2.One; // ensure that static font are not scaled internally + } + if (FontType == SpriteFontType.Dynamic) + { + // Dynamic: if we're not displaying in a situation where we can snap text, we're probably in 3D. + // Let's use virtual resolution (otherwise requested size might change on every camera move) + // TODO: some step function to have LOD without regenerating on every small change? + if (!snapText) + realVirtualResolutionRatio = Vector2.One; + } + + actualFontSize = new Vector2(realVirtualResolutionRatio.Y * requestedFontSize); // we don't want letters non-uniform ratio + } + /// /// Return the glyph associated to provided character at the given size. /// @@ -479,13 +524,13 @@ protected virtual Glyph GetGlyph(CommandList commandList, char character, in Vec return null; } - private void MeasureStringGlyph(ref Vector2 result, in Vector2 fontSize, in Glyph glyph, float x, float y, float nextx, ref Vector2 auxiliaryScaling) + internal void MeasureStringGlyph(ref Vector2 result, in Vector2 fontSize, in GlyphPosition glyphPosition) { // TODO Do we need auxiliaryScaling - var h = y + GetTotalLineSpacing(fontSize.Y); - if (nextx > result.X) + var h = glyphPosition.Y + GetTotalLineSpacing(fontSize.Y); + if (glyphPosition.NextX > result.X) { - result.X = nextx; + result.X = glyphPosition.NextX; } if (h > result.Y) { @@ -493,140 +538,160 @@ private void MeasureStringGlyph(ref Vector2 result, in Vector2 fontSize, in Glyp } } - private delegate void GlyphAction(ref T parameters, in Vector2 fontSize, in Glyph glyph, float x, float y, float nextx, ref Vector2 auxiliaryScaling); + public record struct GlyphPosition(Glyph Glyph, float X, float Y, float NextX, int Index, Vector2 AuxiliaryScaling) + { + public Vector2 Position => new(X, Y); + } + + internal struct GlyphEnumerator : IEnumerator, IEnumerable + { + private int index; + private int key; + private float x; + private float y; + private int forEnd; + private Vector2 textboxSize; + private GlyphPosition current; + private readonly bool updateGpuResources; + private readonly TextAlignment scanOrder; + private readonly StringProxy text; + private readonly Vector2 fontSize; + [CanBeNull] private readonly CommandList commandList; + [NotNull] private readonly SpriteFont font; + + public GlyphEnumerator( + [CanBeNull] CommandList commandList, + StringProxy text, + Vector2 fontSize, + bool updateGpuResources, + int forStart, + int forEnd, + [NotNull] SpriteFont font, + (TextAlignment alignment, Vector2 textboxSize)? scanOptions = null) + { + this.commandList = commandList; + this.text = text; + this.fontSize = fontSize; + this.updateGpuResources = updateGpuResources; + this.forEnd = forEnd; + this.font = font; + this.scanOrder = scanOptions?.alignment ?? TextAlignment.Left; + textboxSize = scanOptions?.textboxSize ?? default; + index = forStart; + y = 0; + x = FindHorizontalOffset(index); + } - private static int FindCariageReturn(ref StringProxy text, int startIndex) - { - var index = startIndex; + public bool MoveNext() + { + while (index < forEnd) + { + char character = text[index]; + index++; - while (index < text.Length && text[index] != '\n') - ++index; + if (character == '\r') + continue; - return index; - } + var currentKey = key; + key |= character; + key = (key << 16); + + switch (character) + { + case '\n': + x = FindHorizontalOffset(index); + y += font.GetTotalLineSpacing(fontSize.Y); + break; + + default: + Vector2 auxiliaryScaling; + var glyph = font.GetGlyph(commandList, character, in fontSize, updateGpuResources, out auxiliaryScaling); + if (glyph == null && !font.IgnoreUnkownCharacters && font.DefaultCharacter.HasValue) + glyph = font.GetGlyph(commandList, font.DefaultCharacter.Value, in fontSize, updateGpuResources, out auxiliaryScaling); + if (glyph == null) + break; + + var dx = glyph.Offset.X; + if (font.KerningMap != null && font.KerningMap.TryGetValue(currentKey, out var kerningOffset)) + dx += kerningOffset; + + float nextX = x + (glyph.XAdvance + font.GetExtraSpacing(fontSize.X)) * auxiliaryScaling.X; + current = new(glyph, x + dx * auxiliaryScaling.X, y, nextX, index - 1, auxiliaryScaling); + x = nextX; + return true; + } + } - private void ForEachGlyph(CommandList commandList, ref StringProxy text, ref Vector2 requestedFontSize, GlyphAction action, ref T parameters, TextAlignment scanOrder, bool updateGpuResources, Vector2? textBoxSize = null) - { - if (scanOrder == TextAlignment.Left) - { - // scan the whole text only one time following the text letter order - ForGlyph(commandList, ref text, ref requestedFontSize, action, ref parameters, 0, text.Length, updateGpuResources); + return false; } - else + + private float FindHorizontalOffset(int scanStart) { - // scan the text line by line incrementing y start position + if (scanOrder == TextAlignment.Left) + { + return 0; + } - // measure the whole string in order to be able to determine xStart - var wholeSize = textBoxSize ?? MeasureString(ref text, ref requestedFontSize); + var nextLine = scanStart; + while (nextLine < text.Length && text[nextLine] != '\n') + ++nextLine; - // scan the text line by line - var yStart = 0f; - var startIndex = 0; - var endIndex = FindCariageReturn(ref text, 0); - while (startIndex < text.Length) + var lineSize = Vector2.Zero; + foreach (var glyphInfo in new GlyphEnumerator(commandList, text, fontSize, updateGpuResources, scanStart, nextLine, font)) { - // measure the size of the current line - var lineSize = Vector2.Zero; - ForGlyph(commandList, ref text, ref requestedFontSize, MeasureStringGlyph, ref lineSize, startIndex, endIndex, updateGpuResources); - - // Determine the start position of the line along the x axis - // We round this value to the closest integer to force alignment of all characters to the same pixels - // Otherwise the starting offset can fall just in between two pixels and due to float imprecision - // some characters can be aligned to the pixel before and others to the pixel after, resulting in gaps and character overlapping - var xStart = (scanOrder == TextAlignment.Center) ? (wholeSize.X - lineSize.X) / 2 : wholeSize.X - lineSize.X; - xStart = MathF.Round(xStart); - - // scan the line - ForGlyph(commandList, ref text, ref requestedFontSize, action, ref parameters, startIndex, endIndex, updateGpuResources, xStart, yStart); - - // update variable before going to next line - yStart += GetTotalLineSpacing(requestedFontSize.Y); - startIndex = endIndex + 1; - endIndex = FindCariageReturn(ref text, startIndex); + font.MeasureStringGlyph(ref lineSize, in fontSize, glyphInfo); } + + // Determine the start position of the line along the x axis + // We round this value to the closest integer to force alignment of all characters to the same pixels + // Otherwise the starting offset can fall just in between two pixels and due to float imprecision + // some characters can be aligned to the pixel before and others to the pixel after, resulting in gaps and character overlapping + var xStart = (scanOrder == TextAlignment.Center) ? (textboxSize.X - lineSize.X) / 2 : textboxSize.X - lineSize.X; + xStart = MathF.Round(xStart); + return xStart; } - } - private void ForGlyph(CommandList commandList, ref StringProxy text, ref Vector2 fontSize, GlyphAction action, ref T parameters, int forStart, int forEnd, bool updateGpuResources, float startX = 0, float startY = 0) - { - var key = 0; - var x = startX; - var y = startY; - for (var i = forStart; i < forEnd; i++) - { - var character = text[i]; + public void Reset() => throw new NotSupportedException(); - switch (character) - { - case '\r': - // Skip carriage returns. - key |= character; - continue; + public void Dispose() { } - case '\n': - // New line. - x = 0; - y += GetTotalLineSpacing(fontSize.Y); - key |= character; - break; - - default: - // Output this character. - Vector2 auxiliaryScaling; - var glyph = GetGlyph(commandList, character, in fontSize, updateGpuResources, out auxiliaryScaling); - if (glyph == null && !IgnoreUnkownCharacters && DefaultCharacter.HasValue) - glyph = GetGlyph(commandList, DefaultCharacter.Value, in fontSize, updateGpuResources, out auxiliaryScaling); - if (glyph == null) - continue; - - key |= character; - - var dx = glyph.Offset.X; - - float kerningOffset; - if (KerningMap != null && KerningMap.TryGetValue(key, out kerningOffset)) - dx += kerningOffset; - - float nextX = x + (glyph.XAdvance + GetExtraSpacing(fontSize.X)) * auxiliaryScaling.X; - action(ref parameters, in fontSize, in glyph, x + dx * auxiliaryScaling.X, y, nextX, ref auxiliaryScaling); - x = nextX; - break; - } + public GlyphPosition Current => current; + [NotNull] object IEnumerator.Current => current; - // Shift the kerning key - key = (key << 16); - } + public GlyphEnumerator GetEnumerator() => this; + IEnumerator IEnumerable.GetEnumerator() => this; + IEnumerator IEnumerable.GetEnumerator() => this; } [StructLayout(LayoutKind.Sequential)] - internal struct StringProxy + internal readonly struct StringProxy { private readonly string textString; private readonly StringBuilder textBuilder; public readonly int Length; - public StringProxy(string text) + public StringProxy([NotNull] string text) { textString = text; textBuilder = null; Length = text.Length; } - public StringProxy(StringBuilder text) + public StringProxy([NotNull] StringBuilder text) { textBuilder = text; textString = null; Length = text.Length; } - public StringProxy(string text, int length) + public StringProxy([NotNull] string text, int length) { textString = text; textBuilder = null; Length = Math.Max(0, Math.Min(length, text.Length)); } - public StringProxy(StringBuilder text, int length) + public StringProxy([NotNull] StringBuilder text, int length) { textBuilder = text; textString = null; diff --git a/sources/engine/Stride.Graphics/UIBatch.cs b/sources/engine/Stride.Graphics/UIBatch.cs index 8fd25707e7..09cca7c4e4 100644 --- a/sources/engine/Stride.Graphics/UIBatch.cs +++ b/sources/engine/Stride.Graphics/UIBatch.cs @@ -3,7 +3,7 @@ using System; using System.Runtime.InteropServices; - +using Stride.Core.Annotations; using Stride.Core.Mathematics; using Stride.Rendering; @@ -426,7 +426,7 @@ internal void DrawCharacter(Texture texture, in Matrix worldViewProjectionMatrix Draw(texture, in elementInfo); } - internal void DrawString(SpriteFont font, string text, ref SpriteFont.InternalUIDrawCommand drawCommand) + internal void DrawString([NotNull] SpriteFont font, [NotNull] string text, ref SpriteFont.InternalUIDrawCommand drawCommand) { if (font == null) throw new ArgumentNullException(nameof(font)); if (text == null) throw new ArgumentNullException(nameof(text)); @@ -443,24 +443,7 @@ internal void DrawString(SpriteFont font, string text, ref SpriteFont.InternalUI // transform the world matrix into the world view project matrix Matrix.Multiply(ref worldMatrix, ref viewProjectionMatrix, out drawCommand.Matrix); - if (font.FontType == SpriteFontType.SDF) - { - drawCommand.SnapText = false; - float scaling = drawCommand.RequestedFontSize / font.Size; - drawCommand.RealVirtualResolutionRatio = 1 / new Vector2(scaling, scaling); - } - if (font.FontType == SpriteFontType.Static) - { - drawCommand.RealVirtualResolutionRatio = Vector2.One; // ensure that static font are not scaled internally - } - if (font.FontType == SpriteFontType.Dynamic) - { - // Dynamic: if we're not displaying in a situation where we can snap text, we're probably in 3D. - // Let's use virtual resolution (otherwise requested size might change on every camera move) - // TODO: some step function to have LOD without regenerating on every small change? - if (!drawCommand.SnapText) - drawCommand.RealVirtualResolutionRatio = Vector2.One; - } + font.TypeSpecificRatios(drawCommand.RequestedFontSize, ref drawCommand.SnapText, ref drawCommand.RealVirtualResolutionRatio, out var actualFontSize); // snap draw start position to prevent characters to be drawn in between two pixels if (drawCommand.SnapText) @@ -477,7 +460,7 @@ internal void DrawString(SpriteFont font, string text, ref SpriteFont.InternalUI drawCommand.Matrix.M42 /= invW; } - font.InternalUIDraw(GraphicsContext.CommandList, ref proxy, ref drawCommand); + font.InternalUIDraw(GraphicsContext.CommandList, proxy, ref drawCommand, actualFontSize); } protected override unsafe void UpdateBufferValuesFromElementInfo(ref ElementInfo elementInfo, IntPtr vertexPtr, IntPtr indexPtr, int vertexOffset) diff --git a/sources/engine/Stride.UI/Controls/EditText.Direct.cs b/sources/engine/Stride.UI/Controls/EditText.Direct.cs index cf7ece964d..5d6a7cba1a 100644 --- a/sources/engine/Stride.UI/Controls/EditText.Direct.cs +++ b/sources/engine/Stride.UI/Controls/EditText.Direct.cs @@ -15,7 +15,7 @@ public partial class EditText { private void OnTouchMoveImpl(TouchEventArgs args) { - var currentPosition = FindNearestCharacterIndex(new Vector2(args.WorldPosition.X - WorldMatrix.M41, args.WorldPosition.Y - WorldMatrix.M42)); + var currentPosition = FindNearestCharacterIndex(args.WorldPosition.XY()); if (caretAtStart) { @@ -36,62 +36,32 @@ private void OnTouchMoveImpl(TouchEventArgs args) private void OnTouchDownImpl(TouchEventArgs args) { // Find the appropriate position for the caret. - CaretPosition = FindNearestCharacterIndex(new Vector2(args.WorldPosition.X - WorldMatrix.M41, args.WorldPosition.Y - WorldMatrix.M42)); + CaretPosition = FindNearestCharacterIndex(args.WorldPosition.XY()); } /// - /// Find the index of the nearest character to the provided position. + /// Find the index of the nearest character to the provided position in space /// - /// The position in edit text space - /// The 0-based index of the nearest character - protected virtual int FindNearestCharacterIndex(Vector2 position) + protected virtual int FindNearestCharacterIndex(Vector2 worldPosition) { if (Font == null) return 0; - var textRegionSize = (ActualWidth - Padding.Left - Padding.Right); - var fontScale = LayoutingContext.RealVirtualResolutionRatio; - var fontSize = new Vector2(fontScale.Y * ActualTextSize); // we don't want letters non-uniform ratio + var textRegion = GetTextRegionSize(); + var regionHalf = textRegion / 2; + var worldMatrix = WorldMatrix; - // calculate the offset of the beginning of the text due to text alignment - var alignmentOffset = -textRegionSize / 2f; - if (TextAlignment != TextAlignment.Left) - { - var textWidth = Font.MeasureString(TextToDisplay, ref fontSize).X; - if (Font.FontType == SpriteFontType.Dynamic) - textWidth /= fontScale.X; - - alignmentOffset = TextAlignment == TextAlignment.Center ? -textWidth / 2 : -textRegionSize / 2f + (textRegionSize - textWidth); - } - var touchInText = position.X - alignmentOffset; + var offset = worldMatrix.TranslationVector; + // Text draws from the upper left corner of the rect, let's account for that + offset -= worldMatrix.Right * regionHalf.X - worldMatrix.Up * regionHalf.Y; + worldPosition -= offset.XY(); - // Find the first character starting after the click - var characterIndex = 1; - var previousCharacterOffset = 0f; - var currentCharacterOffset = Font.MeasureString(TextToDisplay, ref fontSize, characterIndex).X; - while (currentCharacterOffset < touchInText && characterIndex < textToDisplay.Length) - { - ++characterIndex; - previousCharacterOffset = currentCharacterOffset; - currentCharacterOffset = Font.MeasureString(TextToDisplay, ref fontSize, characterIndex).X; - if (Font.FontType == SpriteFontType.Dynamic) - currentCharacterOffset /= fontScale.X; - } + var snapText = false; + var realVirtualResolutionRatio = LayoutingContext.RealVirtualResolutionRatio; - // determine the caret position. - if (touchInText < 0) // click before the start of the text - { - return 0; - } - if (currentCharacterOffset < touchInText) // click after the end of the text - { - return textToDisplay.Length; - } + Font.TypeSpecificRatios(ActualTextSize, ref snapText, ref realVirtualResolutionRatio, out var fontSize); - const float Alpha = 0.66f; - var previousElementRatio = Math.Abs(touchInText - previousCharacterOffset) / Alpha; - var currentElementRation = Math.Abs(currentCharacterOffset - touchInText) / (1 - Alpha); - return previousElementRatio < currentElementRation ? characterIndex - 1 : characterIndex; + return Font.IndexInString(TextToDisplay, fontSize, worldPosition, (TextAlignment, textRegion)); } internal override void OnKeyPressed(KeyEventArgs args) diff --git a/sources/engine/Stride.UI/Controls/EditText.cs b/sources/engine/Stride.UI/Controls/EditText.cs index 52ccb524b6..247d4da7e1 100644 --- a/sources/engine/Stride.UI/Controls/EditText.cs +++ b/sources/engine/Stride.UI/Controls/EditText.cs @@ -321,7 +321,7 @@ public float CaretWidth /// The color of the selection. [DataMember] [Display(category: AppearanceCategory)] - public Color SelectionColor { get; set; } = Color.FromAbgr(0xF0F0F0FF); + public Color SelectionColor { get; set; } = Color.FromAbgr(0x80B4D5FF); /// /// Gets or sets the color of the IME composition selection. @@ -329,7 +329,7 @@ public float CaretWidth /// The color of the selection. [DataMember] [Display(category: AppearanceCategory)] - public Color IMESelectionColor { get; set; } = Color.FromAbgr(0xF0FFF0FF); + public Color IMESelectionColor { get; set; } = Color.FromAbgr(0x80FFF0FF); /// /// Gets or sets whether the control is read-only, or not. @@ -896,5 +896,12 @@ protected override void OnTouchMove(TouchEventArgs args) OnTouchMoveImpl(args); } } + + internal Vector2 GetTextRegionSize() + { + return new Vector2( + ActualWidth - Padding.Left - Padding.Right, + ActualHeight - Padding.Top - Padding.Bottom); + } } -} \ No newline at end of file +} diff --git a/sources/engine/Stride.UI/Controls/TextBlock.cs b/sources/engine/Stride.UI/Controls/TextBlock.cs index e16c775d51..1e57ca41c5 100644 --- a/sources/engine/Stride.UI/Controls/TextBlock.cs +++ b/sources/engine/Stride.UI/Controls/TextBlock.cs @@ -254,7 +254,7 @@ private Vector2 CalculateTextSize(SpriteFont.StringProxy textToMeasure) var sizeRatio = LayoutingContext.RealVirtualResolutionRatio; var measureFontSize = new Vector2(sizeRatio.Y * ActualTextSize); // we don't want letters non-uniform ratio - var realSize = Font.MeasureString(ref textToMeasure, ref measureFontSize); + var realSize = Font.MeasureString(textToMeasure, measureFontSize); // force pre-generation if synchronous generation is required if (SynchronousCharacterGeneration) diff --git a/sources/engine/Stride.UI/Renderers/DefaultEditTextRenderer.cs b/sources/engine/Stride.UI/Renderers/DefaultEditTextRenderer.cs index 9009acbeed..3ec88b7e1b 100644 --- a/sources/engine/Stride.UI/Renderers/DefaultEditTextRenderer.cs +++ b/sources/engine/Stride.UI/Renderers/DefaultEditTextRenderer.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Stride.Core.Annotations; using Stride.Core.Mathematics; using Stride.Graphics; using Stride.Graphics.Font; @@ -20,54 +22,72 @@ public DefaultEditTextRenderer(IServiceRegistry services) { } - private void RenderSelection(EditText editText, UIRenderingContext context, int start, int length, Color color, out float offsetTextStart, out float offsetAlignment, out float selectionSize) + private void RenderSelection([NotNull] EditText editText, [NotNull] UIRenderingContext context, in Vector2 textRegion, int start, int length, Color color, out Matrix caret, out float caretHeight) { - // calculate the size of the text region by removing padding - var textRegionSize = new Vector2(editText.ActualWidth - editText.Padding.Left - editText.Padding.Right, - editText.ActualHeight - editText.Padding.Top - editText.Padding.Bottom); - + var snapText = context.ShouldSnapText && !editText.DoNotSnapText; + var requestedFontSize = editText.ActualTextSize; + var realVirtualResolutionRatio = editText.LayoutingContext.RealVirtualResolutionRatio; var font = editText.Font; - // determine the image to draw in background of the edit text - var fontScale = editText.LayoutingContext.RealVirtualResolutionRatio; - var provider = editText.IsSelectionActive ? editText.ActiveImage : editText.MouseOverState == MouseOverState.MouseOverElement ? editText.MouseOverImage : editText.InactiveImage; - var image = provider?.GetSprite(); + font.TypeSpecificRatios(requestedFontSize, ref snapText, ref realVirtualResolutionRatio, out var fontSize); + + var lineHeight = font.GetTotalLineSpacing(fontSize.Y); - var fontSize = new Vector2(fontScale.Y * editText.ActualTextSize); - offsetTextStart = font.MeasureString(editText.TextToDisplay, ref fontSize, start).X; - selectionSize = font.MeasureString(editText.TextToDisplay, ref fontSize, start + length).X - offsetTextStart; - var lineSpacing = font.GetTotalLineSpacing(editText.ActualTextSize); - if (font.FontType == SpriteFontType.Dynamic) + var worldMatrix = editText.WorldMatrixInternal; + worldMatrix.TranslationVector -= worldMatrix.Right * textRegion.X * 0.5f + worldMatrix.Up * (textRegion.Y * 0.5f - lineHeight * 0.5f); + + Vector2 selectionStart = default, selectionEnd = default, lineStart = default, lineEnd = default; + var end = start + length; + foreach (var glyphInfo in new SpriteFont.GlyphEnumerator(null, new SpriteFont.StringProxy(editText.TextToDisplay), fontSize, false, 0, editText.TextToDisplay.Length, font, (editText.TextAlignment, textRegion))) { - offsetTextStart /= fontScale.X; - selectionSize /= fontScale.X; + if (glyphInfo.Index < start) + { + lineEnd = lineStart = selectionEnd = selectionStart = new Vector2(glyphInfo.NextX, glyphInfo.Position.Y); + } + else if (glyphInfo.Index == start) + { + lineStart = selectionEnd = selectionStart = glyphInfo.Position; + lineEnd = new Vector2(glyphInfo.NextX, glyphInfo.Position.Y); + } + else if (glyphInfo.Index <= end) + { + // We're between start and end + if (lineStart.Y != glyphInfo.Y) // Skipped a line, draw a selection rect between the edges of the previous line + { + DrawSelectionOnGlyphRange(context, color, worldMatrix, lineStart, lineEnd, lineHeight); + lineStart = glyphInfo.Position; + } + + lineEnd = new Vector2(glyphInfo.NextX, glyphInfo.Position.Y); + if (glyphInfo.Index < end) + selectionEnd = new Vector2(glyphInfo.NextX, glyphInfo.Position.Y); + else + selectionEnd = glyphInfo.Position; + } + else + { + break; + } } - var scaleRatio = editText.ActualTextSize / font.Size; - if (font.FontType == SpriteFontType.SDF) + if (end == editText.TextToDisplay.Length) // Edge case for single character selected at the end of a string { - offsetTextStart *= scaleRatio; - selectionSize *= scaleRatio; - lineSpacing *= editText.ActualTextSize / font.Size; + selectionEnd.X = lineEnd.X; } + DrawSelectionOnGlyphRange(context, color, worldMatrix, lineStart, selectionEnd, lineHeight); - offsetAlignment = -textRegionSize.X / 2f; - if (editText.TextAlignment != TextAlignment.Left) - { - var textWidth = font.MeasureString(editText.TextToDisplay, ref fontSize).X; - if (font.FontType == SpriteFontType.Dynamic) - textWidth /= fontScale.X; - if (font.FontType == SpriteFontType.SDF) - textWidth *= scaleRatio; - - offsetAlignment = editText.TextAlignment == TextAlignment.Center ? -textWidth / 2 : -textRegionSize.X / 2f + (textRegionSize.X - textWidth); - } + caretHeight = lineHeight; + caret = worldMatrix; + caret.TranslationVector += caret.Right * selectionStart.X + caret.Up * selectionStart.Y; + } - var selectionWorldMatrix = editText.WorldMatrixInternal; - selectionWorldMatrix.M41 += offsetTextStart + selectionSize / 2 + offsetAlignment; - var selectionScaleVector = new Vector3(selectionSize, editText.LineCount * lineSpacing, 0); - Batch.DrawRectangle(ref selectionWorldMatrix, ref selectionScaleVector, ref color, context.DepthBias + 1); + private void DrawSelectionOnGlyphRange(UIRenderingContext context, Color color, in Matrix worldMatrix, Vector2 start, Vector2 end, float lineHeight) + { + var tempMatrix = worldMatrix; + var selectionRect = new Vector3(end.X - start.X, lineHeight, 0); + tempMatrix.TranslationVector += worldMatrix.Right * (start.X + selectionRect.X * 0.5f) + worldMatrix.Up * start.Y; + Batch.DrawRectangle(ref tempMatrix, ref selectionRect, ref color, context.DepthBias + 1); } public override void RenderColor(UIElement element, UIRenderingContext context) @@ -91,27 +111,25 @@ public override void RenderColor(UIElement element, UIRenderingContext context) } // calculate the size of the text region by removing padding - var textRegionSize = new Vector2(editText.ActualWidth - editText.Padding.Left - editText.Padding.Right, - editText.ActualHeight - editText.Padding.Top - editText.Padding.Bottom); + var textRegionSize = editText.GetTextRegionSize(); var font = editText.Font; var caretColor = editText.RenderOpacity * editText.CaretColor; - var offsetTextStart = 0f; - var offsetAlignment = 0f; - var selectionSize = 0f; + var caretMatrix = Matrix.Identity; + var caretHeight = 0f; // Draw the composition selection if (editText.Composition.Length > 0) { var imeSelectionColor = editText.RenderOpacity * editText.IMESelectionColor; - RenderSelection(editText, context, editText.SelectionStart, editText.Composition.Length, imeSelectionColor, out offsetTextStart, out offsetAlignment, out selectionSize); + RenderSelection(editText, context, textRegionSize, editText.SelectionStart, editText.Composition.Length, imeSelectionColor, out caretMatrix, out caretHeight); } // Draw the regular selection else if (editText.IsSelectionActive) { var selectionColor = editText.RenderOpacity * editText.SelectionColor; - RenderSelection(editText, context, editText.SelectionStart, editText.SelectionLength, selectionColor, out offsetTextStart, out offsetAlignment, out selectionSize); + RenderSelection(editText, context, textRegionSize, editText.SelectionStart, editText.SelectionLength, selectionColor, out caretMatrix, out caretHeight); } // create the text draw command @@ -146,15 +164,8 @@ public override void RenderColor(UIElement element, UIRenderingContext context) // Draw the cursor if (editText.IsCaretVisible) { - var lineSpacing = editText.Font.GetTotalLineSpacing(editText.ActualTextSize); - if (editText.Font.FontType == SpriteFontType.SDF) - lineSpacing *= editText.ActualTextSize / font.Size; - - var sizeCaret = editText.CaretWidth / fontScale.X; - var caretWorldMatrix = element.WorldMatrixInternal; - caretWorldMatrix.M41 += offsetTextStart + offsetAlignment + (editText.CaretPosition > editText.SelectionStart? selectionSize: 0); - var caretScaleVector = new Vector3(sizeCaret, editText.LineCount * lineSpacing, 0); - Batch.DrawRectangle(ref caretWorldMatrix, ref caretScaleVector, ref caretColor, context.DepthBias + 3); + var caretScaleVector = new Vector3(editText.CaretWidth / fontScale.X, caretHeight, 0); + Batch.DrawRectangle(ref caretMatrix, ref caretScaleVector, ref caretColor, context.DepthBias + 3); } } }