diff --git a/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf b/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf index 6bf2347..cd2d448 100644 --- a/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf +++ b/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf @@ -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; } } diff --git a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf index 6eb355a..7801759 100644 --- a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf +++ b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf @@ -135,6 +135,49 @@ namespace GlitchyEngine.Renderer.Text case InvertCase; } + class StyleStack + { + private append List _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 textDirectionStack = scope .(); - textDirectionStack.Add(direction); + StyleStack textDirectionStack = scope .(direction); - List textCaseStack = scope .(); - textCaseStack.Add(.RetainCase); + StyleStack textCaseStack = scope .(.RetainCase); + + StyleStack 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 diff --git a/GlitchyEngine/src/World/Components/TextRendererComponent.bf b/GlitchyEngine/src/World/Components/TextRendererComponent.bf index 6149c7f..5984b92 100644 --- a/GlitchyEngine/src/World/Components/TextRendererComponent.bf +++ b/GlitchyEngine/src/World/Components/TextRendererComponent.bf @@ -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 PreparedText _preparedText; - private bool _isRichText; + 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(); } } diff --git a/GlitchyEngine/src/World/Scene.bf b/GlitchyEngine/src/World/Scene.bf index c35b294..20971f0 100644 --- a/GlitchyEngine/src/World/Scene.bf +++ b/GlitchyEngine/src/World/Scene.bf @@ -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 { @@ -55,6 +57,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()) + { + 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. diff --git a/GlitchyEngine/src/World/SceneRenderer.bf b/GlitchyEngine/src/World/SceneRenderer.bf index 4dd0347..2553b16 100644 --- a/GlitchyEngine/src/World/SceneRenderer.bf +++ b/GlitchyEngine/src/World/SceneRenderer.bf @@ -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()) { - 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);