FontRenderer: Basic Rich Text processing and lazy text building

This commit is contained in:
Simon Lübeß
2024-03-16 23:35:12 +01:00
parent f9ea705e77
commit 96609a6c31
5 changed files with 148 additions and 45 deletions
@@ -477,6 +477,18 @@ namespace GlitchyEditor.EditWindows
private static void ShowTextRendererComponentEditor(Entity entity, TextRendererComponent* textRendererComponent)
{
StartNewProperty("Rich text");
ImGui.AttachTooltip("If checked, the text will be interpreted as rich text. This means, that you can use tags to change the style of the text.");
bool isRichText = textRendererComponent.IsRichText;
if (ImGui.Checkbox("##rich_text", &isRichText))
{
textRendererComponent.IsRichText = isRichText;
textRendererComponent.NeedsRebuild = true;
}
StartNewProperty("Text");
String text = textRendererComponent.[Friend]_text;
@@ -498,6 +510,8 @@ namespace GlitchyEditor.EditWindows
{
text.Length = length;
}
textRendererComponent.NeedsRebuild = true;
}
}
+82 -29
View File
@@ -135,6 +135,49 @@ namespace GlitchyEngine.Renderer.Text
case InvertCase;
}
class StyleStack<T>
{
private append List<T> _stack = .();
public this(T bottomValue)
{
_stack.Add(bottomValue);
}
public void Push(T value)
{
_stack.Add(value);
}
public T CurrentValue()
{
return _stack.Back;
}
public T Pop()
{
if (_stack.Count == 1)
return _stack[0];
return _stack.PopBack();
}
/// If popValue is true, the given value will not be pushed and the current top of the stack will be poped.
/// If popValaue is false, the given value will be pushed and returned.
public T PushButPopIfTrue(T value, bool popValue)
{
if (popValue)
{
return Pop();
}
else
{
Push(value);
return value;
}
}
}
public static PreparedText PrepareText(Font font, StringView text, float fontSize, Color fontColor = .White, Color bitmapColor = .White, float lineSpaceScale = 1.0f, TextDirection direction = .LeftToRight)
{
Debug.Profiler.ProfileRendererFunction!();
@@ -158,11 +201,11 @@ namespace GlitchyEngine.Renderer.Text
hb_buffer_t* buf = hb_buffer_create();
// Stack to keep track of the text direction.
List<TextDirection> textDirectionStack = scope .();
textDirectionStack.Add(direction);
StyleStack<TextDirection> textDirectionStack = scope .(direction);
List<TextCase> textCaseStack = scope .();
textCaseStack.Add(.RetainCase);
StyleStack<TextCase> textCaseStack = scope .(.RetainCase);
StyleStack<bool> richTextStack = scope .(true);
int movedLines = 0;
@@ -185,7 +228,7 @@ namespace GlitchyEngine.Renderer.Text
// Set the script, language and direction of the buffer.
// TODO: change direction, script, etc.
TextDirection direction = textDirectionStack.Back;
TextDirection direction = textDirectionStack.CurrentValue();
switch (direction)
{
@@ -237,7 +280,7 @@ namespace GlitchyEngine.Renderer.Text
bool escapeNextChar = false;
for (char32 char in text.DecodedChars)
mainEnumerator: for (char32 char in text.DecodedChars)
{
defer
{
@@ -271,14 +314,14 @@ namespace GlitchyEngine.Renderer.Text
// LEFT-TO-RIGHT ISOLATE
FlushShapeBuffer();
textDirectionStack.Add(.RightToLeft);
textDirectionStack.Push(.RightToLeft);
continue;
case '\u{2067}':
// RIGHT-TO-LEFT ISOLATE
FlushShapeBuffer();
textDirectionStack.Add(.RightToLeft);
textDirectionStack.Push(.RightToLeft);
continue;
// TODO: FIRST STRONG ISOLATE (U+2068)
@@ -286,8 +329,7 @@ namespace GlitchyEngine.Renderer.Text
// POP DIRECTIONAL ISOLATE
FlushShapeBuffer();
if (textDirectionStack.Count > 1)
textDirectionStack.PopBack();
textDirectionStack.Pop();
continue;
case '\\':
@@ -331,6 +373,30 @@ namespace GlitchyEngine.Renderer.Text
if (isEndTag)
tagText.Remove(0);
tagText.ToLower();
if (tagText == "rt" || tagText == "richtext")
{
richTextStack.PushButPopIfTrue(true, isEndTag);
// Skip the tag in the main enumerator
@char.NextIndex = endIndex;
continue;
}
if (tagText == "pt" || tagText == "plaintext")
{
richTextStack.PushButPopIfTrue(false, isEndTag);
// Skip the tag in the main enumerator
@char.NextIndex = endIndex;
continue;
}
// Only process the other tags if we currently use rich text processing
if (!richTextStack.CurrentValue())
break;
switch(tagText)
{
case "b", "bold":
@@ -348,37 +414,24 @@ namespace GlitchyEngine.Renderer.Text
// Text case control
case "lc", "lowercase":
if (isEndTag && textCaseStack.Count > 0)
textCaseStack.PopBack();
else
textCaseStack.Add(.LowerCase);
textCaseStack.PushButPopIfTrue(.LowerCase, isEndTag);
case "uc", "uppercase":
if (isEndTag && textCaseStack.Count > 0)
textCaseStack.PopBack();
else
textCaseStack.Add(.UpperCase);
if (isEndTag)
textCaseStack.PushButPopIfTrue(.UpperCase, isEndTag);
case "rc", "retaincase":
if (isEndTag && textCaseStack.Count > 0)
textCaseStack.PopBack();
else
textCaseStack.Add(.RetainCase);
textCaseStack.PushButPopIfTrue(.RetainCase, isEndTag);
case "ic", "invertcase":
if (isEndTag && textCaseStack.Count > 0)
textCaseStack.PopBack();
else
textCaseStack.Add(.InvertCase);
textCaseStack.PushButPopIfTrue(.InvertCase, isEndTag);
}
// Skip the tag in the main enumerator
@char.NextIndex = endIndex;
continue;
}
escapeNextChar = false;
switch (textCaseStack.Back)
switch (textCaseStack.CurrentValue())
{
case .RetainCase:
// Nothing to do
@@ -1,11 +1,20 @@
using System;
using static GlitchyEngine.Renderer.Text.FontRenderer;
namespace GlitchyEngine.World.Components;
enum TextRendererFlags
{
IsRichText = 1,
NeedsRebuild = 2
}
struct TextRendererComponent : IDisposableComponent
{
private String _text;
private bool _isRichText;
private PreparedText _preparedText;
private TextRendererFlags _flags;
public StringView Text
{
@@ -19,14 +28,27 @@ struct TextRendererComponent : IDisposableComponent
}
}
public PreparedText PreparedText
{
get => _preparedText;
set mut => SetReference!(_preparedText, value);
}
public bool IsRichText
{
get => _isRichText;
set mut => _isRichText = value;
get => _flags.HasFlag(.IsRichText);
set mut => Enum.SetFlagConditionally(ref _flags, .IsRichText, value);
}
public bool NeedsRebuild
{
get => _flags.HasFlag(.NeedsRebuild);
set mut => Enum.SetFlagConditionally(ref _flags, .NeedsRebuild, value);
}
public void Dispose()
{
delete _text;
_preparedText?.ReleaseRef();
}
}
+24
View File
@@ -10,6 +10,8 @@ using GlitchyEngine.Math;
using GlitchyEngine.Scripting.Classes;
using GlitchyEngine.Serialization;
using GlitchyEngine.World.Components;
using GlitchyEngine.Renderer.Text;
using static GlitchyEngine.Renderer.Text.FontRenderer;
namespace GlitchyEngine.World
{
@@ -56,6 +58,8 @@ namespace GlitchyEngine.World
cameraEntity
};
Font _font ~ _.ReleaseRef();
/// Gets or sets the name of the scene.
public StringView Name
{
@@ -117,6 +121,8 @@ namespace GlitchyEngine.World
scene.InitPolygonCollider2D(e, collider);
}
});
_font = new Font(@"C:\Windows\Fonts\arial.ttf", 24);
}
public ~this()
@@ -768,6 +774,22 @@ namespace GlitchyEngine.World
}
}
private void BuildTexts()
{
for (let (entity, textRenderer) in _ecsWorld.Enumerate<TextRendererComponent>())
{
if (textRenderer.NeedsRebuild || textRenderer.PreparedText == null)
{
using (PreparedText preparedText = FontRenderer.PrepareText(_font, textRenderer.Text, 24, .Black))
{
textRenderer.PreparedText = preparedText;
}
textRenderer.NeedsRebuild = false;
}
}
}
public void Update(GameTime gameTime, UpdateMode mode)
{
Debug.Profiler.ProfileRendererFunction!();
@@ -807,6 +829,8 @@ namespace GlitchyEngine.World
_destroyQueue.Clear();
}
BuildTexts();
}
/// Creates a new Entity with the given name.
+2 -12
View File
@@ -25,10 +25,6 @@ class SceneRenderer
public RenderTargetGroup CompositeTarget => _compositeTarget;
Font _font ~ _.ReleaseRef();
FontRenderer.PreparedText _smallLinesInfo;
public this()
{
RenderTargetGroupDescription desc = .(100, 100,
@@ -52,8 +48,6 @@ class SceneRenderer
_cameraTarget.[Friend]Identifier = "Camera Target";
_gammaCorrectEffect = Content.LoadAsset("Resources/Shaders/GammaCorrect.hlsl");
_font = new Font(@"C:\Windows\Fonts\arial.ttf", 24);
}
/// Sets the size of the viewport into which the scene will be rendered.
@@ -214,14 +208,10 @@ class SceneRenderer
for (var (entity, transform, text, editorFlags) in Scene._ecsWorld.Enumerate<TransformComponent, TextRendererComponent, EditorFlagsComponent>())
{
if (editorFlags.Flags.HasFlag(.HideInScene))
if (editorFlags.Flags.HasFlag(.HideInScene) || text.PreparedText == null)
continue;
_smallLinesInfo = FontRenderer.PrepareText(_font, text.Text, 24, .Black);
FontRenderer.DrawText(_smallLinesInfo, transform.WorldTransform * Matrix.Scaling(1.0f / 24.0f));
_smallLinesInfo.ReleaseRef();
FontRenderer.DrawText(text.PreparedText, transform.WorldTransform * Matrix.Scaling(1.0f / 24.0f));
}
//FontRenderer.DrawText(_smallLinesInfo, 0, 0);