From 96b2ad44ad83e1350e701496ecaeb1bf8e67fd14 Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 2 Jun 2026 01:55:59 +0200 Subject: [PATCH] TextLayout, match rendered text with measured text, implement word wrapping --- TestGame/TestGame.cs | 38 ++--- Voile/Source/Rendering/RaylibRenderSystem.cs | 62 +++++-- Voile/Source/Rendering/RenderSystem.cs | 4 + Voile/Source/Resources/Font.cs | 171 +++++++++++++++---- Voile/Source/UI/Containers/Container.cs | 2 +- Voile/Source/UI/Widgets/Label.cs | 51 +++--- 6 files changed, 232 insertions(+), 96 deletions(-) diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index b39bde0..dff9036 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -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) diff --git a/Voile/Source/Rendering/RaylibRenderSystem.cs b/Voile/Source/Rendering/RaylibRenderSystem.cs index 068e5cf..76524c7 100644 --- a/Voile/Source/Rendering/RaylibRenderSystem.cs +++ b/Voile/Source/Rendering/RaylibRenderSystem.cs @@ -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); + /// + /// Draws the text using a pre-computed text layout. + /// + /// Rasterized font. + /// to draw. + /// Color of the text. + public void DrawText(ResourceRef 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) + ); + } } } diff --git a/Voile/Source/Rendering/RenderSystem.cs b/Voile/Source/Rendering/RenderSystem.cs index 8da1b64..84b7af0 100644 --- a/Voile/Source/Rendering/RenderSystem.cs +++ b/Voile/Source/Rendering/RenderSystem.cs @@ -184,8 +184,12 @@ namespace Voile.Rendering /// Fill 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 void DrawRectangleOutline(Rect rect, Color color, float outlineWidth = 1.0f) => DrawRectangleOutline(new Vector2(rect.Width, rect.Height), color, outlineWidth); + /// /// Draws a debug text with a default font. /// diff --git a/Voile/Source/Resources/Font.cs b/Voile/Source/Resources/Font.cs index bd7dc87..4afa634 100644 --- a/Voile/Source/Resources/Font.cs +++ b/Voile/Source/Resources/Font.cs @@ -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 Runs; + + public float Width; + public float Height; + public float Ascent; + public float Descent; + + public TextLine(List runs) + { + Runs = runs; + Width = 0; + Height = 0; + Ascent = 0; + Descent = 0; + } +} + +public class TextLayout +{ + public List Lines = new(); + + public Rect Size = Rect.Zero; +} + /// /// Represents font data. /// @@ -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 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(); + + 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(); + 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; + } + /// /// Measures a given string using the font metrics. /// @@ -86,44 +219,10 @@ public class Font : Resource, IUpdatableResource, IDisposable return Measure(text.AsSpan()); } - /// - /// Measures a given of characters. - /// - /// - /// A with the sizes of a given text using this font. public Rect Measure(ReadOnlySpan 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) diff --git a/Voile/Source/UI/Containers/Container.cs b/Voile/Source/UI/Containers/Container.cs index cd1b3e6..ec071e9 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -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) diff --git a/Voile/Source/UI/Widgets/Label.cs b/Voile/Source/UI/Widgets/Label.cs index 36c3681..e8b55d2 100644 --- a/Voile/Source/UI/Widgets/Label.cs +++ b/Voile/Source/UI/Widgets/Label.cs @@ -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 fontRef = ResourceRef.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 _suitableFont = ResourceRef.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 = ResourceRef.Empty(); private string _text = "Hello, World!"; - private Rect _textSize = Rect.Zero; + + private TextLayout _layout = new(); } \ No newline at end of file