mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 13:01:52 +00:00
660 lines
16 KiB
Beef
660 lines
16 KiB
Beef
using System;
|
|
using FreeType;
|
|
using System.Diagnostics;
|
|
using GlitchyEngine.Math;
|
|
using System.Collections;
|
|
using GlitchyEngine.Core;
|
|
using GlitchyEngine.Content;
|
|
using System.Text;
|
|
using GlitchyEngine.World.Components;
|
|
using static FreeType.HarfBuzz;
|
|
|
|
using internal GlitchyEngine.Renderer.Text;
|
|
using internal GlitchyEngine.Renderer;
|
|
|
|
namespace GlitchyEngine.Renderer.Text
|
|
{
|
|
public static class FontRenderer
|
|
{
|
|
internal static FT_Library s_Library;
|
|
|
|
public static AssetHandle<Effect> _msdfEffect;
|
|
|
|
internal static bool s_isInitialized;
|
|
|
|
internal static void Init()
|
|
{
|
|
Debug.Profiler.ProfileFunction!();
|
|
|
|
if(s_isInitialized)
|
|
return;
|
|
|
|
InitFreetype();
|
|
|
|
_msdfEffect = Content.LoadAsset("Resources/Shaders/msdfShader.hlsl");
|
|
|
|
s_isInitialized = true;
|
|
}
|
|
|
|
internal static void Deinit()
|
|
{
|
|
Debug.Profiler.ProfileFunction!();
|
|
|
|
DeinitFreetype();
|
|
|
|
s_isInitialized = false;
|
|
}
|
|
|
|
private static void InitFreetype()
|
|
{
|
|
Debug.Profiler.ProfileFunction!();
|
|
|
|
if(s_Library == null)
|
|
{
|
|
var res = FreeType.Init_FreeType(&s_Library);
|
|
Log.EngineLogger.Assert(res.Success, scope $"Failed to initialize freetype ({(int)res}): {res}");
|
|
}
|
|
}
|
|
|
|
private static void DeinitFreetype()
|
|
{
|
|
Debug.Profiler.ProfileFunction!();
|
|
|
|
FreeType.Done_FreeType(s_Library);
|
|
}
|
|
|
|
public class PreparedText : RefCounter
|
|
{
|
|
//public List<PreparedLine> Lines ~ ClearAndDeleteItems!(_);
|
|
public Font Font ~ _?.ReleaseRef();
|
|
public List<PreparedGlyph> Glyphs ~ delete:append _;
|
|
|
|
public float AdvanceX;
|
|
public float AdvanceY;
|
|
|
|
[AllowAppend]
|
|
public this(Font font)
|
|
{
|
|
//List<PreparedLine> lines = append .();
|
|
//Lines = lines;
|
|
List<PreparedGlyph> glyphs = append .();
|
|
Glyphs = glyphs;
|
|
|
|
if(font != null)
|
|
Font = font..AddRef();
|
|
}
|
|
|
|
public static readonly Self Empty = new Self(null) ~ _.ReleaseRef();
|
|
|
|
/// Optimizes rendering of the text by sorting the glyphs
|
|
public void Optimize()
|
|
{
|
|
// Sort by texture -> render all glyphs with same font at once
|
|
Glyphs.Sort((lhs, rhs) => (int)Internal.UnsafeCastToPtr(lhs.Font._atlas) - (int)Internal.UnsafeCastToPtr(rhs.Font._atlas));
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
Glyphs.Clear();
|
|
}
|
|
}
|
|
/*
|
|
public class PreparedLine
|
|
{
|
|
public int Line;
|
|
public List<PreparedGlyph> Glyphs;
|
|
|
|
[AllowAppend]
|
|
public this()
|
|
{
|
|
List<PreparedGlyph> glyphs = append .();
|
|
Glyphs = glyphs;
|
|
}
|
|
}
|
|
*/
|
|
public struct PreparedGlyph
|
|
{
|
|
public Font Font;
|
|
public uint32 GlyphId;
|
|
public float2 Position;
|
|
public float Scale;
|
|
//public float AdvanceX;
|
|
//public float AdvanceY;
|
|
|
|
public this(Font font, uint32 glyphId, float2 position, float scale)//, float advanceX, float advanceY)
|
|
{
|
|
Font = font;
|
|
GlyphId = glyphId;
|
|
Position = position;
|
|
Scale = scale;
|
|
//AdvanceX = advanceX;
|
|
//AdvanceY = advanceY;
|
|
}
|
|
}
|
|
|
|
public enum TextDirection
|
|
{
|
|
case LeftToRight;
|
|
case RightToLeft;
|
|
case TopToBottom;
|
|
case BottomToTop;
|
|
}
|
|
|
|
public enum TextCase
|
|
{
|
|
case RetainCase;
|
|
case UpperCase;
|
|
case LowerCase;
|
|
case InvertCase;
|
|
}
|
|
|
|
public enum HorizontalTextAlignment
|
|
{
|
|
case Left;
|
|
case Right;
|
|
case Center;
|
|
}
|
|
|
|
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 void PrepareText(TextRendererComponent* textRenderer, Font font)
|
|
{
|
|
Debug.Profiler.ProfileRendererFunction!();
|
|
|
|
if (textRenderer.Text.IsWhiteSpace)
|
|
return;
|
|
|
|
if (textRenderer.PreparedText == null)
|
|
{
|
|
textRenderer.PreparedText = new FontRenderer.PreparedText(font);
|
|
}
|
|
else
|
|
{
|
|
textRenderer.PreparedText.AddRef();
|
|
}
|
|
|
|
PreparedText preparedText = textRenderer.PreparedText;
|
|
preparedText.Clear();
|
|
|
|
// The line we are writing on
|
|
float baseline = 0;
|
|
|
|
// Position of the next character on the line
|
|
float penPosition = 0;
|
|
|
|
List<char32> chars = new:ScopedAlloc! .(textRenderer.Text.Length);
|
|
int runStart = 0;
|
|
|
|
hb_buffer_t* buf = hb_buffer_create();
|
|
|
|
// Stack to keep track of the text direction.
|
|
StyleStack<TextDirection> textDirectionStack = scope .(.LeftToRight);
|
|
|
|
StyleStack<TextCase> textCaseStack = scope .(.RetainCase);
|
|
|
|
StyleStack<bool> richTextStack = scope .(true);
|
|
|
|
StyleStack<ColorRGBA> fontColorStack = scope .(textRenderer.Color);
|
|
|
|
StyleStack<float> fontSizeStack = scope .(textRenderer.FontSize);
|
|
|
|
StyleStack<Font> fontStack = scope .(font);
|
|
|
|
StyleStack<float> lineSpaceStack = scope .(1.0f);
|
|
|
|
float scale()
|
|
{
|
|
return fontSizeStack.CurrentValue() / fontStack.CurrentValue()._fontSize;
|
|
}
|
|
|
|
float linespace()
|
|
{
|
|
return (float)fontStack.CurrentValue()._linespace * lineSpaceStack.CurrentValue() * scale();
|
|
}
|
|
|
|
List<int> lineStartIndices = scope .();
|
|
lineStartIndices.Add(0);
|
|
List<float> lineWidths = scope .();
|
|
|
|
int movedLines = 0;
|
|
|
|
Font currentFont = font;
|
|
|
|
int currentIndex = 0;
|
|
|
|
void FlushShapeBuffer()
|
|
{
|
|
Debug.Profiler.ProfileRendererFunction!();
|
|
|
|
float fontScale = scale();
|
|
|
|
hb_buffer_clear_contents(buf);
|
|
|
|
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.
|
|
|
|
TextDirection direction = textDirectionStack.CurrentValue();
|
|
|
|
switch (direction)
|
|
{
|
|
case .LeftToRight:
|
|
hb_buffer_set_direction(buf, .HB_DIRECTION_LTR);
|
|
case .RightToLeft:
|
|
hb_buffer_set_direction(buf, .HB_DIRECTION_RTL);
|
|
case .TopToBottom:
|
|
hb_buffer_set_direction(buf, .HB_DIRECTION_TTB);
|
|
case .BottomToTop:
|
|
hb_buffer_set_direction(buf, .HB_DIRECTION_BTT);
|
|
}
|
|
|
|
hb_buffer_set_script(buf, .HB_SCRIPT_LATIN);
|
|
hb_buffer_set_language(buf, hb_language_from_string("en".CStr(), -1));
|
|
|
|
hb_shape(currentFont._harfBuzzFont, buf, null, 0);
|
|
|
|
|
|
// 5. Get the glyph and position information.
|
|
uint32 glyph_count = ?;
|
|
hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
|
|
hb_glyph_position_t* glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count);
|
|
|
|
for (uint32 i < glyph_count)
|
|
{
|
|
hb_codepoint_t glyphid = glyph_info[i].codepoint;
|
|
//hb_position_t x_offset = glyph_pos[i].x_offset;
|
|
//hb_position_t y_offset = glyph_pos[i].y_offset;
|
|
hb_position_t x_advance = glyph_pos[i].x_advance;
|
|
hb_position_t y_advance = glyph_pos[i].y_advance;
|
|
|
|
// TODO Store color in glyph
|
|
PreparedGlyph glyph = .(currentFont, glyphid, .(penPosition, baseline), fontScale);//, x_advance / 64, y_advance / 64);
|
|
|
|
preparedText.Glyphs.Add(glyph);
|
|
|
|
penPosition += (x_advance / 64) * fontScale;
|
|
baseline += (y_advance / 64) * fontScale;
|
|
}
|
|
|
|
preparedText.AdvanceX = Math.Max(preparedText.AdvanceX, penPosition);
|
|
}
|
|
|
|
void ResetPenPos()
|
|
{
|
|
preparedText.AdvanceX = Math.Max(preparedText.AdvanceX, penPosition);
|
|
penPosition = 0;
|
|
}
|
|
|
|
bool escapeNextChar = false;
|
|
|
|
mainEnumerator: for (char32 char in textRenderer.Text.DecodedChars)
|
|
{
|
|
defer
|
|
{
|
|
currentIndex = @char.NextIndex;
|
|
}
|
|
|
|
void DoLineBreak()
|
|
{
|
|
//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();
|
|
|
|
// carriage return
|
|
ResetPenPos();
|
|
|
|
continue;
|
|
case '\u{2066}':
|
|
// LEFT-TO-RIGHT ISOLATE
|
|
FlushShapeBuffer();
|
|
|
|
textDirectionStack.Push(.RightToLeft);
|
|
|
|
continue;
|
|
case '\u{2067}':
|
|
// RIGHT-TO-LEFT ISOLATE
|
|
FlushShapeBuffer();
|
|
|
|
textDirectionStack.Push(.RightToLeft);
|
|
|
|
continue;
|
|
// TODO: FIRST STRONG ISOLATE (U+2068)
|
|
case '\u{2069}':
|
|
// POP DIRECTIONAL ISOLATE
|
|
FlushShapeBuffer();
|
|
|
|
textDirectionStack.Pop();
|
|
|
|
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<char32>(&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);
|
|
|
|
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":
|
|
// 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":
|
|
textCaseStack.PushButPopIfTrue(.LowerCase, isEndTag);
|
|
case "uc", "uppercase":
|
|
if (isEndTag)
|
|
textCaseStack.PushButPopIfTrue(.UpperCase, isEndTag);
|
|
case "rc", "retaincase":
|
|
textCaseStack.PushButPopIfTrue(.RetainCase, isEndTag);
|
|
case "ic", "invertcase":
|
|
textCaseStack.PushButPopIfTrue(.InvertCase, isEndTag);
|
|
}
|
|
|
|
// Skip the tag in the main enumerator
|
|
@char.NextIndex = endIndex;
|
|
continue;
|
|
}
|
|
|
|
escapeNextChar = false;
|
|
|
|
switch (textCaseStack.CurrentValue())
|
|
{
|
|
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").
|
|
Font charFont = currentFont.GetDrawingFont(char) ?? font;
|
|
|
|
if (charFont != currentFont)
|
|
{
|
|
FlushShapeBuffer();
|
|
|
|
currentFont = charFont;
|
|
}
|
|
|
|
// move baseline if necessary
|
|
if(movedLines != 0)
|
|
{
|
|
// move baseline
|
|
baseline -= linespace() * movedLines;
|
|
|
|
lineWidths.Add(penPosition);
|
|
|
|
// TODO: make carriage return optional?
|
|
// return pen to start of line
|
|
ResetPenPos();
|
|
|
|
movedLines = 0;
|
|
|
|
lineStartIndices.Add(preparedText.Glyphs.Count);
|
|
}
|
|
|
|
chars.Add(char);
|
|
}
|
|
|
|
FlushShapeBuffer();
|
|
|
|
lineWidths.Add(penPosition);
|
|
|
|
for (int i = 0; i < lineStartIndices.Count; i++)
|
|
{
|
|
int currentLineStart = lineStartIndices[i];
|
|
int nextLineStart = i < (lineStartIndices.Count - 1) ? lineStartIndices[i + 1] : preparedText.Glyphs.Count;
|
|
|
|
float offset = 0.0f;
|
|
|
|
if (textRenderer.HorizontalAlignment == .Center)
|
|
offset = lineWidths[i] / 2.0f;
|
|
else if (textRenderer.HorizontalAlignment == .Right)
|
|
offset = lineWidths[i];
|
|
|
|
for (int glyphIndex = currentLineStart; glyphIndex < nextLineStart; glyphIndex++)
|
|
{
|
|
preparedText.Glyphs[glyphIndex].Position.X -= offset;
|
|
}
|
|
}
|
|
|
|
preparedText.AdvanceY = baseline;
|
|
|
|
preparedText.Optimize();
|
|
|
|
textRenderer.PreparedText.ReleaseRef();
|
|
textRenderer.NeedsRebuild = false;
|
|
}
|
|
|
|
public static void DrawText(PreparedText text, Matrix transform, ColorRGBA fontColor = .White)
|
|
{
|
|
Debug.Profiler.ProfileRendererFunction!();
|
|
|
|
text.AddRef();
|
|
defer text.ReleaseRef();
|
|
|
|
if (text.Glyphs.Count == 0)
|
|
return;
|
|
|
|
Renderer2D.Flush();
|
|
|
|
// TODO: this is very not good!
|
|
// At some point we will support using materials with the 2D renderer. At that point we can simply bind different materials with fonts (or glyphs?) and don't need this hack anymore. One day...
|
|
var lastEffect = Renderer2D.[Friend]s_currentQuadEffect;
|
|
Renderer2D.[Friend]s_currentQuadEffect = _msdfEffect;
|
|
// TODO: oh no....
|
|
// Copy viewProjection from current effect
|
|
Matrix viewProjection = Renderer2D.[Friend]s_quadBatchEffect.Variables["ViewProjection"].[Friend]GetData<Matrix>();
|
|
_msdfEffect.Variables["ViewProjection"].SetData(viewProjection);
|
|
|
|
// TODO: this doesn't really work with fallback fonts
|
|
float2 unitRange = ((float)text.Font._range) / float2(text.Font._atlas.Width, text.Font._atlas.Height);
|
|
_msdfEffect.Variables["UnitRange"].SetData(unitRange);
|
|
|
|
List<Texture2D> atlasses = scope .();
|
|
|
|
for (PreparedGlyph glyph in text.Glyphs)
|
|
{
|
|
// Get glyph from Font
|
|
var glyphDesc = glyph.Font.GetGlyph(glyph.GlyphId);
|
|
|
|
// This never happens
|
|
Debug.Assert(glyphDesc != null);
|
|
|
|
// The glyph might belong to a fallback font
|
|
var glyphFont = glyphDesc.Font;
|
|
//float glyphFontScale = (float)glyph.Scale / glyphFont._fontSize;
|
|
|
|
Texture2D atlas = glyphFont._atlas;
|
|
|
|
// Add reference to atlas in case it is recreated during rendering
|
|
if(!atlasses.Contains(atlas))
|
|
{
|
|
atlasses.Add(atlas..AddRef());
|
|
}
|
|
|
|
float2 atlasSize = .(atlas.Width, atlas.Height);
|
|
|
|
float adjustToPenX = glyphDesc.AdjustToPen;
|
|
//adjustToPenX *= glyphFontScale;
|
|
adjustToPenX *= glyph.Scale;
|
|
|
|
float adjustToBaseline = glyphDesc.AdjustToBaseLine;
|
|
//adjustToBaseline *= glyphFontScale;
|
|
adjustToBaseline *= glyph.Scale;
|
|
|
|
// Rectangle on the screen
|
|
float4 viewportRect = .(
|
|
// TODO: merge adjustToPenX and adjustToBaseline into Position
|
|
glyph.Position.X + adjustToPenX,
|
|
glyph.Position.Y + adjustToBaseline,
|
|
glyphDesc.Width * glyph.Scale,
|
|
glyphDesc.Height * glyph.Scale);
|
|
|
|
// Rectangle on the font atlas
|
|
float4 texRect = .(glyphDesc.MapCoord.X, glyphDesc.MapCoord.Y, glyphDesc.Width, glyphDesc.Height);
|
|
|
|
ColorRGBA glyphColor = fontColor;//Color.White;// TODO: glyphDesc.IsBitmap ? bitmapColor : fontColor;
|
|
|
|
texRect /= float4(atlasSize, atlasSize);
|
|
|
|
// Show quads
|
|
// Renderer2D.DrawQuad(float3(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2, 1), .(viewportRect.Z, viewportRect.W), 0, .Red);
|
|
|
|
float3 position = .(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2, 0);
|
|
|
|
Matrix glyphTransform = transform * Matrix.Translation(position) * Matrix.Scaling(viewportRect.Z, viewportRect.W, 1.0f);
|
|
|
|
Renderer2D.DrawQuad(glyphTransform, atlas, glyphColor, texRect);
|
|
//Renderer2D.DrawQuad(float2(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2), .(viewportRect.Z, viewportRect.W), 0, atlas, glyphColor, texRect);
|
|
|
|
// Show pen positions
|
|
// Renderer2D.DrawQuad(float3(float2(x, y) + glyph.Position, -1), .(1), 0, .Green);
|
|
}
|
|
|
|
Renderer2D.Flush();
|
|
|
|
// TODO: not good!
|
|
// Change back effect
|
|
Renderer2D.[Friend]s_currentQuadEffect = lastEffect;
|
|
|
|
// release all atlas textures
|
|
for(int i < atlasses.Count)
|
|
{
|
|
atlasses[i].ReleaseRef();
|
|
}
|
|
}
|
|
}
|
|
}
|