mirror of
https://gitlab.acidiclight.dev/sociallydistant/sociallydistant.git
synced 2025-01-22 09:31:47 -05:00
Emoji support
* Add support for raw emoji parsing in TextWidget * Fix SDL not honouring Wayland Support setting * Fix improper markup parsing in TextWidget after emoji support was added * Use Twemoji for emoji, and add support for COLR font glyphs. * Add support for emoji rendering in TextWidget if the font is provided * Add support for parsing emoji shortcodes in TextWidget * Add emoji lookup table to AcidicGUI * Add Noto Color Emoji font for future use
This commit is contained in:
parent
f22f252aac
commit
d2565530ed
16 changed files with 3959 additions and 108 deletions
|
@ -3,13 +3,13 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-sdcb": {
|
"dotnet-sdcb": {
|
||||||
"version": "1.0.0.456",
|
"version": "1.0.0.568",
|
||||||
"commands": [
|
"commands": [
|
||||||
"sdcb"
|
"sdcb"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dotnet-sdfxc": {
|
"dotnet-sdfxc": {
|
||||||
"version": "1.0.0.456",
|
"version": "1.0.0.568",
|
||||||
"commands": [
|
"commands": [
|
||||||
"sdfxc"
|
"sdfxc"
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FreeTypeSharp" Version="3.0.0" />
|
<PackageReference Include="FreeTypeSharp" Version="3.0.0" />
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="1.0.0.456" />
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="1.0.0.568" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
3368
src/AcidicGUI/Common/Emojis.cs
Normal file
3368
src/AcidicGUI/Common/Emojis.cs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,22 +1,34 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
using System.Reflection.Metadata;
|
using System.Reflection.Metadata;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
using AcidicGUI.Rendering;
|
using AcidicGUI.Rendering;
|
||||||
using FreeTypeSharp;
|
using FreeTypeSharp;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using Color = Microsoft.Xna.Framework.Color;
|
||||||
|
using Point = Microsoft.Xna.Framework.Point;
|
||||||
|
using Rectangle = Microsoft.Xna.Framework.Rectangle;
|
||||||
|
|
||||||
namespace AcidicGUI.TextRendering;
|
namespace AcidicGUI.TextRendering;
|
||||||
|
|
||||||
public class Typeface : Font
|
public class Typeface : Font
|
||||||
{
|
{
|
||||||
private static readonly FreeTypeLibrary library = new();
|
private static readonly FreeTypeLibrary library = new();
|
||||||
|
private static readonly Rune carriageReturn = new Rune('\r');
|
||||||
|
private static readonly Rune lineFeed = new Rune('\n');
|
||||||
private readonly GraphicsDevice graphicsDevice;
|
private readonly GraphicsDevice graphicsDevice;
|
||||||
private readonly Atlas referenceAtlas;
|
private readonly Atlas referenceAtlas;
|
||||||
private static bool hasInitialized;
|
private static bool hasInitialized;
|
||||||
private readonly int baseFontSize;
|
private readonly int baseFontSize;
|
||||||
private readonly Memory<byte> fontData;
|
private readonly Memory<byte> fontData;
|
||||||
private readonly Dictionary<GraphicsDevice, AtlasContainer> atlases = new();
|
private readonly Dictionary<GraphicsDevice, AtlasContainer> atlases = new();
|
||||||
|
private readonly Dictionary<uint, Range> svgGlyphRanges = new();
|
||||||
|
private bool hasReadSvgTable = false;
|
||||||
|
|
||||||
|
|
||||||
private unsafe Typeface(byte[] data, int referenceFontSize, GraphicsDevice graphicsDevice)
|
private unsafe Typeface(byte[] data, int referenceFontSize, GraphicsDevice graphicsDevice)
|
||||||
{
|
{
|
||||||
this.graphicsDevice = graphicsDevice;
|
this.graphicsDevice = graphicsDevice;
|
||||||
|
@ -27,11 +39,89 @@ public class Typeface : Font
|
||||||
referenceAtlas = GetAtlas(graphicsDevice, referenceFontSize);
|
referenceAtlas = GetAtlas(graphicsDevice, referenceFontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? GetSvgDocument(uint glyph)
|
||||||
|
{
|
||||||
|
var fontSpan = fontData.Span;
|
||||||
|
Span<byte> identifierSpan = stackalloc byte[4];
|
||||||
|
|
||||||
|
identifierSpan[0] = (byte) 'S';
|
||||||
|
identifierSpan[1] = (byte) 'V';
|
||||||
|
identifierSpan[2] = (byte)'G';
|
||||||
|
identifierSpan[3] = (byte)' ';
|
||||||
|
|
||||||
|
if (!hasReadSvgTable)
|
||||||
|
{
|
||||||
|
hasReadSvgTable = true;
|
||||||
|
|
||||||
|
int svgOffset = 0;
|
||||||
|
int svgLength = 0;
|
||||||
|
|
||||||
|
var fontHeader = fontSpan.Slice(0, 12);
|
||||||
|
var numTablesRaw = fontHeader.Slice(4, 2);
|
||||||
|
var numTables = BinaryPrimitives.ReadUInt16BigEndian(numTablesRaw);
|
||||||
|
|
||||||
|
for (var i = 0; i < numTables; i++)
|
||||||
|
{
|
||||||
|
const int size = 16;
|
||||||
|
var offset = 12 + size * i;
|
||||||
|
|
||||||
|
var descriptor = fontSpan.Slice(offset, size);
|
||||||
|
var tag = descriptor.Slice(0, identifierSpan.Length);
|
||||||
|
|
||||||
|
if (!identifierSpan.SequenceEqual(tag))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
svgOffset = BinaryPrimitives.ReadInt32BigEndian(descriptor.Slice(8, 4));
|
||||||
|
svgLength = BinaryPrimitives.ReadInt32BigEndian(descriptor.Slice(12, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (svgOffset == 0 || svgLength == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var svgSpan = fontSpan.Slice((int)svgOffset, (int)svgLength);
|
||||||
|
|
||||||
|
var offsetToSvgDocIndex = BinaryPrimitives.ReadInt32BigEndian(svgSpan.Slice(2, 4));
|
||||||
|
|
||||||
|
var docIndex = svgSpan.Slice(offsetToSvgDocIndex);
|
||||||
|
var numEntries = BinaryPrimitives.ReadUInt16BigEndian(docIndex.Slice(0, 2));
|
||||||
|
|
||||||
|
var entriesSpan = docIndex.Slice(2);
|
||||||
|
|
||||||
|
for (var i = 0; i < numEntries; i++)
|
||||||
|
{
|
||||||
|
const int size = 12;
|
||||||
|
var entry = entriesSpan.Slice(size * i, size);
|
||||||
|
|
||||||
|
var startingGlyph = BinaryPrimitives.ReadUInt16BigEndian(entry.Slice(0, 2));
|
||||||
|
var endingGlyph = BinaryPrimitives.ReadUInt16BigEndian(entry.Slice(2, 2));
|
||||||
|
var offset = BinaryPrimitives.ReadInt32BigEndian(entry.Slice(4, 4));
|
||||||
|
var length = BinaryPrimitives.ReadInt32BigEndian(entry.Slice(8, 4));
|
||||||
|
|
||||||
|
var range = new Range((int)(svgOffset + offsetToSvgDocIndex + offset), (int)(svgOffset + offsetToSvgDocIndex + offset + length));
|
||||||
|
|
||||||
|
for (var glyphIndex = startingGlyph; glyphIndex <= endingGlyph; glyphIndex++)
|
||||||
|
{
|
||||||
|
svgGlyphRanges.Add(glyphIndex, range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!svgGlyphRanges.TryGetValue(glyph, out var svgRange))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var (stringOffset, stringLength) = svgRange.GetOffsetAndLength(fontSpan.Length);
|
||||||
|
|
||||||
|
var svgData = fontSpan.Slice(stringOffset, stringLength);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(svgData);
|
||||||
|
}
|
||||||
|
|
||||||
public override float ReferenceFontSize => referenceAtlas.FontSize;
|
public override float ReferenceFontSize => referenceAtlas.FontSize;
|
||||||
public override float UnderlinePosition => referenceAtlas.UnderlinePosition;
|
public override float UnderlinePosition => referenceAtlas.UnderlinePosition;
|
||||||
|
|
||||||
public override unsafe Point Measure(string text, int? fontSize = null)
|
public override unsafe Point Measure(string text, int? fontSize = null)
|
||||||
{
|
{
|
||||||
|
var runes = text.EnumerateRunes().ToArray();
|
||||||
var atlas = GetAtlas(graphicsDevice, fontSize ?? referenceAtlas.FontSize);
|
var atlas = GetAtlas(graphicsDevice, fontSize ?? referenceAtlas.FontSize);
|
||||||
|
|
||||||
float lineHeight = atlas.LineHeight;
|
float lineHeight = atlas.LineHeight;
|
||||||
|
@ -40,27 +130,25 @@ public class Typeface : Font
|
||||||
float width = 0;
|
float width = 0;
|
||||||
float height = 0;
|
float height = 0;
|
||||||
|
|
||||||
var chars = text.AsSpan();
|
atlas.RasterizeGlyphs(runes);
|
||||||
|
|
||||||
atlas.RasterizeGlyphs(chars);
|
|
||||||
|
|
||||||
var lastWasNewline = false;
|
var lastWasNewline = false;
|
||||||
var info = default(GlyphInfo);
|
var info = default(GlyphInfo);
|
||||||
for (var i = 0; i < chars.Length; i++)
|
for (var i = 0; i < runes.Length; i++)
|
||||||
{
|
{
|
||||||
char character = chars[i];
|
Rune character = runes[i];
|
||||||
|
|
||||||
// Newline characters
|
// Newline characters
|
||||||
if (character == '\r' || character == '\n')
|
if (character == carriageReturn || character == lineFeed)
|
||||||
{
|
{
|
||||||
// Check the next char after this one. If it's also a newline, but isn't the same kind as
|
// Check the next char after this one. If it's also a newline, but isn't the same kind as
|
||||||
// the current character, we treat both newlines as if they're one.
|
// the current character, we treat both newlines as if they're one.
|
||||||
//
|
//
|
||||||
// This will gracefully handle "\r\n" and "\n\r".
|
// This will gracefully handle "\r\n" and "\n\r".
|
||||||
if (i + 1 < chars.Length)
|
if (i + 1 < runes.Length)
|
||||||
{
|
{
|
||||||
char nextChar = chars[i + 1];
|
var nextChar = runes[i + 1];
|
||||||
if (nextChar != character && nextChar == '\r' || nextChar == '\n')
|
if (nextChar != character && nextChar == carriageReturn || nextChar == lineFeed)
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,8 +183,14 @@ public class Typeface : Font
|
||||||
int? fontSize = null
|
int? fontSize = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
if (geometryHelper.GraphicsDevice != this.graphicsDevice)
|
if (geometryHelper.GraphicsDevice != this.graphicsDevice)
|
||||||
throw new InvalidOperationException("You may only use this typeface to render text to the same graphics device that the typeface was created with.");
|
throw new InvalidOperationException("You may only use this typeface to render text to the same graphics device that the typeface was created with.");
|
||||||
|
|
||||||
|
var runes = text.EnumerateRunes().ToArray();
|
||||||
|
|
||||||
float sizeScale = (fontSize ?? referenceAtlas.FontSize) / (float) referenceAtlas.FontSize;
|
float sizeScale = (fontSize ?? referenceAtlas.FontSize) / (float) referenceAtlas.FontSize;
|
||||||
float baselineOffset = referenceAtlas.Ascender * sizeScale;
|
float baselineOffset = referenceAtlas.Ascender * sizeScale;
|
||||||
|
@ -112,28 +206,28 @@ public class Typeface : Font
|
||||||
|
|
||||||
var chars = text.AsSpan();
|
var chars = text.AsSpan();
|
||||||
|
|
||||||
atlas.RasterizeGlyphs(chars);
|
atlas.RasterizeGlyphs(runes);
|
||||||
|
|
||||||
var glyphInfo = default(GlyphInfo);
|
var glyphInfo = default(GlyphInfo);
|
||||||
|
|
||||||
var lastTexture = null as Texture2D;
|
var lastTexture = null as Texture2D;
|
||||||
var lastMesh = null as GuiMeshBuilder;
|
var lastMesh = null as GuiMeshBuilder;
|
||||||
|
|
||||||
for (var i = 0; i < chars.Length; i++)
|
for (var i = 0; i < runes.Length; i++)
|
||||||
{
|
{
|
||||||
char character = chars[i];
|
var character = runes[i];
|
||||||
|
|
||||||
// Newline characters
|
// Newline characters
|
||||||
if (character == '\r' || character == '\n')
|
if (character == carriageReturn || character == lineFeed)
|
||||||
{
|
{
|
||||||
// Check the next char after this one. If it's also a newline, but isn't the same kind as
|
// Check the next char after this one. If it's also a newline, but isn't the same kind as
|
||||||
// the current character, we treat both newlines as if they're one.
|
// the current character, we treat both newlines as if they're one.
|
||||||
//
|
//
|
||||||
// This will gracefully handle "\r\n" and "\n\r".
|
// This will gracefully handle "\r\n" and "\n\r".
|
||||||
if (i + 1 < chars.Length)
|
if (i + 1 < runes.Length)
|
||||||
{
|
{
|
||||||
char nextChar = chars[i + 1];
|
var nextChar = runes[i + 1];
|
||||||
if (nextChar != character && nextChar == '\r' || nextChar == '\n')
|
if (nextChar != character && nextChar == carriageReturn || nextChar == lineFeed)
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,11 +332,12 @@ public class Typeface : Font
|
||||||
private sealed unsafe class Atlas
|
private sealed unsafe class Atlas
|
||||||
{
|
{
|
||||||
private const int defaultAtlasSize = 1024;
|
private const int defaultAtlasSize = 1024;
|
||||||
|
private readonly Typeface typeface;
|
||||||
private readonly int fontSize;
|
private readonly int fontSize;
|
||||||
private readonly GraphicsDevice device;
|
private readonly GraphicsDevice device;
|
||||||
private readonly List<GlyphInfo> glyphInfo = new();
|
private readonly List<GlyphInfo> glyphInfo = new();
|
||||||
private readonly List<Texture2D> atlasTextures = new();
|
private readonly List<Texture2D> atlasTextures = new();
|
||||||
private readonly Dictionary<char, int> glyphMap = new();
|
private readonly Dictionary<Rune, int> glyphMap = new();
|
||||||
private readonly FT_FaceRec_* face;
|
private readonly FT_FaceRec_* face;
|
||||||
private readonly float ascender;
|
private readonly float ascender;
|
||||||
private readonly float descender;
|
private readonly float descender;
|
||||||
|
@ -259,8 +354,12 @@ public class Typeface : Font
|
||||||
public float UnderlinePosition => underlinePosition;
|
public float UnderlinePosition => underlinePosition;
|
||||||
public float LineHeight => lineHeight;
|
public float LineHeight => lineHeight;
|
||||||
|
|
||||||
public Atlas(GraphicsDevice device, int fontSize, FT_FaceRec_* face)
|
public Atlas(GraphicsDevice device, Typeface typeface, int fontSize, FT_FaceRec_* face)
|
||||||
{
|
{
|
||||||
|
this.typeface = typeface;
|
||||||
|
|
||||||
|
FT.FT_Select_Charmap(face, FT_Encoding_.FT_ENCODING_UNICODE);
|
||||||
|
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.fontSize = fontSize;
|
this.fontSize = fontSize;
|
||||||
this.face = face;
|
this.face = face;
|
||||||
|
@ -281,43 +380,60 @@ public class Typeface : Font
|
||||||
underlinePosition = ascender + (fontSize * 0.1f);
|
underlinePosition = ascender + (fontSize * 0.1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var flags = default(FT_LOAD);
|
||||||
if (fontSize <= 12)
|
if (fontSize <= 12)
|
||||||
{
|
{
|
||||||
// Disable anti-aliasing for low font sizes to make them less blurry.
|
// Disable anti-aliasing for low font sizes to make them less blurry.
|
||||||
loadFlags = FT_LOAD.FT_LOAD_MONOCHROME | FT_LOAD.FT_LOAD_FORCE_AUTOHINT;
|
flags = FT_LOAD.FT_LOAD_MONOCHROME | FT_LOAD.FT_LOAD_FORCE_AUTOHINT;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
loadFlags = FT_LOAD.FT_LOAD_FORCE_AUTOHINT;
|
flags = FT_LOAD.FT_LOAD_FORCE_AUTOHINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((face->face_flags & (uint)FT_FACE_FLAG.FT_FACE_FLAG_COLOR) != 0)
|
||||||
|
flags |= FT_LOAD.FT_LOAD_COLOR;
|
||||||
|
|
||||||
|
loadFlags = flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GetGlyphInfo(char character, ref GlyphInfo info)
|
public void GetGlyphInfo(Rune character, ref GlyphInfo info)
|
||||||
{
|
{
|
||||||
var index = glyphMap[character];
|
var index = glyphMap[character];
|
||||||
info = glyphInfo[index];
|
info = glyphInfo[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RasterizeGlyphs( ReadOnlySpan<char> characters)
|
public void RasterizeGlyphs( ReadOnlySpan<Rune> characters)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < characters.Length; i++)
|
for (var i = 0; i < characters.Length; i++)
|
||||||
{
|
{
|
||||||
char character = characters[i];
|
Rune character = characters[i];
|
||||||
|
|
||||||
if (glyphMap.TryGetValue(character, out int glyphIndex))
|
if (glyphMap.TryGetValue(character, out int glyphIndex))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
glyphIndex = glyphInfo.Count;
|
glyphIndex = glyphInfo.Count;
|
||||||
|
|
||||||
FT.FT_Load_Char(face, (nuint)character, FT_LOAD.FT_LOAD_RENDER | loadFlags);
|
FT.FT_Load_Char(face, (nuint) character.Value, FT_LOAD.FT_LOAD_RENDER | loadFlags);
|
||||||
|
|
||||||
uint rasterWidth = face->glyph->bitmap.width;
|
uint rasterWidth = face->glyph->bitmap.width;
|
||||||
uint rasterHeight = face->glyph->bitmap.rows;
|
uint rasterHeight = face->glyph->bitmap.rows;
|
||||||
|
var isSvg = (face->face_flags & (uint)FT_FACE_FLAG.FT_FACE_FLAG_SVG) != 0;
|
||||||
|
|
||||||
var isVisual = !(rasterWidth == 0 || rasterHeight == 0 || char.IsWhiteSpace(character));
|
var isVisual = isSvg || !(rasterWidth == 0 || rasterHeight == 0 || Rune.IsWhiteSpace(character));
|
||||||
|
|
||||||
Texture2D? texture = null;
|
Texture2D? texture = null;
|
||||||
|
|
||||||
|
byte[]? svgBitmapData = null;
|
||||||
|
|
||||||
|
if (isSvg)
|
||||||
|
{
|
||||||
|
var glyphIndexInFont = face->glyph->glyph_index;
|
||||||
|
var svgString = typeface.GetSvgDocument(glyphIndexInFont);
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Rendering SVG-based fonts is not supported yet. If you feel like tackling the immense performance problems involved in supporting it, then we're accepting merge requests. :)");
|
||||||
|
}
|
||||||
|
|
||||||
if (isVisual)
|
if (isVisual)
|
||||||
{
|
{
|
||||||
if (atlasTextures.Count > 0)
|
if (atlasTextures.Count > 0)
|
||||||
|
@ -350,18 +466,37 @@ public class Typeface : Font
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyGlyph(
|
if (isSvg)
|
||||||
texture,
|
{
|
||||||
face,
|
CopyGlyph(
|
||||||
rasterWidth,
|
texture,
|
||||||
rasterHeight,
|
face,
|
||||||
cursorX,
|
rasterWidth,
|
||||||
cursorY,
|
rasterHeight,
|
||||||
ref cursorLineHeight,
|
cursorX,
|
||||||
out GlyphInfo info
|
cursorY,
|
||||||
);
|
ref cursorLineHeight,
|
||||||
|
out GlyphInfo info,
|
||||||
|
svgBitmapData
|
||||||
|
);
|
||||||
|
|
||||||
this.glyphInfo.Add(info);
|
this.glyphInfo.Add(info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CopyGlyph(
|
||||||
|
texture,
|
||||||
|
face,
|
||||||
|
rasterWidth,
|
||||||
|
rasterHeight,
|
||||||
|
cursorX,
|
||||||
|
cursorY,
|
||||||
|
ref cursorLineHeight,
|
||||||
|
out GlyphInfo info
|
||||||
|
);
|
||||||
|
|
||||||
|
this.glyphInfo.Add(info);
|
||||||
|
}
|
||||||
|
|
||||||
cursorX += (int) rasterWidth;
|
cursorX += (int) rasterWidth;
|
||||||
glyphMap.Add(character, glyphIndex);
|
glyphMap.Add(character, glyphIndex);
|
||||||
|
@ -376,65 +511,94 @@ public class Typeface : Font
|
||||||
int atlasX,
|
int atlasX,
|
||||||
int atlasY,
|
int atlasY,
|
||||||
ref int lineHeight,
|
ref int lineHeight,
|
||||||
out GlyphInfo info
|
out GlyphInfo info,
|
||||||
|
byte[]? bitmapData = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
info = default;
|
info = default;
|
||||||
|
|
||||||
if (atlas != null)
|
if (atlas != null)
|
||||||
{
|
{
|
||||||
var isMonochrome = loadFlags.HasFlag(FT_LOAD.FT_LOAD_MONOCHROME);
|
var format = face->glyph->bitmap.pixel_mode;
|
||||||
var colors = new Color[(int)rasterWidth * (int)rasterHeight];
|
var colors = new Color[(int)rasterWidth * (int)rasterHeight];
|
||||||
|
|
||||||
if (isMonochrome)
|
switch (format)
|
||||||
{
|
{
|
||||||
// thank you, chatgpt, for generating this cursed shit
|
case FT_Pixel_Mode_.FT_PIXEL_MODE_GRAY:
|
||||||
for (var y = 0; y < rasterHeight; y++)
|
|
||||||
{
|
{
|
||||||
for (var x = 0; x < rasterWidth; x++)
|
for (var y = 0; y < rasterHeight; y++)
|
||||||
{
|
{
|
||||||
// Each byte in the buffer contains 8 pixels (1 bit per pixel)
|
for (var x = 0; x < rasterWidth; x++)
|
||||||
int byteIndex = y * face->glyph->bitmap.pitch + (x / 8);
|
|
||||||
byte bitMask = (byte)(0x80 >> (x % 8)); // Get the specific bit for this pixel
|
|
||||||
|
|
||||||
bool isPixelSet = (face->glyph->bitmap.buffer[byteIndex] & bitMask) != 0;
|
|
||||||
|
|
||||||
int colorIndex = y * (int)rasterWidth + x;
|
|
||||||
|
|
||||||
if (isPixelSet)
|
|
||||||
{
|
{
|
||||||
// Set the pixel to black (opaque)
|
int index = y * face->glyph->bitmap.pitch + x;
|
||||||
|
byte alpha = face->glyph->bitmap.buffer[index];
|
||||||
|
int colorIndex = y * (int)rasterWidth + x;
|
||||||
|
|
||||||
colors[colorIndex].R = 255;
|
colors[colorIndex].R = 255;
|
||||||
colors[colorIndex].G = 255;
|
colors[colorIndex].G = 255;
|
||||||
colors[colorIndex].B = 255;
|
colors[colorIndex].B = 255;
|
||||||
colors[colorIndex].A = 255;
|
colors[colorIndex].A = alpha;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Set the pixel to transparent (white background)
|
|
||||||
colors[colorIndex].R = 0;
|
|
||||||
colors[colorIndex].G = 0;
|
|
||||||
colors[colorIndex].B = 0;
|
|
||||||
colors[colorIndex].A = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
case FT_Pixel_Mode_.FT_PIXEL_MODE_MONO:
|
||||||
else
|
|
||||||
{
|
|
||||||
for (var y = 0; y < rasterHeight; y++)
|
|
||||||
{
|
{
|
||||||
for (var x = 0; x < rasterWidth; x++)
|
// thank you, chatgpt, for generating this cursed shit
|
||||||
|
for (var y = 0; y < rasterHeight; y++)
|
||||||
{
|
{
|
||||||
int index = y * face->glyph->bitmap.pitch + x;
|
for (var x = 0; x < rasterWidth; x++)
|
||||||
byte alpha = face->glyph->bitmap.buffer[index];
|
{
|
||||||
int colorIndex = y * (int)rasterWidth + x;
|
// Each byte in the buffer contains 8 pixels (1 bit per pixel)
|
||||||
|
int byteIndex = y * face->glyph->bitmap.pitch + (x / 8);
|
||||||
|
byte bitMask = (byte)(0x80 >> (x % 8)); // Get the specific bit for this pixel
|
||||||
|
|
||||||
colors[colorIndex].R = 255;
|
bool isPixelSet = (face->glyph->bitmap.buffer[byteIndex] & bitMask) != 0;
|
||||||
colors[colorIndex].G = 255;
|
|
||||||
colors[colorIndex].B = 255;
|
int colorIndex = y * (int)rasterWidth + x;
|
||||||
colors[colorIndex].A = alpha;
|
|
||||||
|
if (isPixelSet)
|
||||||
|
{
|
||||||
|
// Set the pixel to black (opaque)
|
||||||
|
colors[colorIndex].R = 255;
|
||||||
|
colors[colorIndex].G = 255;
|
||||||
|
colors[colorIndex].B = 255;
|
||||||
|
colors[colorIndex].A = 255;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set the pixel to transparent (white background)
|
||||||
|
colors[colorIndex].R = 0;
|
||||||
|
colors[colorIndex].G = 0;
|
||||||
|
colors[colorIndex].B = 0;
|
||||||
|
colors[colorIndex].A = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FT_Pixel_Mode_.FT_PIXEL_MODE_BGRA:
|
||||||
|
{
|
||||||
|
for (int y = 0; y < face->glyph->bitmap.rows; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < face->glyph->bitmap.width; x++)
|
||||||
|
{
|
||||||
|
int offset = (y * face->glyph->bitmap.pitch) + (x * 4); // 4 bytes per pixel
|
||||||
|
var colorIndex = y * rasterWidth + x;
|
||||||
|
|
||||||
|
colors[colorIndex].B = face->glyph->bitmap.buffer[offset + 0];
|
||||||
|
colors[colorIndex].G = face->glyph->bitmap.buffer[offset + 1];
|
||||||
|
colors[colorIndex].R = face->glyph->bitmap.buffer[offset + 2];
|
||||||
|
colors[colorIndex].A = face->glyph->bitmap.buffer[offset + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unsupported glyph format: " + format.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,10 +638,13 @@ public class Typeface : Font
|
||||||
info.VisualOffsetX = face->glyph->bitmap_left;
|
info.VisualOffsetX = face->glyph->bitmap_left;
|
||||||
info.VisualOffsetY = face->glyph->bitmap_top;
|
info.VisualOffsetY = face->glyph->bitmap_top;
|
||||||
|
|
||||||
|
if (info.VisualOffsetY == 0)
|
||||||
|
info.VisualOffsetY = (int) rasterHeight;
|
||||||
|
|
||||||
lineHeight = Math.Max(lineHeight, (int) rasterHeight);
|
lineHeight = Math.Max(lineHeight, (int) rasterHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed unsafe class AtlasContainer(GraphicsDevice device, Typeface typeface)
|
private sealed unsafe class AtlasContainer(GraphicsDevice device, Typeface typeface)
|
||||||
{
|
{
|
||||||
private readonly Typeface typeface = typeface;
|
private readonly Typeface typeface = typeface;
|
||||||
|
@ -488,7 +655,7 @@ public class Typeface : Font
|
||||||
{
|
{
|
||||||
if (!atlases.TryGetValue(fontSize, out var atlas))
|
if (!atlases.TryGetValue(fontSize, out var atlas))
|
||||||
{
|
{
|
||||||
atlas = new Atlas(device, fontSize, typeface.LoadFont());
|
atlas = new Atlas(device, typeface, fontSize, typeface.LoadFont());
|
||||||
atlases.Add(fontSize, atlas);
|
atlases.Add(fontSize, atlas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -496,6 +663,8 @@ public class Typeface : Font
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public struct GlyphInfo
|
public struct GlyphInfo
|
||||||
{
|
{
|
||||||
public bool IsVisual;
|
public bool IsVisual;
|
||||||
|
|
|
@ -9,6 +9,7 @@ namespace AcidicGUI.VisualStyles;
|
||||||
internal sealed class FallbackVisualStyle : IVisualStyle
|
internal sealed class FallbackVisualStyle : IVisualStyle
|
||||||
{
|
{
|
||||||
public Font? IconFont => null;
|
public Font? IconFont => null;
|
||||||
|
public Font? EmojiFont => null;
|
||||||
public Padding DropdownButtonPadding { get; } = 3;
|
public Padding DropdownButtonPadding { get; } = 3;
|
||||||
public Color SelectionColor => Color.LightBlue;
|
public Color SelectionColor => Color.LightBlue;
|
||||||
public Color TextSelectionBackground => Color.Blue;
|
public Color TextSelectionBackground => Color.Blue;
|
||||||
|
|
|
@ -29,6 +29,7 @@ public interface IVisualStyle :
|
||||||
IVisualRenderer<Popover.PopoverVisualState>
|
IVisualRenderer<Popover.PopoverVisualState>
|
||||||
{
|
{
|
||||||
Font? IconFont { get; }
|
Font? IconFont { get; }
|
||||||
|
Font? EmojiFont { get; }
|
||||||
Padding DropdownButtonPadding { get; }
|
Padding DropdownButtonPadding { get; }
|
||||||
Color SelectionColor { get; }
|
Color SelectionColor { get; }
|
||||||
Color TextSelectionBackground { get; }
|
Color TextSelectionBackground { get; }
|
||||||
|
|
|
@ -46,6 +46,12 @@ public static class StyleManager
|
||||||
activeStyle = newStyle;
|
activeStyle = newStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static Font? GetEmojiFont(Widget widget)
|
||||||
|
{
|
||||||
|
var style = widget.GetVisualStyleOverride() ?? activeStyle;
|
||||||
|
return style?.EmojiFont;
|
||||||
|
}
|
||||||
|
|
||||||
internal static IFontFamily GetFont(PresetFontFamily family, IVisualStyle? styleOverride)
|
internal static IFontFamily GetFont(PresetFontFamily family, IVisualStyle? styleOverride)
|
||||||
{
|
{
|
||||||
var style = styleOverride ?? activeStyle;
|
var style = styleOverride ?? activeStyle;
|
||||||
|
|
|
@ -21,7 +21,8 @@ public class TextWidget : Widget,
|
||||||
|
|
||||||
private static readonly Dictionary<string, Texture2D> images = new();
|
private static readonly Dictionary<string, Texture2D> images = new();
|
||||||
private static IImageLocator? _imageLocator;
|
private static IImageLocator? _imageLocator;
|
||||||
|
|
||||||
|
private bool parseEmoji = true;
|
||||||
private int previousWrapWidth;
|
private int previousWrapWidth;
|
||||||
private Color color = Color.White;
|
private Color color = Color.White;
|
||||||
private FontFamilyInfo fontFamily;
|
private FontFamilyInfo fontFamily;
|
||||||
|
@ -135,6 +136,20 @@ public class TextWidget : Widget,
|
||||||
InvalidateLayout();
|
InvalidateLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool AllowEmoji
|
||||||
|
{
|
||||||
|
get => parseEmoji;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (parseEmoji == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
parseEmoji = value;
|
||||||
|
RebuildText();
|
||||||
|
InvalidateLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool UseMarkup
|
public bool UseMarkup
|
||||||
{
|
{
|
||||||
|
@ -345,8 +360,28 @@ public class TextWidget : Widget,
|
||||||
geometry.AddQuad(highlightRect, highlight);
|
geometry.AddQuad(highlightRect, highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
family.Draw(geometry, element.Position.ToVector2(), renderColor, element.Text, element.MarkupData.FontSize ?? FontSize,
|
if (element.IsEmoji && element.EmojiFont != null)
|
||||||
element.MarkupData.Weight ?? FontWeight, element.MarkupData.Italic);
|
{
|
||||||
|
element.EmojiFont.Draw(
|
||||||
|
geometry,
|
||||||
|
element.Position.ToVector2(),
|
||||||
|
renderColor,
|
||||||
|
element.Text,
|
||||||
|
element.MarkupData.FontSize ?? fontSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
family.Draw(
|
||||||
|
geometry,
|
||||||
|
element.Position.ToVector2(),
|
||||||
|
renderColor,
|
||||||
|
element.Text,
|
||||||
|
element.MarkupData.FontSize ?? FontSize,
|
||||||
|
element.MarkupData.Weight ?? FontWeight,
|
||||||
|
element.MarkupData.Italic
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var underlineOffset = family.GetUnderlineOffset(element.MarkupData.FontSize ?? FontSize, element.MarkupData.Weight ?? FontWeight, element.MarkupData.Italic);
|
var underlineOffset = family.GetUnderlineOffset(element.MarkupData.FontSize ?? FontSize, element.MarkupData.Weight ?? FontWeight, element.MarkupData.Italic);
|
||||||
var strikeLine = 1;
|
var strikeLine = 1;
|
||||||
|
@ -396,11 +431,27 @@ public class TextWidget : Widget,
|
||||||
{
|
{
|
||||||
if (!textElements[i].MarkupData.IsImage)
|
if (!textElements[i].MarkupData.IsImage)
|
||||||
{
|
{
|
||||||
var family = (textElements[i].MarkupData.FontOverride ?? fontFamily).GetFont(this);
|
if (textElements[i].IsEmoji && textElements[i].EmojiFont != null)
|
||||||
var newMeasurement = family.Measure(textElements[i].Text.TrimEnd(), textElements[i].MarkupData.FontSize ?? FontSize, textElements[i].MarkupData.Weight ?? FontWeight, textElements[i].MarkupData.Italic);
|
{
|
||||||
|
var emojiFont = textElements[i].EmojiFont;
|
||||||
|
var newMeasurement = emojiFont.Measure(textElements[i].Text.TrimEnd(), textElements[i].MarkupData.FontSize ?? FontSize);
|
||||||
|
|
||||||
newMeasurement.Y = family.GetLineHeight(textElements[i].MarkupData.FontSize ?? FontSize, textElements[i].MarkupData.Weight ?? FontWeight, textElements[i].MarkupData.Italic);
|
newMeasurement.Y = emojiFont.GetLineHeight(textElements[i].MarkupData.FontSize ?? FontSize);
|
||||||
textElements[i].MeasuredSize = newMeasurement;
|
textElements[i].MeasuredSize = newMeasurement;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var family = (textElements[i].MarkupData.FontOverride ?? fontFamily).GetFont(this);
|
||||||
|
var newMeasurement = family.Measure(
|
||||||
|
textElements[i].Text.TrimEnd(),
|
||||||
|
textElements[i].MarkupData.FontSize ?? FontSize,
|
||||||
|
textElements[i].MarkupData.Weight ?? FontWeight,
|
||||||
|
textElements[i].MarkupData.Italic
|
||||||
|
);
|
||||||
|
|
||||||
|
newMeasurement.Y = family.GetLineHeight(textElements[i].MarkupData.FontSize ?? FontSize, textElements[i].MarkupData.Weight ?? FontWeight, textElements[i].MarkupData.Italic);
|
||||||
|
textElements[i].MeasuredSize = newMeasurement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,14 +468,33 @@ public class TextWidget : Widget,
|
||||||
{
|
{
|
||||||
offset.X -= textElements[i - 1].MeasuredSize!.Value.X;
|
offset.X -= textElements[i - 1].MeasuredSize!.Value.X;
|
||||||
|
|
||||||
var newFamily = (textElements[i - 1].MarkupData.FontOverride ?? fontFamily).GetFont(this);
|
if (textElements[i - 1].IsEmoji && textElements[i - 1].EmojiFont != null)
|
||||||
|
{
|
||||||
|
var newEmojiFont = textElements[i - 1].EmojiFont;
|
||||||
|
|
||||||
var newMeasurement = newFamily.Measure(textElements[i - 1].Text.TrimEnd(), textElements[i - 1].MarkupData.FontSize ?? FontSize, textElements[i - 1].MarkupData.Weight ?? FontWeight, textElements[i - 1].MarkupData.Italic);
|
var newMeasurement = newEmojiFont!.Measure(textElements[i - 1].Text.TrimEnd(), textElements[i - 1].MarkupData.FontSize ?? FontSize);
|
||||||
|
|
||||||
newMeasurement.Y = newFamily.GetLineHeight(textElements[i - 1].MarkupData.FontSize ?? FontSize, textElements[i - 1].MarkupData.Weight ?? FontWeight, textElements[i - 1].MarkupData.Italic);
|
newMeasurement.Y = newEmojiFont.GetLineHeight(textElements[i - 1].MarkupData.FontSize ?? FontSize);
|
||||||
textElements[i - 1].MeasuredSize = newMeasurement;
|
textElements[i - 1].MeasuredSize = newMeasurement;
|
||||||
|
|
||||||
|
offset.X += newMeasurement.X;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newFamily = (textElements[i - 1].MarkupData.FontOverride ?? fontFamily).GetFont(this);
|
||||||
|
|
||||||
offset.X += newMeasurement.X;
|
var newMeasurement = newFamily.Measure(
|
||||||
|
textElements[i - 1].Text.TrimEnd(),
|
||||||
|
textElements[i - 1].MarkupData.FontSize ?? FontSize,
|
||||||
|
textElements[i - 1].MarkupData.Weight ?? FontWeight,
|
||||||
|
textElements[i - 1].MarkupData.Italic
|
||||||
|
);
|
||||||
|
|
||||||
|
newMeasurement.Y = newFamily.GetLineHeight(textElements[i - 1].MarkupData.FontSize ?? FontSize, textElements[i - 1].MarkupData.Weight ?? FontWeight, textElements[i - 1].MarkupData.Italic);
|
||||||
|
textElements[i - 1].MeasuredSize = newMeasurement;
|
||||||
|
|
||||||
|
offset.X += newMeasurement.X;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,11 +724,34 @@ public class TextWidget : Widget,
|
||||||
images.Add(path, image);
|
images.Add(path, image);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ParseEmoji(ReadOnlySpan<char> characters, int startIndex, out int rawLength, out string? codepoint)
|
||||||
|
{
|
||||||
|
codepoint = default;
|
||||||
|
rawLength = 0;
|
||||||
|
|
||||||
|
var afterColon = characters.Slice(startIndex + 1);
|
||||||
|
var nextColon = afterColon.IndexOf(':');
|
||||||
|
|
||||||
|
if (nextColon == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var identifier = afterColon.Slice(0, nextColon).ToString();
|
||||||
|
|
||||||
|
if (!Emojis.All.TryGetValue(identifier, out uint codepointValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
codepoint = char.ConvertFromUtf32((int)codepointValue);
|
||||||
|
rawLength = 2 + identifier.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void RebuildText()
|
private void RebuildText()
|
||||||
{
|
{
|
||||||
var markupData = new MarkupData();
|
var markupData = new MarkupData();
|
||||||
var newMarkupData = new MarkupData();
|
var newMarkupData = new MarkupData();
|
||||||
|
var emojiFont = StyleManager.GetEmojiFont(this);
|
||||||
|
var emojiSupported = parseEmoji && emojiFont != null;
|
||||||
|
|
||||||
var sourceStart = 0;
|
var sourceStart = 0;
|
||||||
|
|
||||||
|
@ -707,9 +800,56 @@ public class TextWidget : Widget,
|
||||||
|
|
||||||
switch (character.Value)
|
switch (character.Value)
|
||||||
{
|
{
|
||||||
case '<' when useMarkup:
|
case { } when character.Value >= 0xd800 && character.Value <= 0xdbff:
|
||||||
{
|
{
|
||||||
if (!ParseMarkup(chars, i, ref newMarkupData))
|
if (i + 1 >= chars.Length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
char nextChar = chars[i + 1];
|
||||||
|
if (nextChar < 0xdc00 || nextChar > 0xdfff)
|
||||||
|
goto default;
|
||||||
|
|
||||||
|
Rune surrogatePair = new Rune(character.Value, nextChar);
|
||||||
|
|
||||||
|
if (!emojiSupported)
|
||||||
|
goto default;
|
||||||
|
|
||||||
|
if (!Emojis.Codepoints.Contains((uint)surrogatePair.Value))
|
||||||
|
goto default;
|
||||||
|
|
||||||
|
textElements.Add(new TextElement
|
||||||
|
{
|
||||||
|
Text = stringBuilder.ToString(),
|
||||||
|
SourceStart = sourceStart,
|
||||||
|
SourceEnd = i,
|
||||||
|
MarkupData = markupData
|
||||||
|
});
|
||||||
|
|
||||||
|
stringBuilder.Length = 0;
|
||||||
|
sourceStart = i;
|
||||||
|
|
||||||
|
stringBuilder.Append($"{character.Value}{nextChar}");
|
||||||
|
i += 2;
|
||||||
|
|
||||||
|
textElements.Add(new TextElement
|
||||||
|
{
|
||||||
|
Text = stringBuilder.ToString(),
|
||||||
|
IsEmoji = true,
|
||||||
|
EmojiFont = emojiFont,
|
||||||
|
SourceStart = sourceStart,
|
||||||
|
SourceEnd = i,
|
||||||
|
MarkupData = markupData
|
||||||
|
});
|
||||||
|
|
||||||
|
stringBuilder.Length = 0;
|
||||||
|
sourceStart = i;
|
||||||
|
i--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ':' when emojiSupported && useMarkup:
|
||||||
|
{
|
||||||
|
if (!ParseEmoji(chars, i, out int rawLength, out string? codepoint))
|
||||||
goto default;
|
goto default;
|
||||||
|
|
||||||
textElements.Add(new TextElement
|
textElements.Add(new TextElement
|
||||||
|
@ -719,6 +859,47 @@ public class TextWidget : Widget,
|
||||||
SourceEnd = i,
|
SourceEnd = i,
|
||||||
MarkupData = markupData
|
MarkupData = markupData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stringBuilder.Length = 0;
|
||||||
|
sourceStart = i;
|
||||||
|
|
||||||
|
stringBuilder.Append(codepoint);
|
||||||
|
i += rawLength;
|
||||||
|
|
||||||
|
textElements.Add(new TextElement
|
||||||
|
{
|
||||||
|
Text = stringBuilder.ToString(),
|
||||||
|
IsEmoji = true,
|
||||||
|
EmojiFont = emojiFont,
|
||||||
|
SourceStart = sourceStart,
|
||||||
|
SourceEnd = i,
|
||||||
|
MarkupData = markupData
|
||||||
|
});
|
||||||
|
|
||||||
|
stringBuilder.Length = 0;
|
||||||
|
sourceStart = i;
|
||||||
|
i--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '<' when useMarkup:
|
||||||
|
{
|
||||||
|
if (!ParseMarkup(chars, i, ref newMarkupData))
|
||||||
|
goto default;
|
||||||
|
|
||||||
|
if (stringBuilder.Length > 0)
|
||||||
|
{
|
||||||
|
textElements.Add(
|
||||||
|
new TextElement
|
||||||
|
{
|
||||||
|
Text = stringBuilder.ToString(),
|
||||||
|
SourceStart = sourceStart,
|
||||||
|
SourceEnd = i,
|
||||||
|
MarkupData = markupData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
stringBuilder.Length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
int markupLength = newMarkupData.Length;
|
int markupLength = newMarkupData.Length;
|
||||||
|
|
||||||
|
@ -737,9 +918,6 @@ public class TextWidget : Widget,
|
||||||
|
|
||||||
markupData = newMarkupData;
|
markupData = newMarkupData;
|
||||||
|
|
||||||
stringBuilder.Length = 0;
|
|
||||||
sourceStart = i;
|
|
||||||
|
|
||||||
i += markupLength - 1;
|
i += markupLength - 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -956,6 +1134,12 @@ public class TextWidget : Widget,
|
||||||
if (textElements[i].MeasuredSize != null)
|
if (textElements[i].MeasuredSize != null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (textElements[i].IsEmoji && textElements[i].EmojiFont != null)
|
||||||
|
{
|
||||||
|
textElements[i].MeasuredSize = textElements[i].EmojiFont!.Measure(textElements[i].Text, textElements[i].MarkupData.FontSize ?? fontSize);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (family == null || lastOverride != textElements[i].MarkupData.FontOverride)
|
if (family == null || lastOverride != textElements[i].MarkupData.FontOverride)
|
||||||
{
|
{
|
||||||
family = (textElements[i].MarkupData.FontOverride ?? fontFamily).GetFont(this);
|
family = (textElements[i].MarkupData.FontOverride ?? fontFamily).GetFont(this);
|
||||||
|
@ -1068,6 +1252,8 @@ public class TextWidget : Widget,
|
||||||
private class TextElement
|
private class TextElement
|
||||||
{
|
{
|
||||||
public string Text = string.Empty;
|
public string Text = string.Empty;
|
||||||
|
public bool IsEmoji;
|
||||||
|
public Font? EmojiFont;
|
||||||
public Point Position;
|
public Point Position;
|
||||||
public Point? MeasuredSize;
|
public Point? MeasuredSize;
|
||||||
public bool IsNewLine;
|
public bool IsNewLine;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Net.Mime;
|
||||||
using Microsoft.DotNet.PlatformAbstractions;
|
using Microsoft.DotNet.PlatformAbstractions;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using SociallyDistant.Core.Core.Config;
|
using SociallyDistant.Core.Core.Config;
|
||||||
|
using SociallyDistant.Core.Modules;
|
||||||
|
|
||||||
namespace SociallyDistant.Core.Config.SystemConfigCategories
|
namespace SociallyDistant.Core.Config.SystemConfigCategories
|
||||||
{
|
{
|
||||||
|
@ -144,8 +146,14 @@ namespace SociallyDistant.Core.Config.SystemConfigCategories
|
||||||
"Wayland Support",
|
"Wayland Support",
|
||||||
"Change whether Socially Distant uses Wayland or X11 to communicate with your desktop environment. Disabling Wayland support may work around bugs on certain desktops. Changing this setting requires a game restart.",
|
"Change whether Socially Distant uses Wayland or X11 to communicate with your desktop environment. Disabling Wayland support may work around bugs on certain desktops. Changing this setting requires a game restart.",
|
||||||
UseWaylandBackend,
|
UseWaylandBackend,
|
||||||
x => UseWaylandBackend = x,
|
x =>
|
||||||
linux
|
{
|
||||||
|
UseWaylandBackend = x;
|
||||||
|
Application.Instance.Restart();
|
||||||
|
},
|
||||||
|
linux,
|
||||||
|
true,
|
||||||
|
"Changing this setting requires the game to restart. Are you sure you want to continue? Any unsaved progress will be lost."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,9 @@ public class SociallyDistantVisualStyle : IVisualStyle,
|
||||||
private readonly ChatMessageStyle chatMessageStyle;
|
private readonly ChatMessageStyle chatMessageStyle;
|
||||||
private readonly Color mainBackground = new Color(0x11, 0x13, 0x15);
|
private readonly Color mainBackground = new Color(0x11, 0x13, 0x15);
|
||||||
private readonly CompletionListSTyle completionListSTyle;
|
private readonly CompletionListSTyle completionListSTyle;
|
||||||
private readonly PopoverStyle popoverStyle;
|
private readonly PopoverStyle popoverStyle;
|
||||||
|
private Font? emojiFont;
|
||||||
|
|
||||||
|
|
||||||
private readonly Color statusBarColor = new Color(
|
private readonly Color statusBarColor = new Color(
|
||||||
0x01,
|
0x01,
|
||||||
|
@ -125,6 +127,7 @@ public class SociallyDistantVisualStyle : IVisualStyle,
|
||||||
private Texture2D? checkboxEmblem;
|
private Texture2D? checkboxEmblem;
|
||||||
|
|
||||||
public Font? IconFont => iconFont;
|
public Font? IconFont => iconFont;
|
||||||
|
public Font? EmojiFont => emojiFont;
|
||||||
|
|
||||||
public Padding DropdownButtonPadding { get; } = new Padding(
|
public Padding DropdownButtonPadding { get; } = new Padding(
|
||||||
1,
|
1,
|
||||||
|
@ -173,6 +176,7 @@ public class SociallyDistantVisualStyle : IVisualStyle,
|
||||||
checkboxEmblem = game.GameInstance.Content.Load<Texture2D>("/Core/UI/Textures/checkbox_emblem");
|
checkboxEmblem = game.GameInstance.Content.Load<Texture2D>("/Core/UI/Textures/checkbox_emblem");
|
||||||
|
|
||||||
iconFont = Typeface.FromStream(graphicsDevice, game.GameInstance.Content.Load<Stream>("/Core/UI/lucide.ttf"), 16);
|
iconFont = Typeface.FromStream(graphicsDevice, game.GameInstance.Content.Load<Stream>("/Core/UI/lucide.ttf"), 16);
|
||||||
|
emojiFont = Typeface.FromStream(graphicsDevice, game.GameInstance.Content.Load<Stream>("/Core/UI/Fonts/Emoji/Twemoji.Mozilla.ttf"), 16);
|
||||||
|
|
||||||
defaultFont = LoadFont("/Core/UI/Fonts/Rajdhani", graphicsDevice);
|
defaultFont = LoadFont("/Core/UI/Fonts/Rajdhani", graphicsDevice);
|
||||||
monospace = LoadFont("/Core/UI/Fonts/Monospace/JetBrainsMono", graphicsDevice);
|
monospace = LoadFont("/Core/UI/Fonts/Monospace/JetBrainsMono", graphicsDevice);
|
||||||
|
|
|
@ -8,9 +8,11 @@ metadata() {
|
||||||
}
|
}
|
||||||
|
|
||||||
email() {
|
email() {
|
||||||
echo Hello world.
|
echo ":earth_americas:" Hello world.
|
||||||
echo This is a test.
|
echo This is a test.
|
||||||
echo If you are reading this, "<b>this text should be bold.</b>"
|
echo "💖 this is a test of raw unicode emoji parsing, if you saw the heart then it worked."
|
||||||
|
echo If you are :book: reading this, "<b>this text should be bold.</b>"
|
||||||
|
echo :eyes: Emoji are supported as well.
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|
93
src/SociallyDistant/Content/UI/Fonts/Emoji/OFL.txt
Normal file
93
src/SociallyDistant/Content/UI/Fonts/Emoji/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2021 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
src/SociallyDistant/Content/UI/Fonts/Emoji/Twemoji.Mozilla.ttf
Normal file
BIN
src/SociallyDistant/Content/UI/Fonts/Emoji/Twemoji.Mozilla.ttf
Normal file
Binary file not shown.
|
@ -1,4 +1,6 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using SociallyDistant.Core.Config;
|
using SociallyDistant.Core.Config;
|
||||||
using SociallyDistant.Core.Config.SystemConfigCategories;
|
using SociallyDistant.Core.Config.SystemConfigCategories;
|
||||||
|
@ -23,15 +25,20 @@ internal sealed class GameApplication : Application
|
||||||
settingsManager.Load();
|
settingsManager.Load();
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable("SDL_LOG_PRIORITY", "debug");
|
Environment.SetEnvironmentVariable("SDL_LOG_PRIORITY", "debug");
|
||||||
|
|
||||||
var waylandDisplayIsPresent = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
|
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||||
|
var waylandDisplayIsPresent = isLinux && !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
|
||||||
|
|
||||||
var graphicsSettings = new GraphicsSettings(settingsManager);
|
var graphicsSettings = new GraphicsSettings(settingsManager);
|
||||||
|
|
||||||
if (waylandDisplayIsPresent && graphicsSettings.UseWaylandBackend)
|
if (waylandDisplayIsPresent && graphicsSettings.UseWaylandBackend)
|
||||||
{
|
{
|
||||||
Log.Information("Running SDL2 in Wayland mode.");
|
Log.Information("Running SDL2 in Wayland mode.");
|
||||||
Environment.SetEnvironmentVariable("SDL_VIDEODRIVER", "wayland");
|
SdlPlatformSettings.PreferredVideoDriver = "wayland";
|
||||||
|
}
|
||||||
|
else if (isLinux)
|
||||||
|
{
|
||||||
|
SdlPlatformSettings.PreferredVideoDriver = "xcb"; // fallback to X11.
|
||||||
}
|
}
|
||||||
|
|
||||||
game = new SociallyDistantGame(this);
|
game = new SociallyDistantGame(this);
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<PackageReference Include="AutoPipeline" Version="0.0.2" />
|
<PackageReference Include="AutoPipeline" Version="0.0.2" />
|
||||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
|
||||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="1.0.0.456" />
|
<PackageReference Include="MonoGame.Content.Builder.Task" Version="1.0.0.568" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -99,6 +99,12 @@
|
||||||
<None Update="Content\UI\lucide.ttf">
|
<None Update="Content\UI\lucide.ttf">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Content\UI\Fonts\Emoji\OFL.txt">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Content\UI\Fonts\Emoji\Twemoji.Mozilla.ttf">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ImGui.NET" Version="1.90.1.1" />
|
<PackageReference Include="ImGui.NET" Version="1.90.1.1" />
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="1.0.0.456" />
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="1.0.0.568" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
|
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
|
||||||
|
|
Loading…
Reference in a new issue