Я использовал ответ Томаса, чтобы помочь создать что-то более устойчивое.
Вспомогательные классы:
internal enum LineComponentType { Word, Space, NewLine }
internal interface ILineComponent
{
float Length { get; }
LineComponentType Type { get; }
Colour Colour { get; }
}
internal class NewLine : ILineComponent
{
public float Length => 0;
public LineComponentType Type => LineComponentType.NewLine;
public Colour Colour => Colour.Transparent;
}
internal class Space : ILineComponent
{
public Space(HFont font, FontStyle fontStyle)
{
Length = font.MeasureString(' ', fontStyle).x;
}
public float Length { get; }
public LineComponentType Type => LineComponentType.Space;
public Colour Colour => Colour.Transparent;
public override string ToString() => " ";
}
internal class Word : ILineComponent
{
private readonly HFont _font;
public Word(string text, HFont font, FontStyle style, Colour colour)
{
Text = text;
_font = font;
Style = style;
Colour = colour;
Length = font.MeasureString(text, style).x;
}
public float Length { get; }
public LineComponentType Type => LineComponentType.Word;
public string Text { get; }
public FontStyle Style { get; }
public Colour Colour { get; }
public float MeasureCharAtIndex(int index) => _font.MeasureString(Text[index], Style).x;
public Word SubWord(int startIndex) => new Word(Text.Substring(startIndex), _font, Style, Colour);
public Word SubWord(int startIndex, int length) => new Word(Text.Substring(startIndex, length), _font, Style, Colour);
public override string ToString() => Text;
}
Основной класс для рендеринга:
public class HText : IRectGraphic
{
//...
private List<List<ILineComponent>> SplitTextToLines(string text)
{
//Convert text into a list of IComponents
var allComponents = new List<ILineComponent>();
text.Replace("\r\n", "\n");
text.Replace("\r", "\n");
while (!text.IsNullOrEmpty())
{
if (text[0] == ' ')
{
allComponents.Add(new Space(Font, FontStyle));
text = text.Remove(0, 1);
}
else if (text[0] == '\n')
{
allComponents.Add(new NewLine());
text = text.Remove(0, 1);
}
else //it's a word
{
var words = text.Split(new[] { '\n', ' ' });
allComponents.Add(new Word(words[0], Font, FontStyle, Colour));
text = text.Remove(0, words[0].Length);
}
}
var lines = new List<List<ILineComponent>>();
//Split IComponents into lines
var currentLine = new List<ILineComponent>();
bool oneLetterWiderThanWholeLine = false;
while (!allComponents.IsEmpty())
{
var component = allComponents[0];
switch (component.Type)
{
case LineComponentType.Word:
var word = (Word)component;
if (currentLine.Sum(c => c.Length) + word.Length <= w) //if it fits, add it to the current line
{
//if we started a new line with a space then a word, remove the space
if (lines.Count != 0 && currentLine.Count == 1 && currentLine[0].Type == LineComponentType.Space)
currentLine.RemoveAt(0);
currentLine.Add(component);
allComponents.RemoveAt(0);
}
else if (currentLine.Count == 0) //if doesn't fit, but it is the first word in a line, we will split and wrap it
{
int splitAt = 0;
float width = 0;
float nextCharLength;
while (width + (nextCharLength = word.MeasureCharAtIndex(splitAt)) <= w)
{
width += nextCharLength;
splitAt++;
}
if (splitAt == 0) //if one letter is too wide to fit, display it anyway
{
splitAt = 1;
oneLetterWiderThanWholeLine = true;
}
var word1 = word.SubWord(0, splitAt);
var word2 = word.SubWord(splitAt);
currentLine.Add(word1);
lines.Add(currentLine);
currentLine = new List<ILineComponent>();
allComponents.RemoveAt(0);
allComponents.Insert(0, word2);
}
else //push the word to the next line
{
lines.Add(currentLine);
currentLine = new List<ILineComponent>();
//we don't add the next word to the next line yet - it might not fit
}
break;
case LineComponentType.Space:
if (currentLine.Sum(c => c.Length) + component.Length > w)
{
lines.Add(currentLine);
currentLine = new List<ILineComponent>();
}
currentLine.Add(component);
allComponents.RemoveAt(0);
break;
case LineComponentType.NewLine:
lines.Add(currentLine);
currentLine = new List<ILineComponent>();
allComponents.RemoveAt(0);
break;
default:
throw new HException("Htext/SplitTextToLines: Unhandled component type {0}", component.Type);
}
}
if (currentLine.Count > 0)
lines.Add(currentLine);
//Size warnings
if (lines.Count * Font.BiggestChar.y > h)
HConsole.Log("HText/SetVertices: lines total height ({0}) is bigger than text box height ({1})", lines.Count * Font.BiggestChar.y, h);
if (oneLetterWiderThanWholeLine)
HConsole.Log("HText/SetVertices: a single letter is beyond the text box width ({0})", w);
return lines;
}
private void SetVertices()
{
var vertices = new List<Vertex>();
var lines = SplitTextToLines(Text);
var dest = new Rect();
switch (VAlignment)
{
case VAlignment.Top:
dest.y = 0;
break;
case VAlignment.Centre:
dest.y = (h - lines.Count * Font.BiggestChar.y) / 2;
break;
case VAlignment.Bottom:
dest.y = h - lines.Count * Font.BiggestChar.y;
break;
case VAlignment.Fill:
dest.y = 0;
break;
default:
throw new HException("HText/SetVertices: alignment {0} was not catered for.", VAlignment);
}
foreach (var line in lines)
{
switch (HAlignment)
{
case HAlignment.LeftJustified:
dest.x = 0;
break;
case HAlignment.Centred:
dest.x = (w - line.Sum(c => c.Length)) / 2;
break;
case HAlignment.RightJustified:
dest.x = w - line.Sum(c => c.Length);
break;
case HAlignment.FullyJustified:
dest.x = 0;
break;
default:
throw new HException("HText/SetVertices: alignment {0} was not catered for.", HAlignment);
}
foreach (var com in line)
foreach (char c in com.ToString())
{
var source = Font.CharPositionsNormalised[System.Drawing.FontStyle.Regular][c];
dest.w = Font.CharPositions[System.Drawing.FontStyle.Regular][c].w;
dest.h = Font.CharPositions[System.Drawing.FontStyle.Regular][c].h;
vertices.Add(HF.Geom.QuadToTris(
new Vertex(dest.TopLeft, com.Colour, source.TopLeft),
new Vertex(dest.TopRight, com.Colour, source.TopRight),
new Vertex(dest.BottomLeft, com.Colour, source.BottomLeft),
new Vertex(dest.BottomRight, com.Colour, source.BottomRight)
));
dest.x += dest.w;
}
dest.y += Font.BiggestChar.y;
}
_vertices = vertices.ToArray();
}
public virtual void Render(ref Matrix4 projection, ref Matrix4 modelView)
{
if (w == 0 || h == 0)
return;
if (HV.LastBoundTexture != Texture.ID)
{
OpenGL.BindTexture(TextureTarget.Texture2D, Texture.ID);
HV.LastBoundTexture = Texture.ID;
}
if (HV.LastBoundVertexBuffer != VertexBuffer)
Bind();
if (_verticesChanged)
{
SetVertices();
_verticesChanged = false;
OpenGL.BufferData(BufferTarget.ArrayBuffer, _vertices.Length * Vertex.STRIDE, _vertices, BufferUsageHint.StreamDraw);
}
var mv = Matrix4.Translate(ref modelView, x, y, 0);
Shader.Render(ref projection, ref mv, _vertices.Length, PrimitiveType.Triangles);
}
}