TextLayout, match rendered text with measured text, implement word wrapping

This commit is contained in:
2026-06-02 01:55:59 +02:00
parent 8ba21166be
commit 96b2ad44ad
6 changed files with 232 additions and 96 deletions

View File

@@ -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)

View File

@@ -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)
{
foreach (var run in line.Runs)
{
char c = text[i];
if (i > 0)
x += font.GetKerning(text[i - 1], c);
Raylib.DrawTextCodepoint(
rayFont,
c,
new Vector2(x, y),
run.Character,
run.Position,
font.Size,
VoileColorToRaylibColor(color)
);
}
}
}
var glyph = font.GetGlyph(c);
x += glyph.Advance * font.SpacingScale + font.LetterSpacing;
/// <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;
if (font.Handle == -1)
LoadFont(font);
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)
);
}
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
ResolveFont();
if (!_font.HasValue)
return;
_layout = _font.Value.Layout(_text, GlobalPosition, Size.Width);
}
private void ResolveFont()
{
if (FontSet.TryGetFontFor(c, out var fallbackFont))
if (_font.HasValue)
return;
var text = _text;
for (int i = 0; i < text.Length; i++)
{
if (fallbackFont != fontRef)
if (FontSet.TryGetFontFor(text[i], out var f))
{
fontRef = fallbackFont;
_font = f;
break;
}
}
}
_suitableFont = fontRef;
var font = _suitableFont.Value;
_textSize = font.Measure(_text);
Size = _textSize;
}
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
private string _text = "Hello, World!";
private Rect _textSize = Rect.Zero;
private TextLayout _layout = new();
}