Files
Voile/Voile/Source/Resources/Font.cs

192 lines
4.9 KiB
C#

using System.Numerics;
using FreeTypeSharp;
using static FreeTypeSharp.FT;
using static FreeTypeSharp.FT_LOAD;
namespace Voile;
public struct Glyph
{
public int TextureId { get; set; } = -1;
public float Width { get; set; }
public float Height { get; set; }
public Vector2 Bearing { get; set; }
public int Advance { get; set; }
public Glyph() { }
}
/// <summary>
/// Represents font data.
/// </summary>
public class Font : Resource, IUpdatableResource, IDisposable
{
/// <summary>
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
/// </summary>
internal int Handle { get; set; } = -1;
public int Size { get; set; } = 16;
public byte[]? Buffer { get; private set; }
public long BufferSize { get; set; }
public bool Dirty => _dirty;
internal float UnitsPerEm;
internal nint FacePtr;
internal nint LibraryPtr;
internal List<int> Codepoints => _glyphs.Keys.ToList();
public Font(string path, byte[] buffer) : base(path)
{
Buffer = buffer;
}
public void Dispose()
{
unsafe
{
if (FacePtr != IntPtr.Zero)
FT_Done_Face((FT_FaceRec_*)FacePtr);
if (LibraryPtr != IntPtr.Zero)
FT_Done_FreeType((FT_LibraryRec_*)LibraryPtr);
}
}
/// <summary>
/// Loads a basic ASCII charset for this font.
/// </summary>
public void LoadAsciiData()
{
for (char c = ' '; c < 127; c++)
{
GetGlyph(c);
}
}
/// <summary>
/// Measures a given string using the font metrics.
/// </summary>
/// <param name="text">Text to measure.</param>
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
public Rect Measure(string text)
{
if (string.IsNullOrEmpty(text))
return Rect.Zero;
float totalWidth = 0;
float maxAscent = 0;
float maxDescent = 0;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
Glyph glyph = GetGlyph(c);
totalWidth += glyph.Advance;
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 = text[i - 1];
totalWidth += GetKerning(prevChar, c);
}
}
float totalHeight = maxAscent + maxDescent;
return new Rect(totalWidth, totalHeight);
}
public int GetKerning(char left, char right)
{
unsafe
{
if (FacePtr == IntPtr.Zero)
return 0;
FT_FaceRec_* face = (FT_FaceRec_*)FacePtr;
uint leftIndex = FT_Get_Char_Index(face, left);
uint rightIndex = FT_Get_Char_Index(face, right);
if (leftIndex == 0 || rightIndex == 0)
return 0;
FT_Vector_ kerning;
if (FT_Get_Kerning(face, leftIndex, rightIndex, FT_Kerning_Mode_.FT_KERNING_DEFAULT, &kerning) != 0)
return 0;
return (int)kerning.x;
}
}
public Glyph GetGlyph(char character)
{
if (!HasGlyph(character))
{
_glyphs.TryGetValue('?', out var defGlyph);
return defGlyph;
}
if (_glyphs.TryGetValue(character, out var glyph))
return glyph;
var loaded = LoadGlyph(character);
_glyphs[character] = loaded;
_dirty = true;
return loaded;
}
public bool HasGlyph(char character)
{
unsafe
{
var face = (FT_FaceRec_*)FacePtr;
return FT_Get_Char_Index(face, character) != 0;
}
}
private unsafe Glyph LoadGlyph(char character)
{
var face = (FT_FaceRec_*)FacePtr;
// TODO: for now, we're loading glyphs for metrics, but when implementing WebGPU rendering, we want to somehow render them to SDF or bitmap.
FT_Error error = FT_Set_Pixel_Sizes(face, 0, (uint)Size);
error = FT_Load_Char(face, character, FT_LOAD_NO_BITMAP);
if (error != 0)
throw new Exception($"Failed to load glyph for '{character}'");
var glyph = face->glyph;
var bitmap = glyph->bitmap;
var metrics = glyph->metrics;
return new Glyph
{
Width = metrics.width >> 6,
Height = metrics.height >> 6,
Bearing = new Vector2(metrics.horiBearingX >> 6, metrics.horiBearingY >> 6),
Advance = (int)metrics.horiAdvance >> 6,
};
}
public void MarkUpdated()
{
_dirty = false;
}
private bool _dirty;
private Dictionary<int, Glyph> _glyphs = new();
}