mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
System font API (#5393)
* System font API This is a new API that allows operating system fonts to be loaded by the engine and used by content. Fonts are provided in a flat list exposing all the relevant metadata. They are loaded from disk with a Load call. Initial implementation is only for Windows DirectWrite. * Load system fonts as memory mapped files if possible. This allows sharing the font file memory with other processes which is always good. * Use ArrayPool to reduce char array allocations * Disable verbose logging * Implement system font support on Linux via Fontconfig * Implement macOS support * Add "FREEDESKTOP" define constant This is basically LINUX || FREEBSD. Though FreeBSD currently gets detected as LINUX too. Oh well. * Compile out Fontconfig and CoreText system font backends when not on those platforms * Don't add Fontconfig package dep on Mac/Windows * Allow disabling system font support via CVar Cuz why not.
This commit is contained in:
committed by
GitHub
parent
8b7fbfa646
commit
f3a3f564e1
@@ -62,6 +62,7 @@
|
||||
<PackageVersion Include="SpaceWizards.Sdl" Version="1.0.0" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.1.0" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="SpaceWizards.Fontconfig.Interop" Version="1.0.0" />
|
||||
<PackageVersion Include="libsodium" Version="1.0.20.1" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.8" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>$(DefineConstants);LINUX;UNIX</DefineConstants>
|
||||
<DefineConstants>$(DefineConstants);LINUX;UNIX;FREEDESKTOP</DefineConstants>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
@@ -31,5 +31,6 @@
|
||||
<Python>python3</Python>
|
||||
<Python Condition="'$(ActualOS)' == 'Windows'">py -3</Python>
|
||||
<UseSystemSqlite Condition="'$(TargetOS)' == 'FreeBSD'">True</UseSystemSqlite>
|
||||
<IsFreedesktop Condition="'$(TargetOS)' == 'FreeBSD' Or '$(TargetOS)' == 'Linux'">True</IsFreedesktop>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -47,6 +47,9 @@ END TEMPLATE-->
|
||||
* Sandbox:
|
||||
* Exposed `System.Reflection.Metadata.MetadataUpdateHandlerAttribute`.
|
||||
* Exposed more overloads on `StringBuilder`.
|
||||
* The engine can now load system fonts.
|
||||
* At the moment only available on Windows.
|
||||
* See `ISystemFontManager` for API.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.Graphics.FontManagement;
|
||||
using Robust.Client.HWId;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Localization;
|
||||
@@ -121,6 +122,8 @@ namespace Robust.Client
|
||||
deps.Register<IInputManager, InputManager>();
|
||||
deps.Register<IFileDialogManager, DummyFileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpenerDummy>();
|
||||
deps.Register<ISystemFontManager, SystemFontManagerFallback>();
|
||||
deps.Register<ISystemFontManagerInternal, SystemFontManagerFallback>();
|
||||
break;
|
||||
case GameController.DisplayMode.Clyde:
|
||||
deps.Register<IClyde, Clyde>();
|
||||
@@ -131,6 +134,8 @@ namespace Robust.Client
|
||||
deps.Register<IInputManager, ClydeInputManager>();
|
||||
deps.Register<IFileDialogManager, FileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpener>();
|
||||
deps.Register<ISystemFontManager, SystemFontManager>();
|
||||
deps.Register<ISystemFontManagerInternal, SystemFontManager>();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -143,6 +144,7 @@ namespace Robust.Client
|
||||
_taskManager.Initialize();
|
||||
_parallelMgr.Initialize();
|
||||
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
_systemFontManager.Initialize();
|
||||
|
||||
// Load optional Robust modules.
|
||||
LoadOptionalRobustModules(displayMode, _resourceManifest!);
|
||||
|
||||
@@ -104,6 +104,12 @@ namespace Robust.Client.Graphics
|
||||
Handle = IoCManager.Resolve<IFontManagerInternal>().MakeInstance(res.FontFaceHandle, size);
|
||||
}
|
||||
|
||||
internal VectorFont(IFontInstanceHandle handle, int size)
|
||||
{
|
||||
Size = size;
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
public override int GetAscent(float scale) => Handle.GetAscent(scale);
|
||||
public override int GetHeight(float scale) => Handle.GetHeight(scale);
|
||||
public override int GetDescent(float scale) => Handle.GetDescent(scale);
|
||||
@@ -222,4 +228,74 @@ namespace Robust.Client.Graphics
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible values for font weights. Larger values have thicker font strokes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These values are based on the <c>usWeightClass</c> property of the OpenType specification:
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISystemFontFace.Weight"/>
|
||||
public enum FontWeight : ushort
|
||||
{
|
||||
Thin = 100,
|
||||
ExtraLight = 200,
|
||||
UltraLight = ExtraLight,
|
||||
Light = 300,
|
||||
SemiLight = 350,
|
||||
Normal = 400,
|
||||
Regular = Normal,
|
||||
Medium = 500,
|
||||
SemiBold = 600,
|
||||
DemiBold = SemiBold,
|
||||
Bold = 700,
|
||||
ExtraBold = 800,
|
||||
UltraBold = ExtraBold,
|
||||
Black = 900,
|
||||
Heavy = Black,
|
||||
ExtraBlack = 950,
|
||||
UltraBlack = ExtraBlack,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible slant values for fonts.
|
||||
/// </summary>
|
||||
/// <seealso cref="ISystemFontFace.Slant"/>
|
||||
public enum FontSlant : byte
|
||||
{
|
||||
// NOTE: Enum values correspond to DWRITE_FONT_STYLE.
|
||||
Normal = 0,
|
||||
Oblique = 1,
|
||||
|
||||
// FUN FACT: they're called "italics" because they look like the Leaning Tower of Pisa.
|
||||
// Don't fact-check that.
|
||||
Italic = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible values for font widths. Larger values are proportionally wider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These values are based on the <c>usWidthClass</c> property of the OpenType specification:
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISystemFontFace.Width"/>
|
||||
public enum FontWidth : ushort
|
||||
{
|
||||
UltraCondensed = 1,
|
||||
ExtraCondensed = 2,
|
||||
Condensed = 3,
|
||||
SemiCondensed = 4,
|
||||
Normal = 5,
|
||||
Medium = Normal,
|
||||
SemiExpanded = 6,
|
||||
Expanded = 7,
|
||||
ExtraExpanded = 8,
|
||||
UltraExpanded = 9,
|
||||
}
|
||||
}
|
||||
|
||||
15
Robust.Client/Graphics/FontManagement/SystemFontDebug.cs
Normal file
15
Robust.Client/Graphics/FontManagement/SystemFontDebug.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
internal sealed class SystemFontDebugCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "system_font_debug";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
new SystemFontDebugWindow().OpenCentered();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="System font debug">
|
||||
<SplitContainer Orientation="Horizontal" MinSize="800 600">
|
||||
<ScrollContainer HScrollEnabled="False">
|
||||
<BoxContainer Name="SelectorContainer" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
<ScrollContainer HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="FamilyLabel" />
|
||||
<BoxContainer Orientation="Vertical" Name="FaceContainer" />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</SplitContainer>
|
||||
</DefaultWindow>
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
internal sealed partial class SystemFontDebugWindow : DefaultWindow
|
||||
{
|
||||
private static readonly int[] ExampleFontSizes = [8, 12, 16, 24, 36];
|
||||
private const string ExampleString = "The quick brown fox jumps over the lazy dog";
|
||||
|
||||
[Dependency] private readonly ISystemFontManager _systemFontManager = default!;
|
||||
|
||||
public SystemFontDebugWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var buttonGroup = new ButtonGroup();
|
||||
|
||||
foreach (var group in _systemFontManager.SystemFontFaces.GroupBy(k => k.FamilyName).OrderBy(k => k.Key))
|
||||
{
|
||||
var fonts = group.ToArray();
|
||||
SelectorContainer.AddChild(new Selector(this, buttonGroup, group.Key, fonts));
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectFontFamily(ISystemFontFace[] fonts)
|
||||
{
|
||||
FamilyLabel.Text = fonts[0].FamilyName;
|
||||
|
||||
FaceContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var font in fonts)
|
||||
{
|
||||
var exampleContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(8)
|
||||
};
|
||||
|
||||
foreach (var size in ExampleFontSizes)
|
||||
{
|
||||
var fontInstance = font.Load(size);
|
||||
|
||||
var richTextLabel = new RichTextLabel
|
||||
{
|
||||
Stylesheet = new Stylesheet([
|
||||
StylesheetHelpers.Element<RichTextLabel>().Prop("font", fontInstance)
|
||||
]),
|
||||
};
|
||||
richTextLabel.SetMessage(FormattedMessage.FromUnformatted(ExampleString));
|
||||
exampleContainer.AddChild(richTextLabel);
|
||||
}
|
||||
|
||||
FaceContainer.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
new RichTextLabel
|
||||
{
|
||||
Text = $"""
|
||||
{font.FullName}
|
||||
Family: "{font.FamilyName}", face: "{font.FaceName}", PostScript = "{font.PostscriptName}"
|
||||
Weight: {font.Weight} ({(int) font.Weight}), slant: {font.Slant} ({(int) font.Slant}), width: {font.Width} ({(int) font.Width})
|
||||
""",
|
||||
},
|
||||
exampleContainer
|
||||
},
|
||||
Margin = new Thickness(0, 0, 0, 8)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Selector : Control
|
||||
{
|
||||
public Selector(SystemFontDebugWindow window, ButtonGroup group, string family, ISystemFontFace[] fonts)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Text = family,
|
||||
Group = group,
|
||||
ToggleMode = true
|
||||
};
|
||||
AddChild(button);
|
||||
|
||||
button.OnPressed += _ => window.SelectFontFamily(fonts);
|
||||
}
|
||||
}
|
||||
}
|
||||
170
Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs
Normal file
170
Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
internal abstract class SystemFontManagerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The "standard" locale used when looking up the PostScript name of a font face.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Font files allow the PostScript name to be localized, however in practice
|
||||
/// we would really like to have a language-unambiguous identifier to refer to a font file.
|
||||
/// We use this locale (en-US) to look up teh PostScript font name, if there are multiple provided.
|
||||
/// This matches the behavior of the Local Font Access web API:
|
||||
/// https://wicg.github.io/local-font-access/#concept-font-representation
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected static readonly CultureInfo StandardLocale = new("en-US", false);
|
||||
|
||||
protected readonly IFontManagerInternal FontManager;
|
||||
protected readonly ISawmill Sawmill;
|
||||
|
||||
protected readonly Lock Lock = new();
|
||||
protected readonly List<BaseHandle> Fonts = [];
|
||||
|
||||
public IEnumerable<ISystemFontFace> SystemFontFaces { get; }
|
||||
|
||||
public SystemFontManagerBase(ILogManager logManager, IFontManagerInternal fontManager)
|
||||
{
|
||||
FontManager = fontManager;
|
||||
Sawmill = logManager.GetSawmill("font.system");
|
||||
|
||||
SystemFontFaces = Fonts.AsReadOnly();
|
||||
}
|
||||
|
||||
protected abstract IFontFaceHandle LoadFontFace(BaseHandle handle);
|
||||
|
||||
protected static string GetLocalizedForLocaleOrFirst(LocalizedStringSet set, CultureInfo culture)
|
||||
{
|
||||
var matchCulture = culture;
|
||||
while (!Equals(matchCulture, CultureInfo.InvariantCulture))
|
||||
{
|
||||
if (set.Values.TryGetValue(culture.Name, out var value))
|
||||
return value;
|
||||
|
||||
matchCulture = matchCulture.Parent;
|
||||
}
|
||||
|
||||
return set.Values[set.Primary];
|
||||
}
|
||||
|
||||
protected abstract class BaseHandle(SystemFontManagerBase parent) : ISystemFontFace
|
||||
{
|
||||
private IFontFaceHandle? _cachedFont;
|
||||
|
||||
public required string PostscriptName { get; init; }
|
||||
|
||||
public required LocalizedStringSet FullNames;
|
||||
public required LocalizedStringSet FamilyNames;
|
||||
public required LocalizedStringSet FaceNames;
|
||||
|
||||
public required FontWeight Weight { get; init; }
|
||||
public required FontSlant Slant { get; init; }
|
||||
public required FontWidth Width { get; init; }
|
||||
|
||||
public string FullName => GetLocalizedFullName(CultureInfo.CurrentCulture);
|
||||
public string FamilyName => GetLocalizedFamilyName(CultureInfo.CurrentCulture);
|
||||
public string FaceName => GetLocalizedFaceName(CultureInfo.CurrentCulture);
|
||||
|
||||
public string GetLocalizedFullName(CultureInfo culture)
|
||||
{
|
||||
return GetLocalizedForLocaleOrFirst(FullNames, culture);
|
||||
}
|
||||
|
||||
public string GetLocalizedFamilyName(CultureInfo culture)
|
||||
{
|
||||
return GetLocalizedForLocaleOrFirst(FamilyNames, culture);
|
||||
}
|
||||
|
||||
public string GetLocalizedFaceName(CultureInfo culture)
|
||||
{
|
||||
return GetLocalizedForLocaleOrFirst(FaceNames, culture);
|
||||
}
|
||||
|
||||
public Font Load(int size)
|
||||
{
|
||||
var handle = GetFaceHandle();
|
||||
|
||||
var instance = parent.FontManager.MakeInstance(handle, size);
|
||||
|
||||
return new VectorFont(instance, size);
|
||||
}
|
||||
|
||||
private IFontFaceHandle GetFaceHandle()
|
||||
{
|
||||
lock (parent.Lock)
|
||||
{
|
||||
if (_cachedFont != null)
|
||||
return _cachedFont;
|
||||
|
||||
parent.Sawmill.Verbose($"Loading system font face: {PostscriptName}");
|
||||
|
||||
return _cachedFont = parent.LoadFontFace(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected struct LocalizedStringSet
|
||||
{
|
||||
public static readonly LocalizedStringSet Empty = FromSingle("");
|
||||
|
||||
/// <summary>
|
||||
/// The first locale to appear in the list of localized strings.
|
||||
/// Used as fallback if the desired locale is not provided.
|
||||
/// </summary>
|
||||
public required string Primary;
|
||||
public required Dictionary<string, string> Values;
|
||||
|
||||
public static LocalizedStringSet FromSingle(string value, string language = "en")
|
||||
{
|
||||
return new LocalizedStringSet
|
||||
{
|
||||
Primary = language,
|
||||
Values = new Dictionary<string, string> { { language, value } }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected sealed class MemoryMappedFontMemoryHandle : IFontMemoryHandle
|
||||
{
|
||||
private readonly MemoryMappedFile _mappedFile;
|
||||
private readonly MemoryMappedViewAccessor _accessor;
|
||||
|
||||
public MemoryMappedFontMemoryHandle(string filePath)
|
||||
{
|
||||
_mappedFile = MemoryMappedFile.CreateFromFile(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
null,
|
||||
0,
|
||||
MemoryMappedFileAccess.Read);
|
||||
|
||||
_accessor = _mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
|
||||
}
|
||||
|
||||
public unsafe byte* GetData()
|
||||
{
|
||||
byte* pointer = null;
|
||||
_accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
public nint GetDataSize()
|
||||
{
|
||||
return (nint)_accessor.Capacity;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_accessor.Dispose();
|
||||
_mappedFile.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
#if MACOS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.Interop.MacOS;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using CF = Robust.Client.Interop.MacOS.CoreFoundation;
|
||||
using CT = Robust.Client.Interop.MacOS.CoreText;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that uses CoreText on macOS.
|
||||
/// </summary>
|
||||
internal sealed class SystemFontManagerCoreText : SystemFontManagerBase, ISystemFontManagerInternal
|
||||
{
|
||||
private static readonly FontWidth[] FontWidths = Enum.GetValues<FontWidth>();
|
||||
|
||||
public bool IsSupported => true;
|
||||
|
||||
public SystemFontManagerCoreText(ILogManager logManager, IFontManagerInternal fontManager) : base(logManager,
|
||||
fontManager)
|
||||
{
|
||||
}
|
||||
|
||||
public unsafe void Initialize()
|
||||
{
|
||||
Sawmill.Verbose("Getting CTFontCollection...");
|
||||
|
||||
var collection = CT.CTFontCollectionCreateFromAvailableFonts(null);
|
||||
var array = CT.CTFontCollectionCreateMatchingFontDescriptors(collection);
|
||||
|
||||
var count = CF.CFArrayGetCount(array);
|
||||
Sawmill.Verbose($"Have {count} descriptors...");
|
||||
|
||||
for (nint i = 0; i < count.Value; i++)
|
||||
{
|
||||
var item = (__CTFontDescriptor*)CF.CFRetain(CF.CFArrayGetValueAtIndex(array, new CLong(i)));
|
||||
|
||||
try
|
||||
{
|
||||
LoadFontDescriptor(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Sawmill.Error($"Failed to load font descriptor: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CF.CFRelease(item);
|
||||
}
|
||||
}
|
||||
|
||||
CF.CFRelease(array);
|
||||
CF.CFRelease(collection);
|
||||
}
|
||||
|
||||
private unsafe void LoadFontDescriptor(__CTFontDescriptor* descriptor)
|
||||
{
|
||||
var displayName = GetFontAttributeManaged(descriptor, CT.kCTFontDisplayNameAttribute);
|
||||
var postscriptName = GetFontAttributeManaged(descriptor, CT.kCTFontNameAttribute);
|
||||
var familyName = GetFontAttributeManaged(descriptor, CT.kCTFontFamilyNameAttribute);
|
||||
var styleName = GetFontAttributeManaged(descriptor, CT.kCTFontStyleNameAttribute);
|
||||
|
||||
var url = (__CFURL*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontURLAttribute);
|
||||
|
||||
const int maxPath = 1024;
|
||||
var buf = stackalloc byte[maxPath];
|
||||
var result = CF.CFURLGetFileSystemRepresentation(url, 1, buf, new CLong(maxPath));
|
||||
if (result == 0)
|
||||
throw new Exception("CFURLGetFileSystemRepresentation failed!");
|
||||
|
||||
// Sawmill.Verbose(CF.CFStringToManaged(CF.CFURLGetString(url)));
|
||||
|
||||
CF.CFRelease(url);
|
||||
|
||||
var traits = (__CFDictionary*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontTraitsAttribute);
|
||||
var (weight, slant, width) = ParseTraits(traits);
|
||||
|
||||
CF.CFRelease(traits);
|
||||
|
||||
var path = Marshal.PtrToStringUTF8((nint)buf)!;
|
||||
|
||||
Fonts.Add(new Handle(this)
|
||||
{
|
||||
PostscriptName = postscriptName,
|
||||
FullNames = LocalizedStringSet.FromSingle(displayName),
|
||||
FamilyNames = LocalizedStringSet.FromSingle(familyName),
|
||||
FaceNames = LocalizedStringSet.FromSingle(styleName),
|
||||
Weight = weight,
|
||||
Slant = slant,
|
||||
Width = width,
|
||||
Path = path
|
||||
});
|
||||
}
|
||||
|
||||
private static unsafe (FontWeight, FontSlant, FontWidth) ParseTraits(__CFDictionary* dictionary)
|
||||
{
|
||||
var weight = FontWeight.Normal;
|
||||
var slant = FontSlant.Normal;
|
||||
var width = FontWidth.Normal;
|
||||
|
||||
var weightVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWeightTrait);
|
||||
if (weightVal != null)
|
||||
weight = ConvertWeight(weightVal);
|
||||
|
||||
var slantVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontSlantTrait);
|
||||
if (slantVal != null)
|
||||
slant = ConvertSlant(slantVal);
|
||||
|
||||
var widthVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWidthTrait);
|
||||
if (widthVal != null)
|
||||
width = ConvertWidth(widthVal);
|
||||
|
||||
return (weight, slant, width);
|
||||
}
|
||||
|
||||
private static readonly (float, FontWeight)[] FontWeightTable =
|
||||
[
|
||||
((float) AppKit.NSFontWeightUltraLight, FontWeight.UltraLight),
|
||||
((float) AppKit.NSFontWeightThin, FontWeight.Thin),
|
||||
((float) AppKit.NSFontWeightLight, FontWeight.Light),
|
||||
((float) AppKit.NSFontWeightRegular, FontWeight.Regular),
|
||||
((float) AppKit.NSFontWeightMedium, FontWeight.Medium),
|
||||
((float) AppKit.NSFontWeightSemiBold, FontWeight.SemiBold),
|
||||
((float) AppKit.NSFontWeightBold, FontWeight.Bold),
|
||||
((float) AppKit.NSFontWeightHeavy, FontWeight.Heavy),
|
||||
((float) AppKit.NSFontWeightBlack, FontWeight.Black)
|
||||
];
|
||||
|
||||
private static unsafe FontWeight ConvertWeight(__CFNumber* number)
|
||||
{
|
||||
float val;
|
||||
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
|
||||
|
||||
var valCopy = val;
|
||||
return FontWeightTable.MinBy(tup => Math.Abs(tup.Item1 - valCopy)).Item2;
|
||||
}
|
||||
|
||||
private static unsafe FontWidth ConvertWidth(__CFNumber* number)
|
||||
{
|
||||
float val;
|
||||
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
|
||||
|
||||
// Normalize to 0-1 range
|
||||
val = (val + 1) / 2;
|
||||
var lerped = MathHelper.Lerp((float)FontWidths[0], (float)FontWidths[^1], val);
|
||||
return FontWidths.MinBy(x => Math.Abs((float)x - lerped));
|
||||
}
|
||||
|
||||
private static unsafe FontSlant ConvertSlant(__CFNumber* number)
|
||||
{
|
||||
float val;
|
||||
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
|
||||
|
||||
// Normalize to 0-1 range
|
||||
return val == 0 ? FontSlant.Normal : FontSlant.Italic;
|
||||
}
|
||||
|
||||
private static unsafe string GetFontAttributeManaged(__CTFontDescriptor* descriptor, __CFString* key)
|
||||
{
|
||||
var str = (__CFString*)CT.CTFontDescriptorCopyAttribute(descriptor, key);
|
||||
|
||||
try
|
||||
{
|
||||
return CF.CFStringToManaged(str);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CF.CFRelease(str);
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
|
||||
{
|
||||
var path = ((Handle)handle).Path;
|
||||
Sawmill.Verbose(path);
|
||||
|
||||
// CTFontDescriptor does not seem to have any way to identify *which* index in the font file should be accessed.
|
||||
// So we have to just load every one until the postscript name matches.
|
||||
return FontManager.LoadWithPostscriptName(new MemoryMappedFontMemoryHandle(path), handle.PostscriptName);
|
||||
}
|
||||
|
||||
private sealed class Handle(SystemFontManagerCoreText parent) : BaseHandle(parent)
|
||||
{
|
||||
public required string Path;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,503 @@
|
||||
#if WINDOWS
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static TerraFX.Interop.DirectX.DWRITE_FACTORY_TYPE;
|
||||
using static TerraFX.Interop.DirectX.DWRITE_FONT_PROPERTY_ID;
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
|
||||
/// </summary>
|
||||
internal sealed unsafe class SystemFontManagerDirectWrite : SystemFontManagerBase, ISystemFontManagerInternal
|
||||
{
|
||||
// For future implementors of other platforms:
|
||||
// a significant amount of code in this file will be shareable with that of other platforms,
|
||||
// so some refactoring is warranted.
|
||||
|
||||
private readonly IConfigurationManager _cfg;
|
||||
|
||||
private IDWriteFactory3* _dWriteFactory;
|
||||
private IDWriteFontSet* _systemFontSet;
|
||||
|
||||
public bool IsSupported => true;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
|
||||
/// </summary>
|
||||
public SystemFontManagerDirectWrite(
|
||||
ILogManager logManager,
|
||||
IConfigurationManager cfg,
|
||||
IFontManagerInternal fontManager)
|
||||
: base(logManager, fontManager)
|
||||
{
|
||||
_cfg = cfg;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
CreateDWriteFactory();
|
||||
|
||||
_systemFontSet = GetSystemFontSet(_dWriteFactory);
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
var fontCount = _systemFontSet->GetFontCount();
|
||||
for (var i = 0u; i < fontCount; i++)
|
||||
{
|
||||
LoadSingleFontFromSet(_systemFontSet, i);
|
||||
}
|
||||
}
|
||||
|
||||
Sawmill.Verbose($"Loaded {Fonts.Count} fonts");
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_systemFontSet->Release();
|
||||
_systemFontSet = null;
|
||||
|
||||
_dWriteFactory->Release();
|
||||
_dWriteFactory = null;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
foreach (var systemFont in Fonts)
|
||||
{
|
||||
((Handle)systemFont).FontFace->Release();
|
||||
}
|
||||
|
||||
Fonts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSingleFontFromSet(IDWriteFontSet* set, uint fontIndex)
|
||||
{
|
||||
// Get basic parameters that every font should probably have?
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_POSTSCRIPT_NAME, out var postscriptNames))
|
||||
return;
|
||||
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FULL_NAME, out var fullNames))
|
||||
return;
|
||||
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FAMILY_NAME, out var familyNames))
|
||||
return;
|
||||
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FACE_NAME, out var faceNames))
|
||||
return;
|
||||
|
||||
// I assume these parameters can't be missing in practice, but better safe than sorry.
|
||||
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_WEIGHT, out var weight);
|
||||
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STYLE, out var style);
|
||||
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STRETCH, out var stretch);
|
||||
|
||||
var parsedWeight = ParseFontWeight(weight);
|
||||
var parsedSlant = ParseFontSlant(style);
|
||||
var parsedWidth = ParseFontWidth(stretch);
|
||||
|
||||
IDWriteFontFaceReference* reference = null;
|
||||
var result = set->GetFontFaceReference(fontIndex, &reference);
|
||||
ThrowIfFailed(result);
|
||||
|
||||
var handle = new Handle(this, reference)
|
||||
{
|
||||
PostscriptName = GetLocalizedForLocaleOrFirst(postscriptNames, StandardLocale),
|
||||
FullNames = fullNames,
|
||||
FamilyNames = familyNames,
|
||||
FaceNames = faceNames,
|
||||
Weight = parsedWeight,
|
||||
Slant = parsedSlant,
|
||||
Width = parsedWidth
|
||||
};
|
||||
|
||||
Fonts.Add(handle);
|
||||
}
|
||||
|
||||
private static FontWeight ParseFontWeight(DWriteLocalizedString[]? strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return FontWeight.Regular;
|
||||
|
||||
return (FontWeight)Parse.Int32(strings[0].Value);
|
||||
}
|
||||
|
||||
private static FontSlant ParseFontSlant(DWriteLocalizedString[]? strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return FontSlant.Normal;
|
||||
|
||||
return (FontSlant)Parse.Int32(strings[0].Value);
|
||||
}
|
||||
|
||||
private static FontWidth ParseFontWidth(DWriteLocalizedString[]? strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return FontWidth.Normal;
|
||||
|
||||
return (FontWidth)Parse.Int32(strings[0].Value);
|
||||
}
|
||||
|
||||
private void CreateDWriteFactory()
|
||||
{
|
||||
fixed (IDWriteFactory3** pFactory = &_dWriteFactory)
|
||||
{
|
||||
var result = DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE_SHARED,
|
||||
__uuidof<IDWriteFactory3>(),
|
||||
(IUnknown**)pFactory);
|
||||
|
||||
ThrowIfFailed(result);
|
||||
}
|
||||
}
|
||||
|
||||
private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory)
|
||||
{
|
||||
IDWriteFactory6* factory6;
|
||||
IDWriteFontSet* fontSet;
|
||||
var result = factory->QueryInterface(__uuidof<IDWriteFactory6>(), (void**)&factory6);
|
||||
if (result.SUCCEEDED)
|
||||
{
|
||||
Sawmill.Verbose("IDWriteFactory6 available, using newer GetSystemFontSet");
|
||||
|
||||
result = factory6->GetSystemFontSet(
|
||||
_cfg.GetCVar(CVars.FontWindowsDownloadable),
|
||||
(IDWriteFontSet1**)(&fontSet));
|
||||
|
||||
factory6->Release();
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Verbose("IDWriteFactory6 not available");
|
||||
|
||||
result = factory->GetSystemFontSet(&fontSet);
|
||||
}
|
||||
|
||||
ThrowIfFailed(result, "GetSystemFontSet");
|
||||
return fontSet;
|
||||
}
|
||||
|
||||
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
|
||||
{
|
||||
var fontFace = ((Handle)handle).FontFace;
|
||||
IDWriteFontFile* file = null;
|
||||
IDWriteFontFileLoader* loader = null;
|
||||
|
||||
try
|
||||
{
|
||||
var result = fontFace->GetFontFile(&file);
|
||||
ThrowIfFailed(result, "IDWriteFontFaceReference::GetFontFile");
|
||||
result = file->GetLoader(&loader);
|
||||
ThrowIfFailed(result, "IDWriteFontFile::GetLoader");
|
||||
|
||||
void* referenceKey;
|
||||
uint referenceKeyLength;
|
||||
result = file->GetReferenceKey(&referenceKey, &referenceKeyLength);
|
||||
ThrowIfFailed(result, "IDWriteFontFile::GetReferenceKey");
|
||||
|
||||
IDWriteLocalFontFileLoader* localLoader;
|
||||
result = loader->QueryInterface(__uuidof<IDWriteLocalFontFileLoader>(), (void**)&localLoader);
|
||||
if (result.SUCCEEDED)
|
||||
{
|
||||
Sawmill.Verbose("Loading font face via memory mapped file...");
|
||||
|
||||
// We can get the local file path on disk. This means we can directly load it via mmap.
|
||||
uint filePathLength;
|
||||
ThrowIfFailed(
|
||||
localLoader->GetFilePathLengthFromKey(referenceKey, referenceKeyLength, &filePathLength),
|
||||
"IDWriteLocalFontFileLoader::GetFilePathLengthFromKey");
|
||||
var filePath = new char[filePathLength + 1];
|
||||
fixed (char* pFilePath = filePath)
|
||||
{
|
||||
ThrowIfFailed(
|
||||
localLoader->GetFilePathFromKey(
|
||||
referenceKey,
|
||||
referenceKeyLength,
|
||||
pFilePath,
|
||||
(uint)filePath.Length),
|
||||
"IDWriteLocalFontFileLoader::GetFilePathFromKey");
|
||||
}
|
||||
|
||||
var path = new string(filePath, 0, (int)filePathLength);
|
||||
|
||||
localLoader->Release();
|
||||
|
||||
return FontManager.Load(new MemoryMappedFontMemoryHandle(path));
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Verbose("Loading font face via stream...");
|
||||
|
||||
// DirectWrite doesn't give us anything to go with for this file, read it into regular memory.
|
||||
// If the font file has multiple faces, which is possible, then this approach will duplicate memory.
|
||||
// That sucks, but I'm really not sure whether there's any way around this short of
|
||||
// comparing the memory contents by hashing to check equality.
|
||||
// As I'm pretty sure we can't like reference equality check the font objects somehow.
|
||||
IDWriteFontFileStream* stream;
|
||||
result = loader->CreateStreamFromKey(referenceKey, referenceKeyLength, &stream);
|
||||
ThrowIfFailed(result, "IDWriteFontFileLoader::CreateStreamFromKey");
|
||||
|
||||
using var streamObject = new DirectWriteStream(stream);
|
||||
return FontManager.Load(streamObject, (int)fontFace->GetFontFaceIndex());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file != null)
|
||||
file->Release();
|
||||
if (loader != null)
|
||||
loader->Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetStrings(
|
||||
IDWriteFontSet* set,
|
||||
uint listIndex,
|
||||
DWRITE_FONT_PROPERTY_ID property,
|
||||
[NotNullWhen(true)] out DWriteLocalizedString[]? strings)
|
||||
{
|
||||
BOOL exists;
|
||||
IDWriteLocalizedStrings* dWriteStrings = null;
|
||||
var result = set->GetPropertyValues(
|
||||
listIndex,
|
||||
property,
|
||||
&exists,
|
||||
&dWriteStrings);
|
||||
ThrowIfFailed(result, "IDWriteFontSet::GetPropertyValues");
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
strings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
strings = GetStrings(dWriteStrings);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
dWriteStrings->Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetStringsSet(
|
||||
IDWriteFontSet* set,
|
||||
uint listIndex,
|
||||
DWRITE_FONT_PROPERTY_ID property,
|
||||
out LocalizedStringSet strings)
|
||||
{
|
||||
if (!TryGetStrings(set, listIndex, property, out var stringsArray))
|
||||
{
|
||||
strings = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
strings = StringsToSet(stringsArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DWriteLocalizedString[] GetStrings(IDWriteLocalizedStrings* localizedStrings)
|
||||
{
|
||||
IDWriteStringList* list;
|
||||
ThrowIfFailed(localizedStrings->QueryInterface(__uuidof<IDWriteStringList>(), (void**)&list));
|
||||
|
||||
try
|
||||
{
|
||||
return GetStrings(list);
|
||||
}
|
||||
finally
|
||||
{
|
||||
list->Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList)
|
||||
{
|
||||
var array = new DWriteLocalizedString[stringList->GetCount()];
|
||||
|
||||
var stringPool = ArrayPool<char>.Shared.Rent(256);
|
||||
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
uint length;
|
||||
|
||||
ThrowIfFailed(stringList->GetStringLength((uint)i, &length), "IDWriteStringList::GetStringLength");
|
||||
ExpandIfNecessary(ref stringPool, length + 1);
|
||||
fixed (char* pArr = stringPool)
|
||||
{
|
||||
ThrowIfFailed(
|
||||
stringList->GetString((uint)i, pArr, (uint)stringPool.Length),
|
||||
"IDWriteStringList::GetString");
|
||||
}
|
||||
|
||||
var value = new string(stringPool, 0, (int)length);
|
||||
|
||||
ThrowIfFailed(stringList->GetLocaleNameLength((uint)i, &length), "IDWriteStringList::GetLocaleNameLength");
|
||||
ExpandIfNecessary(ref stringPool, length + 1);
|
||||
fixed (char* pArr = stringPool)
|
||||
{
|
||||
ThrowIfFailed(
|
||||
stringList->GetLocaleName((uint)i, pArr, (uint)stringPool.Length),
|
||||
"IDWriteStringList::GetLocaleName");
|
||||
}
|
||||
|
||||
var localeName = new string(stringPool, 0, (int)length);
|
||||
|
||||
array[i] = new DWriteLocalizedString(value, localeName);
|
||||
}
|
||||
|
||||
ArrayPool<char>.Shared.Return(stringPool);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private static void ExpandIfNecessary(ref char[] array, uint requiredLength)
|
||||
{
|
||||
if (requiredLength < array.Length)
|
||||
return;
|
||||
|
||||
ArrayPool<char>.Shared.Return(array);
|
||||
array = ArrayPool<char>.Shared.Rent(checked((int)requiredLength));
|
||||
}
|
||||
|
||||
private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
|
||||
foreach (var (value, localeName) in strings)
|
||||
{
|
||||
dict[localeName] = value;
|
||||
}
|
||||
|
||||
return new LocalizedStringSet { Primary = strings[0].LocaleName, Values = dict };
|
||||
}
|
||||
|
||||
private sealed class Handle(SystemFontManagerDirectWrite parent, IDWriteFontFaceReference* fontFace) : BaseHandle(parent)
|
||||
{
|
||||
public readonly IDWriteFontFaceReference* FontFace = fontFace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple implementation of a .NET Stream over a IDWriteFontFileStream.
|
||||
/// </summary>
|
||||
private sealed class DirectWriteStream : Stream
|
||||
{
|
||||
private readonly IDWriteFontFileStream* _stream;
|
||||
private readonly ulong _size;
|
||||
|
||||
private ulong _position;
|
||||
private bool _disposed;
|
||||
|
||||
public DirectWriteStream(IDWriteFontFileStream* stream)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
fixed (ulong* pSize = &_size)
|
||||
{
|
||||
var result = _stream->GetFileSize(pSize);
|
||||
ThrowIfFailed(result, "IDWriteFontFileStream::GetFileSize");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return Read(buffer.AsSpan(offset, count));
|
||||
}
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(DirectWriteStream));
|
||||
|
||||
var readLength = (uint)buffer.Length;
|
||||
if (readLength + _position > _size)
|
||||
readLength = (uint)(_size - _position);
|
||||
|
||||
void* fragmentStart;
|
||||
void* fragmentContext;
|
||||
|
||||
var result = _stream->ReadFileFragment(&fragmentStart, _position, readLength, &fragmentContext);
|
||||
ThrowIfFailed(result);
|
||||
|
||||
var data = new ReadOnlySpan<byte>(fragmentStart, (int)readLength);
|
||||
data.CopyTo(buffer);
|
||||
|
||||
_stream->ReleaseFileFragment(fragmentContext);
|
||||
|
||||
_position += readLength;
|
||||
return (int)readLength;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin:
|
||||
Position = offset;
|
||||
break;
|
||||
case SeekOrigin.Current:
|
||||
Position += offset;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
Position = Length + offset;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(origin), origin, null);
|
||||
}
|
||||
|
||||
return Position;
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => (long)_size;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => (long)_position;
|
||||
set
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(value);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, _size);
|
||||
|
||||
_position = (ulong)value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_stream->Release();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private record struct DWriteLocalizedString(string Value, string LocaleName);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
/// <summary>
|
||||
/// A fallback implementation of <see cref="ISystemFontManager"/> that just loads no fonts.
|
||||
/// </summary>
|
||||
internal sealed class SystemFontManagerFallback : ISystemFontManagerInternal
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool IsSupported => false;
|
||||
public IEnumerable<ISystemFontFace> SystemFontFaces => [];
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
#if FREEDESKTOP
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Log;
|
||||
using SpaceWizards.Fontconfig.Interop;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
internal sealed unsafe class SystemFontManagerFontconfig : SystemFontManagerBase, ISystemFontManagerInternal
|
||||
{
|
||||
private static readonly (int Fc, FontWidth Width)[] WidthTable = [
|
||||
(Fontconfig.FC_WIDTH_ULTRACONDENSED, FontWidth.UltraCondensed),
|
||||
(Fontconfig.FC_WIDTH_EXTRACONDENSED, FontWidth.ExtraCondensed),
|
||||
(Fontconfig.FC_WIDTH_CONDENSED, FontWidth.Condensed),
|
||||
(Fontconfig.FC_WIDTH_SEMICONDENSED, FontWidth.SemiCondensed),
|
||||
(Fontconfig.FC_WIDTH_NORMAL, FontWidth.Normal),
|
||||
(Fontconfig.FC_WIDTH_SEMIEXPANDED, FontWidth.SemiExpanded),
|
||||
(Fontconfig.FC_WIDTH_EXPANDED, FontWidth.Expanded),
|
||||
(Fontconfig.FC_WIDTH_EXTRAEXPANDED, FontWidth.ExtraExpanded),
|
||||
(Fontconfig.FC_WIDTH_ULTRAEXPANDED, FontWidth.UltraExpanded),
|
||||
];
|
||||
|
||||
public bool IsSupported => true;
|
||||
|
||||
public SystemFontManagerFontconfig(ILogManager logManager, IFontManagerInternal fontManager)
|
||||
: base(logManager, fontManager)
|
||||
{
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
Sawmill.Verbose("Initializing Fontconfig...");
|
||||
|
||||
var result = Fontconfig.FcInit();
|
||||
if (result == Fontconfig.FcFalse)
|
||||
throw new InvalidOperationException("Failed to initialize fontconfig!");
|
||||
|
||||
Sawmill.Verbose("Listing fonts...");
|
||||
|
||||
var os = Fontconfig.FcObjectSetCreate();
|
||||
AddToObjectSet(os, Fontconfig.FC_FAMILY);
|
||||
AddToObjectSet(os, Fontconfig.FC_FAMILYLANG);
|
||||
AddToObjectSet(os, Fontconfig.FC_STYLE);
|
||||
AddToObjectSet(os, Fontconfig.FC_STYLELANG);
|
||||
AddToObjectSet(os, Fontconfig.FC_FULLNAME);
|
||||
AddToObjectSet(os, Fontconfig.FC_FULLNAMELANG);
|
||||
AddToObjectSet(os, Fontconfig.FC_POSTSCRIPT_NAME);
|
||||
|
||||
AddToObjectSet(os, Fontconfig.FC_SLANT);
|
||||
AddToObjectSet(os, Fontconfig.FC_WEIGHT);
|
||||
AddToObjectSet(os, Fontconfig.FC_WIDTH);
|
||||
|
||||
AddToObjectSet(os, Fontconfig.FC_FILE);
|
||||
AddToObjectSet(os, Fontconfig.FC_INDEX);
|
||||
|
||||
var allPattern = Fontconfig.FcPatternCreate();
|
||||
var set = Fontconfig.FcFontList(null, allPattern, os);
|
||||
|
||||
for (var i = 0; i < set->nfont; i++)
|
||||
{
|
||||
var pattern = set->fonts[i];
|
||||
|
||||
try
|
||||
{
|
||||
LoadPattern(pattern);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Sawmill.Error($"Error while loading pattern: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Fontconfig.FcPatternDestroy(allPattern);
|
||||
Fontconfig.FcObjectSetDestroy(os);
|
||||
Fontconfig.FcFontSetDestroy(set);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
private void LoadPattern(FcPattern* pattern)
|
||||
{
|
||||
var path = PatternGetStrings(pattern, Fontconfig.FC_FILE)![0];
|
||||
var idx = PatternGetInts(pattern, Fontconfig.FC_INDEX)![0];
|
||||
|
||||
var family = PatternToLocalized(pattern, Fontconfig.FC_FAMILY, Fontconfig.FC_FAMILYLANG);
|
||||
var style = PatternToLocalized(pattern, Fontconfig.FC_STYLE, Fontconfig.FC_STYLELANG);
|
||||
var fullName = PatternToLocalized(pattern, Fontconfig.FC_FULLNAME, Fontconfig.FC_FULLNAMELANG);
|
||||
var psName = PatternGetStrings(pattern, Fontconfig.FC_POSTSCRIPT_NAME);
|
||||
if (psName == null)
|
||||
return;
|
||||
|
||||
var slant = PatternGetInts(pattern, Fontconfig.FC_SLANT) ?? [Fontconfig.FC_SLANT_ROMAN];
|
||||
var weight = PatternGetInts(pattern, Fontconfig.FC_WEIGHT) ?? [Fontconfig.FC_WEIGHT_REGULAR];
|
||||
var width = PatternGetInts(pattern, Fontconfig.FC_WIDTH) ?? [Fontconfig.FC_WIDTH_NORMAL];
|
||||
|
||||
Fonts.Add(new Handle(this)
|
||||
{
|
||||
FilePath = path,
|
||||
FileIndex = idx,
|
||||
FaceNames = style ?? LocalizedStringSet.Empty,
|
||||
FullNames = fullName ?? LocalizedStringSet.Empty,
|
||||
FamilyNames = family ?? LocalizedStringSet.Empty,
|
||||
PostscriptName = psName[0],
|
||||
Slant = SlantFromFontconfig(slant[0]),
|
||||
Weight = WeightFromFontconfig(weight[0]),
|
||||
Width = WidthFromFontconfig(width[0])
|
||||
});
|
||||
}
|
||||
|
||||
private static FontWeight WeightFromFontconfig(int value)
|
||||
{
|
||||
return (FontWeight)Fontconfig.FcWeightToOpenType(value);
|
||||
}
|
||||
|
||||
private static FontSlant SlantFromFontconfig(int value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
Fontconfig.FC_SLANT_ITALIC => FontSlant.Italic,
|
||||
Fontconfig.FC_SLANT_OBLIQUE => FontSlant.Italic,
|
||||
_ => FontSlant.Normal,
|
||||
};
|
||||
}
|
||||
|
||||
private static FontWidth WidthFromFontconfig(int value)
|
||||
{
|
||||
return WidthTable.MinBy(t => Math.Abs(t.Fc - value)).Width;
|
||||
}
|
||||
|
||||
private static unsafe void AddToObjectSet(FcObjectSet* os, ReadOnlySpan<byte> value)
|
||||
{
|
||||
var result = Fontconfig.FcObjectSetAdd(os, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(value)));
|
||||
if (result == Fontconfig.FcFalse)
|
||||
throw new InvalidOperationException("Failed to add to object set!");
|
||||
}
|
||||
|
||||
private static unsafe string[]? PatternGetStrings(FcPattern* pattern, ReadOnlySpan<byte> @object)
|
||||
{
|
||||
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out string value) =>
|
||||
{
|
||||
byte* str = null;
|
||||
var res = Fontconfig.FcPatternGetString(p, o, i, &str);
|
||||
value = Marshal.PtrToStringUTF8((nint)str)!;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
private static unsafe int[]? PatternGetInts(FcPattern* pattern, ReadOnlySpan<byte> @object)
|
||||
{
|
||||
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out int value) =>
|
||||
{
|
||||
FcResult res;
|
||||
fixed (int* pValue = &value)
|
||||
{
|
||||
res = Fontconfig.FcPatternGetInteger(p, o, i, pValue);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
private delegate FcResult GetValue<T>(FcPattern* p, sbyte* o, int i, out T value);
|
||||
private static unsafe T[]? PatternGetValues<T>(FcPattern* pattern, ReadOnlySpan<byte> @object, GetValue<T> getValue)
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
var i = 0;
|
||||
while (true)
|
||||
{
|
||||
var result = getValue(pattern, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(@object)), i++, out var value);
|
||||
if (result == FcResult.FcResultMatch)
|
||||
{
|
||||
list.Add(value);
|
||||
}
|
||||
else if (result == FcResult.FcResultNoMatch)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (result == FcResult.FcResultNoId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"FcPatternGetString gave error: {result}");
|
||||
}
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
private static LocalizedStringSet? PatternToLocalized(FcPattern* pattern, ReadOnlySpan<byte> @object, ReadOnlySpan<byte> objectLang)
|
||||
{
|
||||
var values = PatternGetStrings(pattern, @object);
|
||||
var languages = PatternGetStrings(pattern, objectLang);
|
||||
|
||||
if (values == null || languages == null || values.Length == 0 || languages.Length != values.Length)
|
||||
return null;
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
var val = values[i];
|
||||
var lang = languages[i];
|
||||
|
||||
dict.TryAdd(lang, val);
|
||||
}
|
||||
|
||||
return new LocalizedStringSet
|
||||
{
|
||||
Primary = languages[0],
|
||||
Values = dict
|
||||
};
|
||||
}
|
||||
|
||||
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
|
||||
{
|
||||
var cast = (Handle)handle;
|
||||
|
||||
return FontManager.Load(new MemoryMappedFontMemoryHandle(cast.FilePath), cast.FileIndex);
|
||||
}
|
||||
|
||||
private sealed class Handle(SystemFontManagerFontconfig parent) : BaseHandle(parent)
|
||||
{
|
||||
public required string FilePath;
|
||||
public required int FileIndex;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,16 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SharpFont;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
@@ -20,6 +20,7 @@ namespace Robust.Client.Graphics
|
||||
private const int SheetHeight = 256;
|
||||
|
||||
private readonly IClyde _clyde;
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
private uint _baseFontDpi = 96;
|
||||
|
||||
@@ -28,22 +29,56 @@ namespace Robust.Client.Graphics
|
||||
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
|
||||
new();
|
||||
|
||||
public FontManager(IClyde clyde)
|
||||
public FontManager(IClyde clyde, ILogManager logManager)
|
||||
{
|
||||
_clyde = clyde;
|
||||
_library = new Library();
|
||||
_sawmill = logManager.GetSawmill("font");
|
||||
}
|
||||
|
||||
public IFontFaceHandle Load(Stream stream)
|
||||
public IFontFaceHandle Load(Stream stream, int index = 0)
|
||||
{
|
||||
// Freetype directly operates on the font memory managed by us.
|
||||
// As such, the font data should be pinned in POH.
|
||||
var fontData = stream.CopyToPinnedArray();
|
||||
var face = new Face(_library, fontData, 0);
|
||||
var handle = new FontFaceHandle(face);
|
||||
return Load(new ArrayMemoryHandle(fontData), index);
|
||||
}
|
||||
|
||||
public IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0)
|
||||
{
|
||||
var face = FaceLoad(memory, index);
|
||||
var handle = new FontFaceHandle(face, memory);
|
||||
return handle;
|
||||
}
|
||||
|
||||
public IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName)
|
||||
{
|
||||
var numFaces = 1;
|
||||
|
||||
for (var i = 0; i < numFaces; i++)
|
||||
{
|
||||
var face = FaceLoad(memory, i);
|
||||
numFaces = face.FaceCount;
|
||||
|
||||
if (face.GetPostscriptName() == postscriptName)
|
||||
return new FontFaceHandle(face, memory);
|
||||
|
||||
face.Dispose();
|
||||
}
|
||||
|
||||
// Fallback, load SOMETHING.
|
||||
_sawmill.Warning($"Failed to load correct font via postscript name! {postscriptName}");
|
||||
return new FontFaceHandle(FaceLoad(memory, 0), memory);
|
||||
}
|
||||
|
||||
private unsafe Face FaceLoad(IFontMemoryHandle memory, int index)
|
||||
{
|
||||
return new Face(_library,
|
||||
(nint)memory.GetData(),
|
||||
checked((int)memory.GetDataSize()),
|
||||
index);
|
||||
}
|
||||
|
||||
void IFontManagerInternal.SetFontDpi(uint fontDpi)
|
||||
{
|
||||
_baseFontDpi = fontDpi;
|
||||
@@ -235,10 +270,13 @@ namespace Robust.Client.Graphics
|
||||
|
||||
private sealed class FontFaceHandle : IFontFaceHandle
|
||||
{
|
||||
// Keep this alive to avoid it being GC'd.
|
||||
private readonly IFontMemoryHandle _memoryHandle;
|
||||
public Face Face { get; }
|
||||
|
||||
public FontFaceHandle(Face face)
|
||||
public FontFaceHandle(Face face, IFontMemoryHandle memoryHandle)
|
||||
{
|
||||
_memoryHandle = memoryHandle;
|
||||
Face = face;
|
||||
}
|
||||
}
|
||||
@@ -377,5 +415,32 @@ namespace Robust.Client.Graphics
|
||||
public CharMetrics Metrics;
|
||||
public AtlasTexture? Texture;
|
||||
}
|
||||
|
||||
private sealed class ArrayMemoryHandle(byte[] array) : IFontMemoryHandle
|
||||
{
|
||||
private GCHandle _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
|
||||
|
||||
public unsafe byte* GetData()
|
||||
{
|
||||
return (byte*) _gcHandle.AddrOfPinnedObject();
|
||||
}
|
||||
|
||||
public IntPtr GetDataSize()
|
||||
{
|
||||
return array.Length;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gcHandle.Free();
|
||||
_gcHandle = default;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~ArrayMemoryHandle()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Robust.Shared.Graphics;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
@@ -10,7 +10,15 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
internal interface IFontManagerInternal : IFontManager
|
||||
{
|
||||
IFontFaceHandle Load(Stream stream);
|
||||
IFontFaceHandle Load(Stream stream, int index = 0);
|
||||
IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Load a specified font in a font collection.
|
||||
/// </summary>
|
||||
/// <param name="memory">Memory for the entire font collection.</param>
|
||||
/// <param name="postscriptName">The postscript name of the font to load.</param>
|
||||
IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName);
|
||||
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
|
||||
void SetFontDpi(uint fontDpi);
|
||||
}
|
||||
@@ -22,8 +30,6 @@ namespace Robust.Client.Graphics
|
||||
|
||||
internal interface IFontInstanceHandle
|
||||
{
|
||||
|
||||
|
||||
Texture? GetCharTexture(Rune codePoint, float scale);
|
||||
Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale);
|
||||
CharMetrics? GetCharMetrics(Rune codePoint, float scale);
|
||||
@@ -35,6 +41,12 @@ namespace Robust.Client.Graphics
|
||||
int GetLineHeight(float scale);
|
||||
}
|
||||
|
||||
internal unsafe interface IFontMemoryHandle : IDisposable
|
||||
{
|
||||
byte* GetData();
|
||||
nint GetDataSize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metrics for a single glyph in a font.
|
||||
/// Refer to https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html for more information.
|
||||
|
||||
127
Robust.Client/Graphics/ISystemFontManager.cs
Normal file
127
Robust.Client/Graphics/ISystemFontManager.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to fonts installed on the user's operating system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Different operating systems ship different fonts, so you should generally not rely on any one
|
||||
/// specific font being available. This system is primarily provided for allowing user preference.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISystemFontFace"/>
|
||||
public interface ISystemFontManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether access to system fonts is currently supported on this platform.
|
||||
/// </summary>
|
||||
bool IsSupported { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of font face available from the operating system.
|
||||
/// </summary>
|
||||
IEnumerable<ISystemFontFace> SystemFontFaces { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single font face, provided by the user's operating system.
|
||||
/// </summary>
|
||||
/// <seealso cref="ISystemFontManager"/>
|
||||
public interface ISystemFontFace
|
||||
{
|
||||
/// <summary>
|
||||
/// The PostScript name of the font face.
|
||||
/// This is generally the closest to an unambiguous unique identifier as you're going to get.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Arial-ItalicMT"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
string PostscriptName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full name of the font face, localized to the current locale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Arial Cursiva"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GetLocalizedFullName"/>
|
||||
string FullName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The family name of the font face, localized to the current locale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Arial"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GetLocalizedFamilyName"/>
|
||||
string FamilyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The face name (or "style name") of the font face, localized to the current locale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Cursiva"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GetLocalizedFaceName"/>
|
||||
string FaceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="FullName"/>, localized to a specific locale.
|
||||
/// </summary>
|
||||
/// <param name="culture">The locale to fetch the localized string for.</param>
|
||||
string GetLocalizedFullName(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="FamilyName"/>, localized to a specific locale.
|
||||
/// </summary>
|
||||
/// <param name="culture">The locale to fetch the localized string for.</param>
|
||||
string GetLocalizedFamilyName(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="FaceName"/>, localized to a specific locale.
|
||||
/// </summary>
|
||||
/// <param name="culture">The locale to fetch the localized string for.</param>
|
||||
string GetLocalizedFaceName(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// The weight of the font face.
|
||||
/// </summary>
|
||||
FontWeight Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The slant of the font face.
|
||||
/// </summary>
|
||||
FontSlant Slant { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the font face.
|
||||
/// </summary>
|
||||
FontWidth Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Load the font face so that it can be used in-engine.
|
||||
/// </summary>
|
||||
/// <param name="size">The size to load the font at.</param>
|
||||
/// <returns>A font object that can be used to render text.</returns>
|
||||
Font Load(int size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Engine-internal API for <see cref="ISystemFontManager"/>.
|
||||
/// </summary>
|
||||
internal interface ISystemFontManagerInternal : ISystemFontManager
|
||||
{
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
}
|
||||
89
Robust.Client/Graphics/SystemFontManager.cs
Normal file
89
Robust.Client/Graphics/SystemFontManager.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics.FontManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that proxies to platform-specific implementations,
|
||||
/// and adds additional logging.
|
||||
/// </summary>
|
||||
internal sealed class SystemFontManager : ISystemFontManagerInternal, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private ISystemFontManagerInternal _implementation = default!;
|
||||
|
||||
public bool IsSupported => _implementation.IsSupported;
|
||||
public IEnumerable<ISystemFontFace> SystemFontFaces => _implementation.SystemFontFaces;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_implementation = GetImplementation();
|
||||
_sawmill.Verbose($"Using {_implementation.GetType()}");
|
||||
|
||||
_sawmill.Debug("Initializing system font manager implementation");
|
||||
try
|
||||
{
|
||||
var sw = RStopwatch.StartNew();
|
||||
_implementation.Initialize();
|
||||
_sawmill.Debug($"Done initializing system font manager in {sw.Elapsed}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// This is a non-critical engine system that has to parse significant amounts of external data.
|
||||
// Best to fail gracefully to avoid full startup failures.
|
||||
|
||||
_sawmill.Error($"Error while initializing system font manager, resorting to fallback: {e}");
|
||||
_implementation = new SystemFontManagerFallback();
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_sawmill.Verbose("Shutting down system font manager");
|
||||
|
||||
try
|
||||
{
|
||||
_implementation.Shutdown();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Exception shutting down system font manager: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
_sawmill.Verbose("Successfully shut down system font manager");
|
||||
}
|
||||
|
||||
private ISystemFontManagerInternal GetImplementation()
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.FontSystem))
|
||||
return new SystemFontManagerFallback();
|
||||
|
||||
#if WINDOWS
|
||||
return new SystemFontManagerDirectWrite(_logManager, _cfg, _fontManager);
|
||||
#elif FREEDESKTOP
|
||||
return new SystemFontManagerFontconfig(_logManager, _fontManager);
|
||||
#elif MACOS
|
||||
return new SystemFontManagerCoreText(_logManager, _fontManager);
|
||||
#else
|
||||
return new SystemFontManagerFallback();
|
||||
#endif
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("font.system");
|
||||
// _sawmill.Level = LogLevel.Verbose;
|
||||
}
|
||||
}
|
||||
24
Robust.Client/Interop/MacOS/AppKit.cs
Normal file
24
Robust.Client/Interop/MacOS/AppKit.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
#if MACOS
|
||||
|
||||
namespace Robust.Client.Interop.MacOS;
|
||||
|
||||
/// <summary>
|
||||
/// Binding to macOS AppKit.
|
||||
/// </summary>
|
||||
internal static class AppKit
|
||||
{
|
||||
// Values pulled from here:
|
||||
// https://chromium.googlesource.com/chromium/src/+/b5019b491932dfa597acb3a13a9e7780fb6525a9/ui/gfx/platform_font_mac.mm#53
|
||||
public const double NSFontWeightUltraLight = -0.8;
|
||||
public const double NSFontWeightThin = -0.6;
|
||||
public const double NSFontWeightLight = -0.4;
|
||||
public const double NSFontWeightRegular = 0;
|
||||
public const double NSFontWeightMedium = 0.23;
|
||||
public const double NSFontWeightSemiBold = 0.30;
|
||||
public const double NSFontWeightBold = 0.40;
|
||||
public const double NSFontWeightHeavy = 0.56;
|
||||
public const double NSFontWeightBlack = 0.62;
|
||||
}
|
||||
|
||||
#endif
|
||||
97
Robust.Client/Interop/MacOS/CoreFoundation.cs
Normal file
97
Robust.Client/Interop/MacOS/CoreFoundation.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
#if MACOS
|
||||
using System.Runtime.InteropServices;
|
||||
using CFIndex = System.Runtime.InteropServices.CLong;
|
||||
using Boolean = byte;
|
||||
|
||||
namespace Robust.Client.Interop.MacOS;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
/// <summary>
|
||||
/// Binding to macOS Core Foundation.
|
||||
/// </summary>
|
||||
internal static unsafe class CoreFoundation
|
||||
{
|
||||
private const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
|
||||
|
||||
public const int kCFNumberFloat32Type = 5;
|
||||
|
||||
public static string CFStringToManaged(__CFString* str)
|
||||
{
|
||||
var length = CFStringGetLength(str);
|
||||
|
||||
return string.Create(
|
||||
checked((int)length.Value),
|
||||
(nint)str,
|
||||
static (span, arg) =>
|
||||
{
|
||||
fixed (char* pBuffer = span)
|
||||
{
|
||||
CFStringGetCharacters((__CFString*)arg,
|
||||
new CFRange
|
||||
{
|
||||
location = new CFIndex(0),
|
||||
length = new CFIndex(span.Length),
|
||||
},
|
||||
pBuffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void* CFRetain(void* cf);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFRelease(void* cf);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern CFIndex CFArrayGetCount(__CFArray* array);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void* CFArrayGetValueAtIndex(__CFArray* array, CFIndex index);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern CFIndex CFStringGetLength(__CFString* str);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFStringGetCharacters(__CFString* str, CFRange range, char* buffer);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern Boolean CFURLGetFileSystemRepresentation(
|
||||
__CFURL* url,
|
||||
Boolean resolveAgainstBase,
|
||||
byte* buffer,
|
||||
CFIndex maxBufLen);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern __CFString* CFURLGetString(__CFURL* url);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern CFIndex CFDictionaryGetCount(__CFDictionary* theDict);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void* CFDictionaryGetValue(__CFDictionary* theDict, void* key);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFDictionaryGetKeysAndValues(__CFDictionary* theDict, void** keys, void** values);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFNumberGetValue(__CFNumber* number, CLong theType, void* valuePtr);
|
||||
}
|
||||
|
||||
internal struct __CFNumber;
|
||||
|
||||
internal struct __CFString;
|
||||
|
||||
internal struct __CFURL;
|
||||
|
||||
internal struct __CFArray;
|
||||
|
||||
internal struct __CFDictionary;
|
||||
|
||||
internal struct CFRange
|
||||
{
|
||||
public CFIndex location;
|
||||
public CFIndex length;
|
||||
}
|
||||
#endif
|
||||
54
Robust.Client/Interop/MacOS/CoreText.cs
Normal file
54
Robust.Client/Interop/MacOS/CoreText.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
#if MACOS
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Robust.Client.Interop.MacOS;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
/// <summary>
|
||||
/// Binding to macOS Core Text.
|
||||
/// </summary>
|
||||
internal static unsafe class CoreText
|
||||
{
|
||||
private const string CoreTextLibrary = "/System/Library/Frameworks/CoreText.framework/CoreText";
|
||||
|
||||
public static readonly __CFString* kCTFontURLAttribute;
|
||||
public static readonly __CFString* kCTFontNameAttribute;
|
||||
public static readonly __CFString* kCTFontDisplayNameAttribute;
|
||||
public static readonly __CFString* kCTFontFamilyNameAttribute;
|
||||
public static readonly __CFString* kCTFontStyleNameAttribute;
|
||||
public static readonly __CFString* kCTFontTraitsAttribute;
|
||||
public static readonly __CFString* kCTFontWeightTrait;
|
||||
public static readonly __CFString* kCTFontWidthTrait;
|
||||
public static readonly __CFString* kCTFontSlantTrait;
|
||||
|
||||
static CoreText()
|
||||
{
|
||||
var lib = NativeLibrary.Load(CoreTextLibrary);
|
||||
kCTFontURLAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontURLAttribute));
|
||||
kCTFontNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontNameAttribute));
|
||||
kCTFontDisplayNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontDisplayNameAttribute));
|
||||
kCTFontFamilyNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontFamilyNameAttribute));
|
||||
kCTFontStyleNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontStyleNameAttribute));
|
||||
kCTFontTraitsAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontTraitsAttribute));
|
||||
kCTFontWeightTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWeightTrait));
|
||||
kCTFontWidthTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWidthTrait));
|
||||
kCTFontSlantTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontSlantTrait));
|
||||
}
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern __CTFontCollection* CTFontCollectionCreateFromAvailableFonts(__CFDictionary* options);
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern __CFArray* CTFontCollectionCreateMatchingFontDescriptors(__CTFontCollection* collection);
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern void* CTFontDescriptorCopyAttribute(__CTFontDescriptor* descriptor, __CFString* attribute);
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern __CFDictionary* CTFontDescriptorCopyAttributes(__CTFontDescriptor* descriptor);
|
||||
}
|
||||
|
||||
internal struct __CTFontCollection;
|
||||
internal struct __CTFontDescriptor;
|
||||
#endif
|
||||
@@ -70,6 +70,11 @@
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
<ItemGroup Condition="'$(IsFreedesktop)' == 'True'">
|
||||
<PackageReference Include="SpaceWizards.Fontconfig.Interop" />
|
||||
<RobustLinkAssemblies Include="SpaceWizards.Fontconfig.Interop" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\MSBuild\XamlIL.targets" />
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Trimming.targets" />
|
||||
|
||||
@@ -1924,5 +1924,21 @@ namespace Robust.Shared
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> XamlHotReloadMarkerName =
|
||||
CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* FONT
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// If false, disable system font support.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> FontSystem =
|
||||
CVarDef.Create("font.system", true, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If true, allow Windows "downloadable" fonts to be exposed to the system fonts API.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> FontWindowsDownloadable =
|
||||
CVarDef.Create("font.windows_downloadable", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user