TextLayout, match rendered text with measured text, implement word wrapping
This commit is contained in:
@@ -98,39 +98,33 @@ public class TestGame : Game
|
||||
PlaceholderText = "Hello, World!"
|
||||
};
|
||||
|
||||
c.AddChild(addButton);
|
||||
c.AddChild(removeButton);
|
||||
c.AddChild(outlineButton);
|
||||
c.AddChild(linkButton);
|
||||
c.AddChild(inputField);
|
||||
// c.AddChild(addButton);
|
||||
// c.AddChild(removeButton);
|
||||
// c.AddChild(outlineButton);
|
||||
// c.AddChild(linkButton);
|
||||
// c.AddChild(inputField);
|
||||
|
||||
var vc = new VerticalContainer(0.0f);
|
||||
vc.AddChild(c);
|
||||
_label = new Label("What the heck??? Word wrapping!!! That's crazy... Noooo wayyy Before GTA 6 too!!!\nnewline :)", _defaultFontSet)
|
||||
{
|
||||
Size = new Rect(256.0f, 128.0f),
|
||||
};
|
||||
|
||||
var f = new MarginContainer(new Size(0.0f));
|
||||
f.AddChild(_container);
|
||||
|
||||
vc.AddChild(f);
|
||||
|
||||
_rootFill.AddChild(vc);
|
||||
_uiSystem.AddElement(_rootFill);
|
||||
// _rootFill.AddChild(_label);
|
||||
_uiSystem.AddElement(_label);
|
||||
}
|
||||
|
||||
|
||||
protected override void Update(double deltaTime)
|
||||
{
|
||||
if (Input.IsActionPressed("reload"))
|
||||
{
|
||||
// ResourceManager.Reload();
|
||||
// _particleSystem!.RestartEmitter(_emitterId);
|
||||
}
|
||||
|
||||
_uiSystem.SetWindowSize(Renderer.WindowSize);
|
||||
|
||||
var mousePos = Input.GetMousePosition();
|
||||
_label.Size = new Rect(mousePos.X, mousePos.Y);
|
||||
}
|
||||
|
||||
protected override void Render(double deltaTime)
|
||||
{
|
||||
Renderer.ClearBackground(Color.Black);
|
||||
Renderer.ClearBackground(Color.White);
|
||||
|
||||
// foreach (var emitter in _particleSystem!.Emitters)
|
||||
// {
|
||||
@@ -139,6 +133,8 @@ public class TestGame : Game
|
||||
|
||||
Renderer.ResetTransform();
|
||||
_uiSystem.Render(Renderer);
|
||||
Renderer.SetTransform(_label.GlobalPosition, Vector2.Zero);
|
||||
Renderer.DrawRectangleOutline(_label.Size, Color.Red, 2);
|
||||
}
|
||||
|
||||
private void DrawEmitter(ParticleEmitter emitter)
|
||||
|
||||
@@ -247,9 +247,7 @@ namespace Voile.Rendering
|
||||
var font = fontResource.Value;
|
||||
|
||||
if (font.Handle == -1)
|
||||
{
|
||||
LoadFont(font);
|
||||
}
|
||||
|
||||
if (font.Dirty && font.Handle != -1)
|
||||
{
|
||||
@@ -259,26 +257,56 @@ namespace Voile.Rendering
|
||||
|
||||
var rayFont = _fontPool[font.Handle];
|
||||
|
||||
float x = transformPosition.X;
|
||||
float y = transformPosition.Y;
|
||||
var layout = font.Layout(text, transformPosition);
|
||||
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
foreach (var line in layout.Lines)
|
||||
{
|
||||
char c = text[i];
|
||||
foreach (var run in line.Runs)
|
||||
{
|
||||
Raylib.DrawTextCodepoint(
|
||||
rayFont,
|
||||
run.Character,
|
||||
run.Position,
|
||||
font.Size,
|
||||
VoileColorToRaylibColor(color)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
x += font.GetKerning(text[i - 1], c);
|
||||
/// <summary>
|
||||
/// Draws the text using a pre-computed text layout.
|
||||
/// </summary>
|
||||
/// <param name="font">Rasterized font.</param>
|
||||
/// <param name="layout"><see cref="TextLayout"/> to draw.</param>
|
||||
/// <param name="color">Color of the text.</param>
|
||||
public void DrawText(ResourceRef<Font> fontResource, TextLayout layout, Color color)
|
||||
{
|
||||
var font = fontResource.Value;
|
||||
|
||||
Raylib.DrawTextCodepoint(
|
||||
rayFont,
|
||||
c,
|
||||
new Vector2(x, y),
|
||||
font.Size,
|
||||
VoileColorToRaylibColor(color)
|
||||
);
|
||||
if (font.Handle == -1)
|
||||
LoadFont(font);
|
||||
|
||||
var glyph = font.GetGlyph(c);
|
||||
x += glyph.Advance * font.SpacingScale + font.LetterSpacing;
|
||||
if (font.Dirty && font.Handle != -1)
|
||||
{
|
||||
UnloadFont(font);
|
||||
LoadFont(font);
|
||||
}
|
||||
|
||||
var rayFont = _fontPool[font.Handle];
|
||||
|
||||
foreach (var line in layout.Lines)
|
||||
{
|
||||
foreach (var run in line.Runs)
|
||||
{
|
||||
Raylib.DrawTextCodepoint(
|
||||
rayFont,
|
||||
run.Character,
|
||||
run.Position,
|
||||
font.Size,
|
||||
VoileColorToRaylibColor(color)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,8 +184,12 @@ namespace Voile.Rendering
|
||||
/// <param name="color">Fill color.</param>
|
||||
public abstract void DrawRectangle(Vector2 size, Color color);
|
||||
|
||||
public void DrawRectangle(Rect rect, Color color) => DrawRectangle(new Vector2(rect.Width, rect.Height), color);
|
||||
|
||||
public abstract void DrawRectangleOutline(Vector2 size, Color color, float outlineWidth = 1.0f);
|
||||
|
||||
public void DrawRectangleOutline(Rect rect, Color color, float outlineWidth = 1.0f) => DrawRectangleOutline(new Vector2(rect.Width, rect.Height), color, outlineWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a debug text with a default font.
|
||||
/// </summary>
|
||||
|
||||
@@ -22,6 +22,44 @@ public struct Glyph
|
||||
public Glyph() { }
|
||||
}
|
||||
|
||||
public struct GlyphRun
|
||||
{
|
||||
public char Character;
|
||||
public Vector2 Position;
|
||||
|
||||
public GlyphRun(char character, Vector2 position)
|
||||
{
|
||||
Character = character;
|
||||
Position = position;
|
||||
}
|
||||
}
|
||||
|
||||
public struct TextLine
|
||||
{
|
||||
public List<GlyphRun> Runs;
|
||||
|
||||
public float Width;
|
||||
public float Height;
|
||||
public float Ascent;
|
||||
public float Descent;
|
||||
|
||||
public TextLine(List<GlyphRun> runs)
|
||||
{
|
||||
Runs = runs;
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
Ascent = 0;
|
||||
Descent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class TextLayout
|
||||
{
|
||||
public List<TextLine> Lines = new();
|
||||
|
||||
public Rect Size = Rect.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents font data.
|
||||
/// </summary>
|
||||
@@ -76,6 +114,101 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public TextLayout Layout(string text, Vector2 origin) => Layout(text.AsSpan(), origin);
|
||||
|
||||
public TextLayout Layout(ReadOnlySpan<char> chars, Vector2 origin, float maxWidth = float.MaxValue)
|
||||
{
|
||||
var layout = new TextLayout();
|
||||
|
||||
float startX = origin.X;
|
||||
float x = startX;
|
||||
float y = origin.Y;
|
||||
|
||||
float lineHeight = Size;
|
||||
|
||||
var currentLine = new List<GlyphRun>();
|
||||
|
||||
float lineAscent = 0;
|
||||
float lineDescent = 0;
|
||||
|
||||
float lineWidth = 0;
|
||||
|
||||
char prev = '\0';
|
||||
|
||||
void BreakLine()
|
||||
{
|
||||
layout.Lines.Add(new TextLine(currentLine)
|
||||
{
|
||||
Width = lineWidth,
|
||||
Height = lineHeight,
|
||||
Ascent = lineAscent,
|
||||
Descent = lineDescent
|
||||
});
|
||||
|
||||
layout.Size.Width = Math.Max(layout.Size.Width, lineWidth);
|
||||
|
||||
currentLine = new List<GlyphRun>();
|
||||
x = startX;
|
||||
y += lineHeight;
|
||||
|
||||
lineWidth = 0;
|
||||
lineAscent = 0;
|
||||
lineDescent = 0;
|
||||
|
||||
prev = '\0';
|
||||
}
|
||||
|
||||
for (int i = 0; i < chars.Length; i++)
|
||||
{
|
||||
char c = chars[i];
|
||||
|
||||
if (c == '\n')
|
||||
{
|
||||
BreakLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var glyph = GetGlyph(c);
|
||||
|
||||
float advance = 0;
|
||||
|
||||
if (prev != '\0')
|
||||
advance += GetKerning(prev, c);
|
||||
|
||||
advance += glyph.Advance * SpacingScale + LetterSpacing;
|
||||
|
||||
if (maxWidth > 0 && lineWidth + advance > maxWidth)
|
||||
{
|
||||
BreakLine();
|
||||
}
|
||||
|
||||
if (prev != '\0')
|
||||
{
|
||||
float kerning = GetKerning(prev, c);
|
||||
x += kerning;
|
||||
}
|
||||
|
||||
var pos = new Vector2(x, y);
|
||||
|
||||
currentLine.Add(new GlyphRun(c, pos));
|
||||
|
||||
x += glyph.Advance * SpacingScale + LetterSpacing;
|
||||
lineWidth += advance;
|
||||
|
||||
lineAscent = Math.Max(lineAscent, glyph.Bearing.Y);
|
||||
lineDescent = Math.Max(lineDescent, glyph.Height - glyph.Bearing.Y);
|
||||
|
||||
prev = c;
|
||||
}
|
||||
|
||||
if (currentLine.Count > 0)
|
||||
BreakLine();
|
||||
|
||||
layout.Size.Height = layout.Lines.Count * lineHeight;
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures a given string using the font metrics.
|
||||
/// </summary>
|
||||
@@ -86,44 +219,10 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
||||
return Measure(text.AsSpan());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures a given <see cref="ReadOnlySpan"/> of characters.
|
||||
/// </summary>
|
||||
/// <param name="chars"></param>
|
||||
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
||||
public Rect Measure(ReadOnlySpan<char> chars)
|
||||
{
|
||||
if (chars.Length == 0)
|
||||
return Rect.Zero;
|
||||
|
||||
float totalWidth = 0;
|
||||
float maxAscent = 0;
|
||||
float maxDescent = 0;
|
||||
|
||||
for (int i = 0; i < chars.Length; i++)
|
||||
{
|
||||
char c = chars[i];
|
||||
Glyph glyph = GetGlyph(c);
|
||||
|
||||
totalWidth += glyph.Advance * SpacingScale + LetterSpacing;
|
||||
|
||||
float ascent = glyph.Bearing.Y;
|
||||
float descent = glyph.Height - glyph.Bearing.Y;
|
||||
|
||||
if (ascent > maxAscent)
|
||||
maxAscent = ascent;
|
||||
if (descent > maxDescent)
|
||||
maxDescent = descent;
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
char prevChar = chars[i - 1];
|
||||
totalWidth += GetKerning(prevChar, c);
|
||||
}
|
||||
}
|
||||
|
||||
float totalHeight = Size;
|
||||
return new Rect(totalWidth, totalHeight);
|
||||
var layout = Layout(chars, Vector2.Zero);
|
||||
return layout.Size;
|
||||
}
|
||||
|
||||
public int GetKerning(char left, char right)
|
||||
|
||||
@@ -49,7 +49,7 @@ public abstract class Container : UIElement, IParentableElement
|
||||
if (!child.Visible)
|
||||
continue;
|
||||
|
||||
if (child is IUpdatableElement updatable && updatable.Dirty)
|
||||
if (child is IUpdatableElement updatable)
|
||||
updatable.Update(layoutContext);
|
||||
|
||||
if (child is IAnchorableElement anchorable)
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Voile.UI.Widgets;
|
||||
|
||||
public class Label : Widget
|
||||
{
|
||||
public override Rect MinimumSize => _textSize;
|
||||
public override Rect MinimumSize => Rect.Zero;
|
||||
|
||||
public string Text
|
||||
{
|
||||
@@ -50,34 +50,43 @@ public class Label : Widget
|
||||
|
||||
protected override void OnRender(RenderSystem renderer, Style style)
|
||||
{
|
||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||
renderer.DrawText(_suitableFont, _text, style.TextColor ?? Color.Black);
|
||||
if (!_font.HasValue)
|
||||
return;
|
||||
|
||||
if (renderer is not RaylibRenderSystem rayRenderer) return; // TODO: Do NOT rely on RaylibRenderSystem check here. Very bad.
|
||||
rayRenderer.DrawText(_font, _layout, style.TextColor ?? Color.Black);
|
||||
}
|
||||
|
||||
protected override void OnUpdate(LayoutContext layoutContext)
|
||||
{
|
||||
ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
|
||||
foreach (var c in _text)
|
||||
{
|
||||
if (FontSet.TryGetFontFor(c, out var fallbackFont))
|
||||
{
|
||||
if (fallbackFont != fontRef)
|
||||
{
|
||||
fontRef = fallbackFont;
|
||||
}
|
||||
}
|
||||
}
|
||||
ResolveFont();
|
||||
|
||||
_suitableFont = fontRef;
|
||||
if (!_font.HasValue)
|
||||
return;
|
||||
|
||||
var font = _suitableFont.Value;
|
||||
_textSize = font.Measure(_text);
|
||||
|
||||
Size = _textSize;
|
||||
_layout = _font.Value.Layout(_text, GlobalPosition, Size.Width);
|
||||
}
|
||||
|
||||
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
|
||||
private void ResolveFont()
|
||||
{
|
||||
if (_font.HasValue)
|
||||
return;
|
||||
|
||||
var text = _text;
|
||||
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (FontSet.TryGetFontFor(text[i], out var f))
|
||||
{
|
||||
_font = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
||||
|
||||
private string _text = "Hello, World!";
|
||||
private Rect _textSize = Rect.Zero;
|
||||
|
||||
private TextLayout _layout = new();
|
||||
}
|
||||
Reference in New Issue
Block a user