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!"
|
PlaceholderText = "Hello, World!"
|
||||||
};
|
};
|
||||||
|
|
||||||
c.AddChild(addButton);
|
// c.AddChild(addButton);
|
||||||
c.AddChild(removeButton);
|
// c.AddChild(removeButton);
|
||||||
c.AddChild(outlineButton);
|
// c.AddChild(outlineButton);
|
||||||
c.AddChild(linkButton);
|
// c.AddChild(linkButton);
|
||||||
c.AddChild(inputField);
|
// c.AddChild(inputField);
|
||||||
|
|
||||||
var vc = new VerticalContainer(0.0f);
|
_label = new Label("What the heck??? Word wrapping!!! That's crazy... Noooo wayyy Before GTA 6 too!!!\nnewline :)", _defaultFontSet)
|
||||||
vc.AddChild(c);
|
{
|
||||||
|
Size = new Rect(256.0f, 128.0f),
|
||||||
|
};
|
||||||
|
|
||||||
var f = new MarginContainer(new Size(0.0f));
|
// _rootFill.AddChild(_label);
|
||||||
f.AddChild(_container);
|
_uiSystem.AddElement(_label);
|
||||||
|
|
||||||
vc.AddChild(f);
|
|
||||||
|
|
||||||
_rootFill.AddChild(vc);
|
|
||||||
_uiSystem.AddElement(_rootFill);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void Update(double deltaTime)
|
protected override void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
if (Input.IsActionPressed("reload"))
|
|
||||||
{
|
|
||||||
// ResourceManager.Reload();
|
|
||||||
// _particleSystem!.RestartEmitter(_emitterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiSystem.SetWindowSize(Renderer.WindowSize);
|
_uiSystem.SetWindowSize(Renderer.WindowSize);
|
||||||
|
|
||||||
|
var mousePos = Input.GetMousePosition();
|
||||||
|
_label.Size = new Rect(mousePos.X, mousePos.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Render(double deltaTime)
|
protected override void Render(double deltaTime)
|
||||||
{
|
{
|
||||||
Renderer.ClearBackground(Color.Black);
|
Renderer.ClearBackground(Color.White);
|
||||||
|
|
||||||
// foreach (var emitter in _particleSystem!.Emitters)
|
// foreach (var emitter in _particleSystem!.Emitters)
|
||||||
// {
|
// {
|
||||||
@@ -139,6 +133,8 @@ public class TestGame : Game
|
|||||||
|
|
||||||
Renderer.ResetTransform();
|
Renderer.ResetTransform();
|
||||||
_uiSystem.Render(Renderer);
|
_uiSystem.Render(Renderer);
|
||||||
|
Renderer.SetTransform(_label.GlobalPosition, Vector2.Zero);
|
||||||
|
Renderer.DrawRectangleOutline(_label.Size, Color.Red, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawEmitter(ParticleEmitter emitter)
|
private void DrawEmitter(ParticleEmitter emitter)
|
||||||
|
|||||||
@@ -247,9 +247,7 @@ namespace Voile.Rendering
|
|||||||
var font = fontResource.Value;
|
var font = fontResource.Value;
|
||||||
|
|
||||||
if (font.Handle == -1)
|
if (font.Handle == -1)
|
||||||
{
|
|
||||||
LoadFont(font);
|
LoadFont(font);
|
||||||
}
|
|
||||||
|
|
||||||
if (font.Dirty && font.Handle != -1)
|
if (font.Dirty && font.Handle != -1)
|
||||||
{
|
{
|
||||||
@@ -259,26 +257,56 @@ namespace Voile.Rendering
|
|||||||
|
|
||||||
var rayFont = _fontPool[font.Handle];
|
var rayFont = _fontPool[font.Handle];
|
||||||
|
|
||||||
float x = transformPosition.X;
|
var layout = font.Layout(text, transformPosition);
|
||||||
float y = transformPosition.Y;
|
|
||||||
|
|
||||||
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(
|
Raylib.DrawTextCodepoint(
|
||||||
rayFont,
|
rayFont,
|
||||||
c,
|
run.Character,
|
||||||
new Vector2(x, y),
|
run.Position,
|
||||||
font.Size,
|
font.Size,
|
||||||
VoileColorToRaylibColor(color)
|
VoileColorToRaylibColor(color)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var glyph = font.GetGlyph(c);
|
/// <summary>
|
||||||
x += glyph.Advance * font.SpacingScale + font.LetterSpacing;
|
/// 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,8 +184,12 @@ namespace Voile.Rendering
|
|||||||
/// <param name="color">Fill color.</param>
|
/// <param name="color">Fill color.</param>
|
||||||
public abstract void DrawRectangle(Vector2 size, Color color);
|
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 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>
|
/// <summary>
|
||||||
/// Draws a debug text with a default font.
|
/// Draws a debug text with a default font.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -22,6 +22,44 @@ public struct Glyph
|
|||||||
public 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>
|
/// <summary>
|
||||||
/// Represents font data.
|
/// Represents font data.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Measures a given string using the font metrics.
|
/// Measures a given string using the font metrics.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -86,44 +219,10 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
return Measure(text.AsSpan());
|
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)
|
public Rect Measure(ReadOnlySpan<char> chars)
|
||||||
{
|
{
|
||||||
if (chars.Length == 0)
|
var layout = Layout(chars, Vector2.Zero);
|
||||||
return Rect.Zero;
|
return layout.Size;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetKerning(char left, char right)
|
public int GetKerning(char left, char right)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
if (!child.Visible)
|
if (!child.Visible)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (child is IUpdatableElement updatable && updatable.Dirty)
|
if (child is IUpdatableElement updatable)
|
||||||
updatable.Update(layoutContext);
|
updatable.Update(layoutContext);
|
||||||
|
|
||||||
if (child is IAnchorableElement anchorable)
|
if (child is IAnchorableElement anchorable)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Voile.UI.Widgets;
|
|||||||
|
|
||||||
public class Label : Widget
|
public class Label : Widget
|
||||||
{
|
{
|
||||||
public override Rect MinimumSize => _textSize;
|
public override Rect MinimumSize => Rect.Zero;
|
||||||
|
|
||||||
public string Text
|
public string Text
|
||||||
{
|
{
|
||||||
@@ -50,34 +50,43 @@ public class Label : Widget
|
|||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
protected override void OnRender(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
if (!_font.HasValue)
|
||||||
renderer.DrawText(_suitableFont, _text, style.TextColor ?? Color.Black);
|
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)
|
protected override void OnUpdate(LayoutContext layoutContext)
|
||||||
{
|
{
|
||||||
ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
|
ResolveFont();
|
||||||
foreach (var c in _text)
|
|
||||||
|
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;
|
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
var font = _suitableFont.Value;
|
|
||||||
_textSize = font.Measure(_text);
|
|
||||||
|
|
||||||
Size = _textSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
|
|
||||||
|
|
||||||
private string _text = "Hello, World!";
|
private string _text = "Hello, World!";
|
||||||
private Rect _textSize = Rect.Zero;
|
|
||||||
|
private TextLayout _layout = new();
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user