diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 7930f5c79..6350da4aa 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -134,6 +134,15 @@ namespace Dalamud.Configuration.Internal
///
public bool UseAxisFontsFromGame { get; set; } = false;
+ ///
+ /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness.
+ ///
+ /// Before gamma is applied...
+ /// * ...TTF fonts loaded with stb or FreeType are in linear space.
+ /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
+ ///
+ public float FontGamma { get; set; } = 1.0f;
+
///
/// Gets or sets a value indicating whether or not plugin UI should be hidden.
///
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 2d2ef88b6..07d79df2b 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -8,7 +8,7 @@
- 6.3.0.4
+ 6.3.0.6
XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs
index a50941883..e2fa1d941 100644
--- a/Dalamud/Interface/GameFonts/GameFontHandle.cs
+++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs
@@ -1,5 +1,6 @@
using System;
using System.Numerics;
+
using ImGuiNET;
namespace Dalamud.Interface.GameFonts
@@ -31,7 +32,16 @@ namespace Dalamud.Interface.GameFonts
///
/// Gets a value indicating whether this font is ready for use.
///
- public bool Available => this.manager.GetFont(this.fontStyle) != null;
+ public bool Available
+ {
+ get
+ {
+ unsafe
+ {
+ return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null;
+ }
+ }
+ }
///
/// Gets the font.
@@ -68,9 +78,13 @@ namespace Dalamud.Interface.GameFonts
}
else
{
- this.LayoutBuilder(text)
- .Build()
- .Draw(ImGui.GetWindowDrawList(), ImGui.GetWindowPos() + ImGui.GetCursorPos(), ImGui.GetColorU32(ImGuiCol.Text));
+ var pos = ImGui.GetWindowPos() + ImGui.GetCursorPos();
+ pos.X -= ImGui.GetScrollX();
+ pos.Y -= ImGui.GetScrollY();
+
+ var layout = this.LayoutBuilder(text).Build();
+ layout.Draw(ImGui.GetWindowDrawList(), pos, ImGui.GetColorU32(ImGuiCol.Text));
+ ImGui.Dummy(new Vector2(layout.Width, layout.Height));
}
}
diff --git a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
index dc2d5f380..482ef22e2 100644
--- a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
+++ b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
@@ -73,7 +73,6 @@ namespace Dalamud.Interface.GameFonts
/// Color.
public void Draw(ImDrawListPtr drawListPtr, Vector2 pos, uint col)
{
- ImGui.Dummy(new Vector2(this.Width, this.Height));
foreach (var element in this.Elements)
{
if (element.IsControl)
diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs
index 44772bc48..83178acc9 100644
--- a/Dalamud/Interface/GameFonts/GameFontManager.cs
+++ b/Dalamud/Interface/GameFonts/GameFontManager.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Numerics;
using System.Text;
+using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using ImGuiNET;
@@ -116,18 +117,21 @@ namespace Dalamud.Interface.GameFonts
/// Target font.
/// Whether to copy missing glyphs only.
/// Whether to call target.BuildLookupTable().
- public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable)
+ /// Low codepoint range to copy.
+ /// High codepoing range to copy.
+ public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE)
{
if (!source.HasValue || !target.HasValue)
return;
+ var scale = target.Value!.FontSize / source.Value!.FontSize;
unsafe
{
var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data;
for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++)
{
var glyph = &glyphs[j];
- if (glyph->Codepoint < 32 || glyph->Codepoint >= 0xFFFF)
+ if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh)
continue;
var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr;
@@ -136,27 +140,27 @@ namespace Dalamud.Interface.GameFonts
target.Value!.AddGlyph(
target.Value!.ConfigData,
(ushort)glyph->Codepoint,
- glyph->X0,
- glyph->Y0,
- glyph->X0 + ((glyph->X1 - glyph->X0) * target.Value!.FontSize / source.Value!.FontSize),
- glyph->Y0 + ((glyph->Y1 - glyph->Y0) * target.Value!.FontSize / source.Value!.FontSize),
+ glyph->X0 * scale,
+ glyph->Y0 * scale,
+ glyph->X1 * scale,
+ glyph->Y1 * scale,
glyph->U0,
glyph->V0,
glyph->U1,
glyph->V1,
- glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize);
+ glyph->AdvanceX * scale);
}
else if (!missingOnly)
{
- prevGlyphPtr->X0 = glyph->X0;
- prevGlyphPtr->Y0 = glyph->Y0;
- prevGlyphPtr->X1 = glyph->X0 + ((glyph->X1 - glyph->X0) * target.Value!.FontSize / source.Value!.FontSize);
- prevGlyphPtr->Y1 = glyph->Y0 + ((glyph->Y1 - glyph->Y0) * target.Value!.FontSize / source.Value!.FontSize);
+ prevGlyphPtr->X0 = glyph->X0 * scale;
+ prevGlyphPtr->Y0 = glyph->Y0 * scale;
+ prevGlyphPtr->X1 = glyph->X1 * scale;
+ prevGlyphPtr->Y1 = glyph->Y1 * scale;
prevGlyphPtr->U0 = glyph->U0;
prevGlyphPtr->V0 = glyph->V0;
prevGlyphPtr->U1 = glyph->U1;
prevGlyphPtr->V1 = glyph->V1;
- prevGlyphPtr->AdvanceX = glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize;
+ prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale;
}
}
}
@@ -165,6 +169,39 @@ namespace Dalamud.Interface.GameFonts
target.Value!.BuildLookupTable();
}
+ ///
+ /// Unscales fonts after they have been rendered onto atlas.
+ ///
+ /// Font to unscale.
+ /// Scale factor.
+ /// Whether to call target.BuildLookupTable().
+ public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true)
+ {
+ unsafe
+ {
+ var font = fontPtr.NativePtr;
+ for (int i = 0, i_ = font->IndexAdvanceX.Size; i < i_; ++i)
+ ((float*)font->IndexAdvanceX.Data)[i] /= fontScale;
+ font->FallbackAdvanceX /= fontScale;
+ font->FontSize /= fontScale;
+ font->Ascent /= fontScale;
+ font->Descent /= fontScale;
+ var glyphs = (ImFontGlyphReal*)font->Glyphs.Data;
+ for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++)
+ {
+ var glyph = &glyphs[i];
+ glyph->X0 /= fontScale;
+ glyph->X1 /= fontScale;
+ glyph->Y0 /= fontScale;
+ glyph->Y1 /= fontScale;
+ glyph->AdvanceX /= fontScale;
+ }
+ }
+
+ if (rebuildLookupTable)
+ fontPtr.BuildLookupTable();
+ }
+
///
public void Dispose()
{
@@ -247,39 +284,48 @@ namespace Dalamud.Interface.GameFonts
///
public void BuildFonts()
{
- var io = ImGui.GetIO();
- io.Fonts.TexDesiredWidth = 4096;
-
- this.glyphRectIds.Clear();
- this.fonts.Clear();
-
- foreach (var style in this.fontUseCounter.Keys)
+ unsafe
{
- var rectIds = this.glyphRectIds[style] = new();
+ ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
+ fontConfig.OversampleH = 1;
+ fontConfig.OversampleV = 1;
+ fontConfig.PixelSnapH = true;
- var fdt = this.fdts[(int)style.FamilyAndSize];
- if (fdt == null)
- continue;
+ var io = ImGui.GetIO();
- var font = io.Fonts.AddFontDefault();
- this.fonts[style] = font;
- foreach (var glyph in fdt.Glyphs)
+ this.glyphRectIds.Clear();
+ this.fonts.Clear();
+
+ foreach (var style in this.fontUseCounter.Keys)
{
- var c = glyph.Char;
- if (c < 32 || c >= 0xFFFF)
+ var rectIds = this.glyphRectIds[style] = new();
+
+ var fdt = this.fdts[(int)style.FamilyAndSize];
+ if (fdt == null)
continue;
- var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph);
- rectIds[c] = Tuple.Create(
- io.Fonts.AddCustomRectFontGlyph(
- font,
- c,
- glyph.BoundingWidth + widthAdjustment + 1,
- glyph.BoundingHeight + 1,
- glyph.AdvanceWidth,
- new Vector2(0, glyph.CurrentOffsetY)),
- glyph);
+ var font = io.Fonts.AddFontDefault(fontConfig);
+ this.fonts[style] = font;
+ foreach (var glyph in fdt.Glyphs)
+ {
+ var c = glyph.Char;
+ if (c < 32 || c >= 0xFFFF)
+ continue;
+
+ var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph);
+ rectIds[c] = Tuple.Create(
+ io.Fonts.AddCustomRectFontGlyph(
+ font,
+ c,
+ glyph.BoundingWidth + widthAdjustment + 1,
+ glyph.BoundingHeight + 1,
+ glyph.AdvanceWidth,
+ new Vector2(0, glyph.CurrentOffsetY)),
+ glyph);
+ }
}
+
+ fontConfig.Destroy();
}
}
@@ -288,9 +334,10 @@ namespace Dalamud.Interface.GameFonts
///
public unsafe void AfterBuildFonts()
{
- var io = ImGui.GetIO();
- io.Fonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height);
+ var ioFonts = ImGui.GetIO().Fonts;
+ ioFonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height);
var pixels32 = (uint*)pixels8;
+ var fontGamma = this.interfaceManager.FontGamma;
foreach (var (style, font) in this.fonts)
{
@@ -319,7 +366,7 @@ namespace Dalamud.Interface.GameFonts
foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style])
{
- var rc = io.Fonts.GetCustomRectByIndex(rectId);
+ var rc = ioFonts.GetCustomRectByIndex(rectId);
var sourceBuffer = this.texturePixels[glyph.TextureFileIndex];
var sourceBufferDelta = glyph.TextureChannelByteIndex;
var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph);
@@ -366,6 +413,19 @@ namespace Dalamud.Interface.GameFonts
}
}
}
+
+ if (Math.Abs(fontGamma - 1.4f) >= 0.001)
+ {
+ // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4)
+ for (int y = rc.Y, y_ = rc.Y + rc.Height; y < y_; y++)
+ {
+ for (int x = rc.X, x_ = rc.X + rc.Width; x < x_; x++)
+ {
+ var i = (((y * width) + x) * 4) + 3;
+ pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f);
+ }
+ }
+ }
}
}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 234f583a3..ee56e749f 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -393,23 +393,22 @@ namespace Dalamud.Interface.Internal
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
+ ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0);
- var windowPos = ImGui.GetMainViewport().Pos + new Vector2(40);
+ var windowPos = ImGui.GetMainViewport().Pos + new Vector2(20);
ImGui.SetNextWindowPos(windowPos, ImGuiCond.Always);
ImGui.SetNextWindowBgAlpha(1);
- var imageSize = new Vector2(90);
-
if (ImGui.Begin("DevMenu Opener", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings))
{
ImGui.SetNextItemWidth(40);
- if (ImGui.Button("###devMenuOpener", imageSize))
+ if (ImGui.Button("###devMenuOpener", new Vector2(20, 20)))
this.isImGuiDrawDevMenu = true;
ImGui.End();
}
- ImGui.PopStyleVar(3);
+ ImGui.PopStyleVar(4);
ImGui.PopStyleColor(8);
}
}
@@ -725,10 +724,14 @@ namespace Dalamud.Interface.Internal
if (Service.Get().GameUiHidden)
ImGui.BeginMenu("UI is hidden...", false);
+ ImGui.PushFont(InterfaceManager.MonoFont);
+
ImGui.BeginMenu(Util.GetGitHash(), false);
- ImGui.BeginMenu(this.frameCount.ToString(), false);
- ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("F2"), false);
- ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))} total managed", false);
+ ImGui.BeginMenu(this.frameCount.ToString("000000"), false);
+ ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false);
+ ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false);
+
+ ImGui.PopFont();
ImGui.EndMainMenuBar();
}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 58f18bf70..a58260c0a 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -47,8 +47,15 @@ namespace Dalamud.Interface.Internal
///
internal class InterfaceManager : IDisposable
{
+ private const float DefaultFontSizePt = 12.0f;
+ private const float DefaultFontSizePx = DefaultFontSizePt * 4.0f / 3.0f;
+ private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing.
+ private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable.
+
private readonly string rtssPath;
+ private readonly HashSet glyphRequests = new();
+
private readonly Hook presentHook;
private readonly Hook resizeBuffersHook;
private readonly Hook setCursorHook;
@@ -57,7 +64,8 @@ namespace Dalamud.Interface.Internal
private readonly SwapChainVtableResolver address;
private RawDX11Scene? scene;
- private GameFontHandle? axisFontHandle;
+ private GameFontHandle[] axisFontHandles;
+ private bool overwriteAllNotoGlyphsWithAxis;
// can't access imgui IO before first present call
private bool lastWantCapture = false;
@@ -189,6 +197,16 @@ namespace Dalamud.Interface.Internal
///
public bool IsReady => this.scene != null;
+ ///
+ /// Gets or sets the overrided font gamma value, instead of using the value from configuration.
+ ///
+ public float? FontGammaOverride { get; set; } = null;
+
+ ///
+ /// Gets the font gamma value to use.
+ ///
+ public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGamma));
+
///
/// Enable this module.
///
@@ -327,6 +345,70 @@ namespace Dalamud.Interface.Internal
this.fontBuildSignal.WaitOne();
}
+ ///
+ /// Requests a default font of specified size to exist.
+ ///
+ /// Font size in pixels.
+ /// Ranges of glyphs.
+ /// Requets handle.
+ public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges)
+ {
+ var allContained = false;
+ var fonts = ImGui.GetIO().Fonts.Fonts;
+ ImFontPtr foundFont = null;
+ unsafe
+ {
+ for (int i = 0, i_ = fonts.Size; i < i_; i++)
+ {
+ if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr))
+ continue;
+
+ allContained = true;
+ foreach (var range in ranges)
+ {
+ if (!allContained)
+ break;
+
+ for (var j = range.Item1; j <= range.Item2 && allContained; j++)
+ allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null;
+ }
+
+ if (allContained)
+ foundFont = fonts[i];
+
+ break;
+ }
+ }
+
+ var req = new SpecialGlyphRequest(this, size, ranges);
+ req.FontInternal = foundFont;
+
+ if (!allContained)
+ this.RebuildFonts();
+
+ return req;
+ }
+
+ ///
+ /// Requests a default font of specified size to exist.
+ ///
+ /// Font size in pixels.
+ /// Text to calculate glyph ranges from.
+ /// Requets handle.
+ public SpecialGlyphRequest NewFontSizeRef(float size, string text)
+ {
+ List> ranges = new();
+ foreach (var c in new SortedSet(text.ToHashSet()))
+ {
+ if (ranges.Any() && ranges[^1].Item2 + 1 == c)
+ ranges[^1] = Tuple.Create(ranges[^1].Item1, c);
+ else
+ ranges.Add(Tuple.Create(c, c));
+ }
+
+ return this.NewFontSizeRef(size, ranges);
+ }
+
private static void ShowFontError(string path)
{
Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error");
@@ -335,20 +417,18 @@ namespace Dalamud.Interface.Internal
private void SetAxisFonts()
{
var configuration = Service.Get();
- if (configuration.UseAxisFontsFromGame)
- {
- var currentFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, this.axisFontHandle?.Style.Size ?? 0f);
- var expectedFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, 12 * ImGui.GetIO().FontGlobalScale);
- if (currentFamilyAndSize == expectedFamilyAndSize)
- return;
+ this.overwriteAllNotoGlyphsWithAxis = configuration.UseAxisFontsFromGame;
- this.axisFontHandle?.Dispose();
- this.axisFontHandle = Service.Get().NewFontRef(new(expectedFamilyAndSize));
- }
- else
+ if (this.axisFontHandles == null)
{
- this.axisFontHandle?.Dispose();
- this.axisFontHandle = null;
+ this.axisFontHandles = new GameFontHandle[]
+ {
+ Service.Get().NewFontRef(new(GameFontFamilyAndSize.Axis96)),
+ Service.Get().NewFontRef(new(GameFontFamilyAndSize.Axis12)),
+ Service.Get().NewFontRef(new(GameFontFamilyAndSize.Axis14)),
+ Service.Get().NewFontRef(new(GameFontFamilyAndSize.Axis18)),
+ Service.Get().NewFontRef(new(GameFontFamilyAndSize.Axis36)),
+ };
}
}
@@ -521,62 +601,108 @@ namespace Dalamud.Interface.Internal
private unsafe void SetupFonts()
{
var dalamud = Service.Get();
+ var io = ImGui.GetIO();
+ var ioFonts = io.Fonts;
+ var fontScale = io.FontGlobalScale;
+ var fontGamma = this.FontGamma;
+ List fontsToUnscale = new();
this.fontBuildSignal.Reset();
-
- ImGui.GetIO().Fonts.Clear();
+ ioFonts.Clear();
+ ioFonts.TexDesiredWidth = 4096;
ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
+ fontConfig.OversampleH = 1;
+ fontConfig.OversampleV = 1;
fontConfig.PixelSnapH = true;
var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf");
-
if (!File.Exists(fontPathJp))
ShowFontError(fontPathJp);
- var japaneseRangeHandle = GCHandle.Alloc(GlyphRangesJapanese.GlyphRanges, GCHandleType.Pinned);
+ // Default font
+ {
+ var japaneseRangeHandle = GCHandle.Alloc(GlyphRangesJapanese.GlyphRanges, GCHandleType.Pinned);
+ DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, (DefaultFontSizePx + 1) * fontScale, fontConfig, japaneseRangeHandle.AddrOfPinnedObject());
+ japaneseRangeHandle.Free();
+ fontsToUnscale.Add(DefaultFont);
+ }
- DefaultFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathJp, 17.0f, null, japaneseRangeHandle.AddrOfPinnedObject());
+ // FontAwesome icon font
+ {
+ var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesome5FreeSolid.otf");
+ if (!File.Exists(fontPathIcon))
+ ShowFontError(fontPathIcon);
- var fontPathGame = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "gamesym.ttf");
+ var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned);
+ IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * fontScale, fontConfig, iconRangeHandle.AddrOfPinnedObject());
+ iconRangeHandle.Free();
+ fontsToUnscale.Add(IconFont);
+ }
- if (!File.Exists(fontPathGame))
- ShowFontError(fontPathGame);
+ // Monospace font
+ {
+ var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf");
+ if (!File.Exists(fontPathMono))
+ ShowFontError(fontPathMono);
+ MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * fontScale, fontConfig);
+ fontsToUnscale.Add(MonoFont);
+ }
- var gameRangeHandle = GCHandle.Alloc(
- new ushort[]
+ // Default font but in requested size for requested glyphs
+ {
+ Dictionary> extraFontRequests = new();
+ foreach (var extraFontRequest in this.glyphRequests)
{
- 0xE020,
- 0xE0DB,
- 0,
- },
- GCHandleType.Pinned);
+ if (!extraFontRequests.ContainsKey(extraFontRequest.Size))
+ extraFontRequests[extraFontRequest.Size] = new();
+ extraFontRequests[extraFontRequest.Size].Add(extraFontRequest);
+ }
- fontConfig.MergeMode = false;
- ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathGame, 17.0f, fontConfig, gameRangeHandle.AddrOfPinnedObject());
- fontConfig.MergeMode = true;
-
- var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesome5FreeSolid.otf");
-
- if (!File.Exists(fontPathIcon))
- ShowFontError(fontPathIcon);
-
- var iconRangeHandle = GCHandle.Alloc(
- new ushort[]
+ foreach (var (fontSize, requests) in extraFontRequests)
{
- 0xE000,
- 0xF8FF,
- 0,
- },
- GCHandleType.Pinned);
- IconFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathIcon, 17.0f, null, iconRangeHandle.AddrOfPinnedObject());
+ List> codepointRanges = new();
+ codepointRanges.Add(Tuple.Create(Fallback1Codepoint, Fallback1Codepoint));
+ codepointRanges.Add(Tuple.Create(Fallback2Codepoint, Fallback2Codepoint));
- var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf");
+ // ImGui default ellipsis characters
+ codepointRanges.Add(Tuple.Create(0x2026, 0x2026));
+ codepointRanges.Add(Tuple.Create(0x0085, 0x0085));
- if (!File.Exists(fontPathMono))
- ShowFontError(fontPathMono);
+ foreach (var request in requests)
+ {
+ foreach (var range in request.CodepointRanges)
+ codepointRanges.Add(range);
+ }
- MonoFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathMono, 16.0f);
+ codepointRanges.Sort((x, y) => (x.Item1 == y.Item1 ? (x.Item2 < y.Item2 ? -1 : (x.Item2 == y.Item2 ? 0 : 1)) : (x.Item1 < y.Item1 ? -1 : 1)));
+
+ List flattenedRanges = new();
+ foreach (var range in codepointRanges)
+ {
+ if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1)
+ {
+ flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2);
+ }
+ else
+ {
+ flattenedRanges.Add(range.Item1);
+ flattenedRanges.Add(range.Item2);
+ }
+ }
+
+ flattenedRanges.Add(0);
+
+ var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned);
+ var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * fontScale, fontConfig, rangeHandle.AddrOfPinnedObject());
+ rangeHandle.Free();
+
+ fontsToUnscale.Add(sizedFont);
+
+ foreach (var request in requests)
+ request.FontInternal = sizedFont;
+ }
+ }
var gameFontManager = Service.Get();
gameFontManager.BuildFonts();
@@ -590,10 +716,58 @@ namespace Dalamud.Interface.Internal
Log.Verbose("{0} - {1}", i, ImGui.GetIO().Fonts.Fonts[i].GetDebugName());
}
- ImGui.GetIO().Fonts.Build();
+ ioFonts.Build();
+
+ if (Math.Abs(fontGamma - 1.0f) >= 0.001)
+ {
+ // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4)
+ ioFonts.GetTexDataAsRGBA32(out byte* texPixels, out var texWidth, out var texHeight);
+ for (int i = 3, i_ = texWidth * texHeight * 4; i < i_; i += 4)
+ texPixels[i] = (byte)(Math.Pow(texPixels[i] / 255.0f, 1.0f / fontGamma) * 255.0f);
+ }
+
+ foreach (var font in fontsToUnscale)
+ GameFontManager.UnscaleFont(font, fontScale, false);
gameFontManager.AfterBuildFonts();
- GameFontManager.CopyGlyphsAcrossFonts(this.axisFontHandle?.ImFont, DefaultFont, false, true);
+
+ foreach (var font in fontsToUnscale)
+ {
+ // Leave IconFont alone.
+ if (font.NativePtr == IconFont.NativePtr)
+ continue;
+
+ // MonoFont will be filled later from DefaultFont.
+ if (font.NativePtr == MonoFont.NativePtr)
+ continue;
+
+ var axisFont = this.axisFontHandles[^1];
+ for (var i = this.axisFontHandles.Length - 2; i >= 0; i--)
+ {
+ if (this.axisFontHandles[i].Style.Size >= (font.FontSize - 1) * fontScale * 3 / 4)
+ axisFont = this.axisFontHandles[i];
+ else
+ break;
+ }
+
+ if (this.overwriteAllNotoGlyphsWithAxis)
+ GameFontManager.CopyGlyphsAcrossFonts(axisFont.ImFont, font, false, false);
+ else
+ GameFontManager.CopyGlyphsAcrossFonts(axisFont.ImFont, font, false, false, 0xE020, 0xE0DB);
+
+ // Fill missing glyphs in DefaultFont from Axis
+ if (font.NativePtr == DefaultFont.NativePtr)
+ GameFontManager.CopyGlyphsAcrossFonts(axisFont.ImFont, DefaultFont, true, false);
+ }
+
+ // Fill missing glyphs in MonoFont from DefaultFont
+ GameFontManager.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false);
+
+ foreach (var font in fontsToUnscale)
+ {
+ font.FallbackChar = Fallback1Codepoint;
+ font.BuildLookupTable();
+ }
Log.Verbose("[FONT] Invoke OnAfterBuildFonts");
this.AfterBuildFonts?.Invoke();
@@ -601,12 +775,8 @@ namespace Dalamud.Interface.Internal
Log.Verbose("[FONT] Fonts built!");
- this.fontBuildSignal.Set();
-
fontConfig.Destroy();
- japaneseRangeHandle.Free();
- gameRangeHandle.Free();
- iconRangeHandle.Free();
+ this.fontBuildSignal.Set();
this.FontsReady = true;
}
@@ -745,5 +915,65 @@ namespace Dalamud.Interface.Internal
Service.Get().Draw();
}
+
+ ///
+ /// Represents a glyph request.
+ ///
+ public class SpecialGlyphRequest : IDisposable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// InterfaceManager to associate.
+ /// Font size in pixels.
+ /// Codepoint ranges.
+ internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges)
+ {
+ this.Manager = manager;
+ this.Size = size;
+ this.CodepointRanges = ranges;
+ this.Manager.glyphRequests.Add(this);
+ }
+
+ ///
+ /// Gets the font of specified size, or DefaultFont if it's not ready yet.
+ ///
+ public ImFontPtr Font
+ {
+ get
+ {
+ unsafe
+ {
+ return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the associated ImFont.
+ ///
+ internal ImFontPtr FontInternal { get; set; }
+
+ ///
+ /// Gets associated InterfaceManager.
+ ///
+ internal InterfaceManager Manager { get; init; }
+
+ ///
+ /// Gets font size.
+ ///
+ internal float Size { get; init; }
+
+ ///
+ /// Gets codepoint ranges.
+ ///
+ internal List> CodepointRanges { get; init; }
+
+ ///
+ public void Dispose()
+ {
+ this.Manager.glyphRequests.Remove(this);
+ }
+ }
}
}
diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
index ac01392a7..686839d0f 100644
--- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
@@ -26,7 +26,7 @@ namespace Dalamud.Interface.Internal.Windows
internal class SettingsWindow : Window
{
private const float MinScale = 0.3f;
- private const float MaxScale = 2.0f;
+ private const float MaxScale = 3.0f;
private readonly string[] languages;
private readonly string[] locLanguages;
@@ -39,6 +39,7 @@ namespace Dalamud.Interface.Internal.Windows
private float globalUiScale;
private bool doUseAxisFontsFromGame;
+ private float fontGamma;
private bool doToggleUiHide;
private bool doToggleUiHideDuringCutscenes;
private bool doToggleUiHideDuringGpose;
@@ -91,6 +92,7 @@ namespace Dalamud.Interface.Internal.Windows
this.doCfChatMessage = configuration.DutyFinderChatMessage;
this.globalUiScale = configuration.GlobalUiScale;
+ this.fontGamma = configuration.FontGamma;
this.doUseAxisFontsFromGame = configuration.UseAxisFontsFromGame;
this.doToggleUiHide = configuration.ToggleUiHide;
this.doToggleUiHideDuringCutscenes = configuration.ToggleUiHideDuringCutscenes;
@@ -176,13 +178,20 @@ namespace Dalamud.Interface.Internal.Windows
public override void OnClose()
{
var configuration = Service.Get();
+ var interfaceManager = Service.Get();
+
+ var rebuildFont = interfaceManager.FontGamma != configuration.FontGamma;
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
+ interfaceManager.FontGammaOverride = null;
this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
this.devPluginLocations = configuration.DevPluginLoadLocations.Select(x => x.Clone()).ToList();
configuration.DtrOrder = this.dtrOrder;
configuration.DtrIgnore = this.dtrIgnore;
+
+ if (rebuildFont)
+ interfaceManager.RebuildFonts();
}
///
@@ -193,25 +202,25 @@ namespace Dalamud.Interface.Internal.Windows
if (ImGui.BeginTabBar("SetTabBar"))
{
- if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsGeneral", "General")))
+ if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsGeneral", "General") + "###settingsTabGeneral"))
{
this.DrawGeneralTab();
ImGui.EndTabItem();
}
- if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsVisual", "Look & Feel")))
+ if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsVisual", "Look & Feel") + "###settingsTabVisual"))
{
this.DrawLookAndFeelTab();
ImGui.EndTabItem();
}
- if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsServerInfoBar", "Server Info Bar")))
+ if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsServerInfoBar", "Server Info Bar") + "###settingsTabInfoBar"))
{
this.DrawServerInfoBarTab();
ImGui.EndTabItem();
}
- if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsExperimental", "Experimental")))
+ if (ImGui.BeginTabItem(Loc.Localize("DalamudSettingsExperimental", "Experimental") + "###settingsTabExperimental"))
{
this.DrawExperimentalTab();
ImGui.EndTabItem();
@@ -272,21 +281,23 @@ namespace Dalamud.Interface.Internal.Windows
private void DrawLookAndFeelTab()
{
+ var interfaceManager = Service.Get();
+
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global UI Scale"));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3);
- if (ImGui.Button("Reset"))
+ if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsGlobalUiScaleReset"))
{
this.globalUiScale = 1.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
- Service.Get().RebuildFonts();
+ interfaceManager.RebuildFonts();
}
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f"))
{
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
- Service.Get().RebuildFonts();
+ interfaceManager.RebuildFonts();
}
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays."));
@@ -332,6 +343,28 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleTsm", "Show title screen menu"), ref this.doTsm);
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen."));
+
+ ImGuiHelpers.ScaledDummy(10, 16);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
+ ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma"));
+ ImGui.SameLine();
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3);
+ if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset"))
+ {
+ this.fontGamma = 1.4f;
+ interfaceManager.FontGammaOverride = this.fontGamma;
+ interfaceManager.RebuildFonts();
+ }
+
+ if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, MinScale, MaxScale, "%.2f"))
+ {
+ interfaceManager.FontGammaOverride = this.fontGamma;
+ interfaceManager.RebuildFonts();
+ }
+
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text."));
+
+ ImGuiHelpers.ScaledDummy(10, 16);
}
private void DrawServerInfoBarTab()
@@ -811,6 +844,7 @@ namespace Dalamud.Interface.Internal.Windows
configuration.ShowTsm = this.doTsm;
configuration.UseAxisFontsFromGame = this.doUseAxisFontsFromGame;
+ configuration.FontGamma = this.fontGamma;
// This is applied every frame in InterfaceManager::CheckViewportState()
configuration.IsDisableViewport = !this.doViewport;
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index b4c089fbe..40bb29672 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Numerics;
using Dalamud.Configuration.Internal;
@@ -20,17 +21,18 @@ namespace Dalamud.Interface.Internal.Windows
///
internal class TitleScreenMenuWindow : Window, IDisposable
{
- private const float TargetFontSize = 16.2f;
+ private const float TargetFontSizePt = 18f;
+ private const float TargetFontSizePx = TargetFontSizePt * 4 / 3;
+
private readonly TextureWrap shadeTexture;
private readonly Dictionary shadeEasings = new();
private readonly Dictionary moveEasings = new();
private readonly Dictionary logoEasings = new();
+ private readonly Dictionary specialGlyphRequests = new();
private InOutCubic? fadeOutEasing;
- private GameFontHandle? axisFontHandle;
-
private State state = State.Hide;
///
@@ -71,19 +73,14 @@ namespace Dalamud.Interface.Internal.Windows
///
public override void PreDraw()
{
- this.SetAxisFonts();
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
- if (this.axisFontHandle?.Available ?? false)
- ImGui.PushFont(this.axisFontHandle.ImFont);
base.PreDraw();
}
///
public override void PostDraw()
{
- if (this.axisFontHandle?.Available ?? false)
- ImGui.PopFont();
ImGui.PopStyleVar(2);
base.PostDraw();
}
@@ -99,7 +96,7 @@ namespace Dalamud.Interface.Internal.Windows
///
public override void Draw()
{
- ImGui.SetWindowFontScale(TargetFontSize / ImGui.GetFont().FontSize * 4 / 3);
+ var scale = ImGui.GetIO().FontGlobalScale;
var tsm = Service.Get();
@@ -129,7 +126,7 @@ namespace Dalamud.Interface.Internal.Windows
moveEasing.Update();
- var finalPos = (i + 1) * this.shadeTexture.Height;
+ var finalPos = (i + 1) * this.shadeTexture.Height * scale;
var pos = moveEasing.Value * finalPos;
// FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment.
@@ -180,7 +177,7 @@ namespace Dalamud.Interface.Internal.Windows
{
var entry = tsm.Entries[i];
- var finalPos = (i + 1) * this.shadeTexture.Height;
+ var finalPos = (i + 1) * this.shadeTexture.Height * scale;
this.DrawEntry(entry, i != 0, true, i == 0, false);
@@ -222,26 +219,36 @@ namespace Dalamud.Interface.Internal.Windows
break;
}
}
- }
- private void SetAxisFonts()
- {
- var configuration = Service.Get();
- if (configuration.UseAxisFontsFromGame)
+ var srcText = tsm.Entries.Select(e => e.Name).ToHashSet();
+ var keys = this.specialGlyphRequests.Keys.ToHashSet();
+ keys.RemoveWhere(x => srcText.Contains(x));
+ foreach (var key in keys)
{
- if (this.axisFontHandle == null)
- this.axisFontHandle = Service.Get().NewFontRef(new(GameFontFamily.Axis, TargetFontSize));
- }
- else
- {
- this.axisFontHandle?.Dispose();
- this.axisFontHandle = null;
+ this.specialGlyphRequests[key].Dispose();
+ this.specialGlyphRequests.Remove(key);
}
}
private bool DrawEntry(
TitleScreenMenu.TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha)
{
+ InterfaceManager.SpecialGlyphRequest fontHandle;
+ if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx)
+ {
+ fontHandle.Dispose();
+ this.specialGlyphRequests.Remove(entry.Name);
+ fontHandle = null;
+ }
+
+ if (fontHandle == null)
+ this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name);
+
+ ImGui.PushFont(fontHandle.Font);
+ ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size);
+
+ var scale = ImGui.GetIO().FontGlobalScale;
+
if (!this.shadeEasings.TryGetValue(entry.Id, out var shadeEasing))
{
shadeEasing = new InOutCubic(TimeSpan.FromMilliseconds(350));
@@ -251,7 +258,7 @@ namespace Dalamud.Interface.Internal.Windows
var initialCursor = ImGui.GetCursorPos();
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)shadeEasing.Value);
- ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width, this.shadeTexture.Height));
+ ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width * scale, this.shadeTexture.Height * scale));
ImGui.PopStyleVar();
var isHover = ImGui.IsItemHovered();
@@ -305,7 +312,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
}
- ImGui.Image(entry.Texture.ImGuiHandle, new Vector2(TitleScreenMenu.TextureSize));
+ ImGui.Image(entry.Texture.ImGuiHandle, new Vector2(TitleScreenMenu.TextureSize * scale));
if (overrideAlpha || isFirst)
{
ImGui.PopStyleVar();
@@ -319,23 +326,36 @@ namespace Dalamud.Interface.Internal.Windows
var textHeight = ImGui.GetTextLineHeightWithSpacing();
var cursor = ImGui.GetCursorPos();
- cursor.Y += (entry.Texture.Height / 2) - (textHeight / 2);
- ImGui.SetCursorPos(cursor);
+ cursor.Y += (entry.Texture.Height * scale / 2) - (textHeight / 2);
if (overrideAlpha)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f);
}
+ // Drop shadow
+ ImGui.PushStyleColor(ImGuiCol.Text, 0xFF000000);
+ for (int i = 0, i_ = (int)Math.Ceiling(1 * scale); i < i_; i++)
+ {
+ ImGui.SetCursorPos(new Vector2(cursor.X, cursor.Y + i));
+ ImGui.Text(entry.Name);
+ }
+
+ ImGui.PopStyleColor();
+
+ ImGui.SetCursorPos(cursor);
ImGui.Text(entry.Name);
+
if (overrideAlpha)
{
ImGui.PopStyleVar();
}
- initialCursor.Y += entry.Texture.Height;
+ initialCursor.Y += entry.Texture.Height * scale;
ImGui.SetCursorPos(initialCursor);
+ ImGui.PopFont();
+
return isHover;
}