diff --git a/GlitchyEditor/resources/Shaders/msdfShader.hlsl b/GlitchyEditor/resources/Shaders/msdfShader.hlsl index 69207c4..886f650 100644 --- a/GlitchyEditor/resources/Shaders/msdfShader.hlsl +++ b/GlitchyEditor/resources/Shaders/msdfShader.hlsl @@ -74,7 +74,11 @@ float4 PS(PS_Input input) : SV_Target0 float screenPxDistance = ScreenPxRange(input.TexCoord) * (sd - 0.5); float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); - return float4(input.Color.rgb, opacity * input.Color.a); + clip(opacity - 0.001f); + + //return float4(input.Color.rgb, opacity * input.Color.a) * 0.0001f + float4(opacity.xxxx); + return input.Color * opacity; // TODO: This is not quite right // float4(input.Color.rgb, input.Color.a * opacity); + //return float4(input.Color.rgb, opacity * input.Color.a) * 0.0001f + float4(msd, 1.0f); } /* diff --git a/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf b/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf index 2a40b9d..6bf2347 100644 --- a/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf +++ b/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf @@ -9,6 +9,7 @@ using GlitchyEngine.Content; using GlitchyEngine.Scripting; using GlitchyEngine.Core; using Mono; +using GlitchyEngine.World.Components; namespace GlitchyEditor.EditWindows { @@ -81,9 +82,12 @@ namespace GlitchyEditor.EditWindows ShowComponentEditor("Sprite Renderer", entity, => ShowSpriteRendererComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Circle Renderer", entity, => ShowCircleRendererComponentEditor, => ShowComponentContextMenu); + ShowComponentEditor("Text Renderer", entity, => ShowTextRendererComponentEditor, => ShowComponentContextMenu); + ShowComponentEditor("Mesh Renderer", entity, => ShowMeshRendererComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Light", entity, => ShowLightComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Mesh", entity, => ShowMeshComponentEditor, => ShowComponentContextMenu); + ShowComponentEditor("Rigidbody 2D", entity, => ShowRigidBody2DComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Box collider 2D", entity, => ShowBoxCollider2DComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Circle collider 2D", entity, => ShowCircleCollider2DComponentEditor, => ShowComponentContextMenu); @@ -453,6 +457,49 @@ namespace GlitchyEditor.EditWindows StartNewProperty("Inner Radius"); ImGui.DragFloat("##Inner Radius", &circleRendererComponent.InnerRadius, 0.1f, 0.0f, 1.0f); } + + static int InputTextCallback(ImGui.InputTextCallbackData* data) + { + if (data.EventFlag == .CallbackResize) + { + String str = (String)Internal.UnsafeCastToObject(data.UserData); + + Log.EngineLogger.AssertDebug(str.Ptr == data.Buf); + + str.PadRight(data.BufTextLen * 2, '\0'); + data.Buf = str.Ptr; + + return 0; + } + + return 0; + } + + private static void ShowTextRendererComponentEditor(Entity entity, TextRendererComponent* textRendererComponent) + { + StartNewProperty("Text"); + + String text = textRendererComponent.[Friend]_text; + + if (text == null) + { + text = textRendererComponent.[Friend]_text = new String(); + } + + text?.EnsureNullTerminator(); + + if (ImGui.InputTextMultiline("##text", text.Ptr, (uint64)text.Length, .Zero, .CallbackResize, => InputTextCallback, Internal.UnsafeCastToPtr(text))) + { + // To deleting text, ImGui simply puts a null char after the remaining text. + // Search for null char and set the length of the string accordingly. + int length = text.IndexOf('\0'); + + if (length >= 0) + { + text.Length = length; + } + } + } private static void ShowMeshRendererComponentEditor(Entity entity, MeshRendererComponent* meshRendererComponent) { @@ -844,6 +891,7 @@ namespace GlitchyEditor.EditWindows ShowComponentButton("Camera"); ShowComponentButton("Sprite Renderer"); ShowComponentButton("Circle Renderer"); + ShowComponentButton("Text Renderer"); ShowComponentButton("Light"); ShowComponentButton("Rigidbody 2D"); ShowComponentButton("Box collider 2D"); diff --git a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf index d4d2858..6eb355a 100644 --- a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf +++ b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf @@ -5,6 +5,7 @@ using GlitchyEngine.Math; using System.Collections; using GlitchyEngine.Core; using GlitchyEngine.Content; +using System.Text; using static FreeType.HarfBuzz; using internal GlitchyEngine.Renderer.Text; @@ -126,7 +127,15 @@ namespace GlitchyEngine.Renderer.Text case BottomToTop; } - public static PreparedText PrepareText(Font font, String text, float fontSize, Color fontColor = .White, Color bitmapColor = .White, float lineSpaceScale = 1.0f, TextDirection direction = .LeftToRight) + public enum TextCase + { + case RetainCase; + case UpperCase; + case LowerCase; + case InvertCase; + } + + 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!(); @@ -143,17 +152,18 @@ namespace GlitchyEngine.Renderer.Text // Position of the next character on the line float penPosition = 0; - // TODO: use stringview? - StringView textBuffer = .(text, 0, 0);//new:ScopedAlloc! String(); - - hb_buffer_t* buf = hb_buffer_create(); + List chars = new:ScopedAlloc! .(text.Length); + int runStart = 0; - //PreparedLine currentLine = new PreparedLine(); + hb_buffer_t* buf = hb_buffer_create(); // Stack to keep track of the text direction. List textDirectionStack = scope .(); textDirectionStack.Add(direction); - + + List textCaseStack = scope .(); + textCaseStack.Add(.RetainCase); + int movedLines = 0; Font currentFont = font; @@ -168,10 +178,9 @@ namespace GlitchyEngine.Renderer.Text hb_buffer_clear_contents(buf); - hb_buffer_add_utf8(buf, text.Ptr, (.)text.Length, (.)(textBuffer.Ptr - text.Ptr), (.)textBuffer.Length); - - textBuffer.Ptr += textBuffer.Length; - textBuffer.Length = 0; + hb_buffer_add_utf32(buf, chars.Ptr, (.)chars.Count, (.)runStart, (.)(chars.Count - runStart)); + + runStart = chars.Count; // Set the script, language and direction of the buffer. // TODO: change direction, script, etc. @@ -226,6 +235,8 @@ namespace GlitchyEngine.Renderer.Text penPosition = 0; } + bool escapeNextChar = false; + for (char32 char in text.DecodedChars) { defer @@ -233,16 +244,21 @@ namespace GlitchyEngine.Renderer.Text currentIndex = @char.NextIndex; } - switch(char) + void DoLineBreak() { - case '\n': + chars.Add('\n'); // Only flush after the first new line if (movedLines == 0) FlushShapeBuffer(); // carriage return movedLines++; + } + switch(char) + { + case '\n': + DoLineBreak(); continue; case '\u{240}': FlushShapeBuffer(); @@ -274,6 +290,107 @@ namespace GlitchyEngine.Renderer.Text textDirectionStack.PopBack(); continue; + case '\\': + if (escapeNextChar) + break; + else + { + escapeNextChar = true; + continue; + } + case '<': + if (escapeNextChar) + break; + + // Copy the char enumerator to make a lookahead + String.UTF8Enumerator tagEnumerator = @char; + + String tagText = scope .(); + + int endIndex = -1; + + for (char32 tagChar in tagEnumerator) + { + if (tagChar == '>') + { + endIndex = tagEnumerator.NextIndex; + break; + } + else + { + UTF32.Decode(Span(&tagChar, 1), tagText); + } + } + + // It seems that it's just an unescaped < + if (endIndex == -1) + break; // TODO: Probably warn or something + + bool isEndTag = tagText.StartsWith('/'); + + if (isEndTag) + tagText.Remove(0); + + switch(tagText) + { + case "b", "bold": + // bold + case "br", "linebreak": + DoLineBreak(); + case "u", "underline": + // underlined + case "i", "italic": + // italic + case "small": + case "sub", "subscript": + case "sup", "superscript": + case "s", "strikethrough": + + // Text case control + case "lc", "lowercase": + if (isEndTag && textCaseStack.Count > 0) + textCaseStack.PopBack(); + else + textCaseStack.Add(.LowerCase); + case "uc", "uppercase": + if (isEndTag && textCaseStack.Count > 0) + textCaseStack.PopBack(); + else + textCaseStack.Add(.UpperCase); + case "rc", "retaincase": + if (isEndTag && textCaseStack.Count > 0) + textCaseStack.PopBack(); + else + textCaseStack.Add(.RetainCase); + case "ic", "invertcase": + if (isEndTag && textCaseStack.Count > 0) + textCaseStack.PopBack(); + else + textCaseStack.Add(.InvertCase); + + } + + // Skip the tag in the main enumerator + @char.NextIndex = endIndex; + + continue; + } + + escapeNextChar = false; + + switch (textCaseStack.Back) + { + case .RetainCase: + // Nothing to do + case .LowerCase: + char = char.ToLower; + case .UpperCase: + char = char.ToUpper; + case .InvertCase: + if (char.IsUpper) + char = char.ToLower; + else if (char.IsLower) + char = char.ToUpper; } // Get the font that can draw the char. Use the given font if no fallback is found (will draw the "missing glyph"). @@ -299,12 +416,7 @@ namespace GlitchyEngine.Renderer.Text movedLines = 0; } - //textBuffer.Append(char); - if(textBuffer.Length == 0) - { - textBuffer.Ptr = text.Ptr + currentIndex; - } - textBuffer.Length += @char.NextIndex - currentIndex; + chars.Add(char); } FlushShapeBuffer(); @@ -336,7 +448,7 @@ namespace GlitchyEngine.Renderer.Text Renderer2D.[Friend]s_currentQuadEffect = _msdfEffect; // TODO: oh no.... // Copy viewProjection from current effect - Matrix viewProjection = lastEffect.Variables["ViewProjection"].[Friend]GetData(); + Matrix viewProjection = Renderer2D.[Friend]s_quadBatchEffect.Variables["ViewProjection"].[Friend]GetData(); _msdfEffect.Variables["ViewProjection"].SetData(viewProjection); // TODO: this doesn't really work with fallback fonts diff --git a/GlitchyEngine/src/World/Components/TextRendererComponent.bf b/GlitchyEngine/src/World/Components/TextRendererComponent.bf new file mode 100644 index 0000000..6149c7f --- /dev/null +++ b/GlitchyEngine/src/World/Components/TextRendererComponent.bf @@ -0,0 +1,32 @@ +using System; +namespace GlitchyEngine.World.Components; + +struct TextRendererComponent : IDisposableComponent +{ + private String _text; + + private bool _isRichText; + + public StringView Text + { + get => _text; + set mut + { + if (_text == null) + _text = new String(); + + _text.Set(value); + } + } + + public bool IsRichText + { + get => _isRichText; + set mut => _isRichText = value; + } + + public void Dispose() + { + delete _text; + } +} diff --git a/GlitchyEngine/src/World/SceneRenderer.bf b/GlitchyEngine/src/World/SceneRenderer.bf index c0347ae..4dd0347 100644 --- a/GlitchyEngine/src/World/SceneRenderer.bf +++ b/GlitchyEngine/src/World/SceneRenderer.bf @@ -3,6 +3,7 @@ using GlitchyEngine.Content; using GlitchyEngine.Math; using System; using GlitchyEngine.World.Components; +using GlitchyEngine.Renderer.Text; namespace GlitchyEngine.World; @@ -24,6 +25,10 @@ class SceneRenderer public RenderTargetGroup CompositeTarget => _compositeTarget; + Font _font ~ _.ReleaseRef(); + + FontRenderer.PreparedText _smallLinesInfo; + public this() { RenderTargetGroupDescription desc = .(100, 100, @@ -47,6 +52,8 @@ 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. @@ -204,6 +211,20 @@ class SceneRenderer Renderer2D.DrawCircle(transform.WorldTransform, circle, entity.Index); } + + for (var (entity, transform, text, editorFlags) in Scene._ecsWorld.Enumerate()) + { + if (editorFlags.Flags.HasFlag(.HideInScene)) + continue; + + _smallLinesInfo = FontRenderer.PrepareText(_font, text.Text, 24, .Black); + + FontRenderer.DrawText(_smallLinesInfo, transform.WorldTransform * Matrix.Scaling(1.0f / 24.0f)); + + _smallLinesInfo.ReleaseRef(); + } + + //FontRenderer.DrawText(_smallLinesInfo, 0, 0); Renderer2D.EndScene(); diff --git a/GlitchyEngine/src/World/SceneSerializer.bf b/GlitchyEngine/src/World/SceneSerializer.bf index 4bd2a32..5722815 100644 --- a/GlitchyEngine/src/World/SceneSerializer.bf +++ b/GlitchyEngine/src/World/SceneSerializer.bf @@ -249,6 +249,12 @@ class SceneSerializer { Serialize.Value(writer, "Flags", component.Flags); }); + + SerializeComponent(writer, entity, "TextRenderer", scope (component) => + { + Serialize.Value(writer, "IsRichText", component.IsRichText); + Serialize.Value(writer, "Text", component.Text); + }); } writer.EntryEnd(); @@ -671,6 +677,27 @@ class SceneSerializer { Try!(Deserialize.Value(reader, "Flags", out component.Flags)); + return .Ok; + })); + case "TextRenderer": + Try!(DeserializeComponent(reader, entity, scope (component) => + { + Try!(Deserialize.Value(reader, "IsRichText", let isRichText)); + component.IsRichText = isRichText; + + Try!(reader.EntryEnd()); + + String text = null; + + Try!(Deserialize.Value(reader, "Text", out text)); + + if (text != null) + { + component.Text = text; + + delete text; + } + return .Ok; })); default: