From badc73c3aac68d2ff67c0eb829ecb17e7d4b07c7 Mon Sep 17 00:00:00 2001 From: liam Date: Sat, 5 Feb 2022 01:29:14 -0500 Subject: [PATCH 01/22] fix(DtrBar): fix ordering --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index f85bf1ddf..7ba210179 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -48,10 +48,14 @@ namespace Dalamud.Game.Gui.Dtr if (this.entries.Any(x => x.Title == title)) throw new ArgumentException("An entry with the same title already exists."); + var configuration = Service.Get(); var node = this.MakeNode(++this.runningNodeIds); var entry = new DtrBarEntry(title, node); entry.Text = text; + // Add the entry to the end of the order list, if it's not there already. + if (!configuration.DtrOrder!.Contains(title)) + configuration.DtrOrder!.Add(title); this.entries.Add(entry); this.ApplySort(); @@ -98,22 +102,16 @@ namespace Dalamud.Game.Gui.Dtr var configuration = Service.Get(); // Sort the current entry list, based on the order in the configuration. - var ordered = configuration.DtrOrder.Select(entry => this.entries.FirstOrDefault(x => x.Title == entry)).Where(value => value != null).ToList(); + var positions = configuration.DtrOrder! + .Select(entry => (entry, index: configuration.DtrOrder!.IndexOf(entry))) + .ToDictionary(x => x.entry, x => x.index); - // Add entries that weren't sorted to the end of the list. - if (ordered.Count != this.entries.Count) + this.entries.Sort((x, y) => { - ordered.AddRange(this.entries.Where(x => ordered.All(y => y.Title != x.Title))); - } - - // Update the order list for new entries. - configuration.DtrOrder.Clear(); - foreach (var dtrEntry in ordered) - { - configuration.DtrOrder.Add(dtrEntry.Title); - } - - this.entries = ordered; + var xPos = positions.TryGetValue(x.Title, out var xIndex) ? xIndex : int.MaxValue; + var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue; + return xPos.CompareTo(yPos); + }); } private static AtkUnitBase* GetDtr() => (AtkUnitBase*)Service.Get().GetAddonByName("_DTR", 1).ToPointer(); From bab7f33e969d9ab5d4e11bca822a3cc53ffef04e Mon Sep 17 00:00:00 2001 From: liam Date: Mon, 7 Feb 2022 00:09:38 -0500 Subject: [PATCH 02/22] fix(DtrBar): fix relogging --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 7ba210179..658a46a95 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -19,8 +19,11 @@ namespace Dalamud.Game.Gui.Dtr [InterfaceVersion("1.0")] public sealed unsafe class DtrBar : IDisposable { + private const uint BaseNodeCount = 17; + private const uint BaseNodeId = 1000; + private List entries = new(); - private uint runningNodeIds = 1000; + private uint runningNodeIds = BaseNodeId; /// /// Initializes a new instance of the class. @@ -130,6 +133,13 @@ namespace Dalamud.Game.Gui.Dtr // The collision node on the DTR element is always the width of its content if (dtr->UldManager.NodeList == null) return; + + // If we have an unmodified DTR but still have entries, we need to + // work to reset our state. + var nodeCount = dtr->UldManager.NodeListCount; + if (nodeCount == BaseNodeCount) + this.Reset(); + var collisionNode = dtr->UldManager.NodeList[1]; if (collisionNode == null) return; var runningXPos = collisionNode->X; @@ -176,6 +186,16 @@ namespace Dalamud.Game.Gui.Dtr } } + private void Reset() + { + this.runningNodeIds = BaseNodeId; + foreach (var entry in this.entries) + { + entry.TextNode = this.MakeNode(++this.runningNodeIds); + entry.Added = false; + } + } + private bool AddNode(AtkTextNode* node) { var dtr = GetDtr(); From f6206bc91316d852d6bfc63ab2b4c20e8dddec4b Mon Sep 17 00:00:00 2001 From: liam Date: Mon, 7 Feb 2022 00:10:34 -0500 Subject: [PATCH 03/22] feat(DtrBar): allow for swapping draw direction --- .../Internal/DalamudConfiguration.cs | 9 ++++++++ Dalamud/Game/Gui/Dtr/DtrBar.cs | 21 +++++++++++++++---- .../Internal/Windows/SettingsWindow.cs | 7 +++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 4405a11a7..3d2b58683 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -261,6 +261,15 @@ namespace Dalamud.Configuration.Internal /// public int DtrSpacing { get; set; } = 10; + /// + /// Gets or sets a value indicating whether to swap the + /// direction in which elements are drawn in the DTR. + /// False indicates that elements will be drawn from the end of + /// the left side of the Server Info bar, and continue leftwards. + /// True indicates the opposite. + /// + public bool DtrSwapDirection { get; set; } = false; + /// /// Gets or sets a value indicating whether the title screen menu is shown. /// diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 658a46a95..85b1ece65 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -142,10 +142,13 @@ namespace Dalamud.Game.Gui.Dtr var collisionNode = dtr->UldManager.NodeList[1]; if (collisionNode == null) return; - var runningXPos = collisionNode->X; var configuration = Service.Get(); + // If we are drawing backwards, we should start from the right side of the collision node. That is, + // collisionNode->X + collisionNode->Width. + var runningXPos = configuration.DtrSwapDirection ? collisionNode->X + collisionNode->Width : collisionNode->X; + for (var i = 0; i < this.entries.Count; i++) { var data = this.entries[i]; @@ -178,8 +181,18 @@ namespace Dalamud.Game.Gui.Dtr if (!isHide) { - runningXPos -= data.TextNode->AtkResNode.Width + configuration.DtrSpacing; - data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + var elementWidth = data.TextNode->AtkResNode.Width + configuration.DtrSpacing; + + if (configuration.DtrSwapDirection) + { + data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + runningXPos += elementWidth; + } + else + { + runningXPos -= elementWidth; + data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + } } this.entries[i] = data; diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index e4c6281da..d02dae56d 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -49,6 +49,7 @@ namespace Dalamud.Interface.Internal.Windows private List? dtrOrder; private List? dtrIgnore; private int dtrSpacing; + private bool dtrSwapDirection; private List thirdRepoList; private bool thirdRepoListChanged; @@ -99,6 +100,7 @@ namespace Dalamud.Interface.Internal.Windows this.doTsm = configuration.ShowTsm; this.dtrSpacing = configuration.DtrSpacing; + this.dtrSwapDirection = configuration.DtrSwapDirection; this.doPluginTest = configuration.DoPluginTest; this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); @@ -416,6 +418,10 @@ namespace Dalamud.Interface.Internal.Windows ImGui.Text(Loc.Localize("DalamudSettingServerInfoBarSpacing", "Server Info Bar spacing")); ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarSpacingHint", "Configure the amount of space between entries in the server info bar here.")); ImGui.SliderInt("Spacing", ref this.dtrSpacing, 0, 40); + + ImGui.Text(Loc.Localize("DalamudSettingServerInfoBarDirection", "Server Info Bar direction")); + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDirectionHint", "If checked, the Server Info Bar elements will expand to the right instead of the left.")); + ImGui.Checkbox("Swap Direction", ref this.dtrSwapDirection); } private void DrawExperimentalTab() @@ -823,6 +829,7 @@ namespace Dalamud.Interface.Internal.Windows this.dtrIgnore = configuration.DtrIgnore; configuration.DtrSpacing = this.dtrSpacing; + configuration.DtrSwapDirection = this.dtrSwapDirection; configuration.DoPluginTest = this.doPluginTest; configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList(); From 008e83e2f4bba2fcd12464e1184cf816d93846ee Mon Sep 17 00:00:00 2001 From: liam Date: Mon, 7 Feb 2022 00:22:40 -0500 Subject: [PATCH 04/22] fix(DtrBar): clearer names, no longer assumes static node list size --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 85b1ece65..406cc48d2 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -19,7 +19,6 @@ namespace Dalamud.Game.Gui.Dtr [InterfaceVersion("1.0")] public sealed unsafe class DtrBar : IDisposable { - private const uint BaseNodeCount = 17; private const uint BaseNodeId = 1000; private List entries = new(); @@ -136,9 +135,8 @@ namespace Dalamud.Game.Gui.Dtr // If we have an unmodified DTR but still have entries, we need to // work to reset our state. - var nodeCount = dtr->UldManager.NodeListCount; - if (nodeCount == BaseNodeCount) - this.Reset(); + if (!this.CheckForDalamudNodes()) + this.RecreateNodes(); var collisionNode = dtr->UldManager.NodeList[1]; if (collisionNode == null) return; @@ -199,7 +197,25 @@ namespace Dalamud.Game.Gui.Dtr } } - private void Reset() + /// + /// Checks if there are any Dalamud nodes in the DTR. + /// + /// True if there are nodes with an ID > 1000. + private bool CheckForDalamudNodes() + { + var dtr = GetDtr(); + if (dtr == null || dtr->RootNode == null) return false; + + for (int i = 0; i < dtr->UldManager.NodeListCount; i++) + { + if (dtr->UldManager.NodeList[i]->NodeID > 1000) + return true; + } + + return false; + } + + private void RecreateNodes() { this.runningNodeIds = BaseNodeId; foreach (var entry in this.entries) From 53489fd810309c5f017419386c3d1f82cb496cb0 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 8 Feb 2022 00:20:48 +0100 Subject: [PATCH 05/22] build: 6.3.0.3 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index bd8c971b1..89bd72417 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 6.3.0.2 + 6.3.0.3 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 948b2e6f7bc5237a607e73923e701dcc90a66899 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 13 Feb 2022 04:12:54 +0100 Subject: [PATCH 06/22] deps: update Nuke.Common --- build/build.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/build.csproj b/build/build.csproj index a9b6c7ef0..270ea0a0f 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -7,8 +7,9 @@ IDE0002;IDE0051;IDE1006;CS0649;CS0169 .. .. + 1 - + From 9156f85ffff4878fb69687c6835779d5aff89d97 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 13 Feb 2022 04:46:30 +0100 Subject: [PATCH 07/22] ci: run on windows-latest --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9c1af8f4..1982ea238 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,7 @@ concurrency: build_dalamud jobs: build: name: Build on Windows - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Checkout Dalamud uses: actions/checkout@v2 From 8c66acf95007cf33508e2ed29bf386374b74aea9 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 13 Feb 2022 04:49:33 +0100 Subject: [PATCH 08/22] chore: language level 10 for all projects --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 +- Dalamud.Injector/Dalamud.Injector.csproj | 2 +- Dalamud/Dalamud.csproj | 2 +- .../Internal/Windows/PluginImageCache.cs | 34 +++++++++---------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index f1ce7bac9..3b7556af7 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -3,7 +3,7 @@ Dalamud.CorePlugin net5.0-windows x64 - 9.0 + 10.0 true false false diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index d9ce042a6..dbe4c94da 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -5,7 +5,7 @@ win-x64 x64 x64;AnyCPU - 9.0 + 10.0 diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 89bd72417..93fe54d24 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -4,7 +4,7 @@ net5.0-windows x64 x64;AnyCPU - 9.0 + 10.0 diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index d61ba8638..684ffc439 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; + using Dalamud.Game; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; @@ -80,21 +81,6 @@ namespace Dalamud.Interface.Internal.Windows framework.Update += this.FrameworkOnUpdate; } - private void FrameworkOnUpdate(Framework framework) - { - try - { - if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token)) - return; - - loadAction.Invoke(); - } - catch (Exception ex) - { - Log.Error(ex, "An unhandled exception occurred in image loader framework dispatcher"); - } - } - /// /// Gets the default plugin icon. /// @@ -231,6 +217,21 @@ namespace Dalamud.Interface.Internal.Windows return false; } + private void FrameworkOnUpdate(Framework framework) + { + try + { + if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token)) + return; + + loadAction.Invoke(); + } + catch (Exception ex) + { + Log.Error(ex, "An unhandled exception occurred in image loader framework dispatcher"); + } + } + private async void DownloadTask() { while (!this.downloadToken.Token.IsCancellationRequested) @@ -482,12 +483,9 @@ namespace Dalamud.Interface.Internal.Windows var bytes = await data.Content.ReadAsByteArrayAsync(); imageBytes[i] = bytes; - - Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} downloaded"); didAny = true; - } if (didAny) From aac5cdfef72bf78436b9a43d5c4307d5c6feda46 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 13 Feb 2022 04:52:50 +0100 Subject: [PATCH 09/22] ci: explicitly use windows-2022 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1982ea238..70c0b33ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,7 @@ concurrency: build_dalamud jobs: build: name: Build on Windows - runs-on: windows-latest + runs-on: windows-2022 steps: - name: Checkout Dalamud uses: actions/checkout@v2 From 5cb693e834053025158d8e33699452c47bd34f96 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 14 Feb 2022 01:39:25 +0100 Subject: [PATCH 10/22] fix: handle DTR node removals when reloading plugins (fixes #759) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 22 +++++++++++++++------- Dalamud/Plugin/Internal/PluginManager.cs | 6 +++++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 406cc48d2..9fcf0de6e 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -74,6 +74,19 @@ namespace Dalamud.Game.Gui.Dtr Service.Get().Update -= this.Update; } + /// + /// Remove nodes marked as "should be removed" from the bar. + /// + internal void HandleRemovedNodes() + { + foreach (var data in this.entries.Where(d => d.ShouldBeRemoved)) + { + this.RemoveNode(data.TextNode); + } + + this.entries.RemoveAll(d => d.ShouldBeRemoved); + } + /// /// Check whether an entry with the specified title exists. /// @@ -123,12 +136,7 @@ namespace Dalamud.Game.Gui.Dtr var dtr = GetDtr(); if (dtr == null) return; - foreach (var data in this.entries.Where(d => d.ShouldBeRemoved)) - { - this.RemoveNode(data.TextNode); - } - - this.entries.RemoveAll(d => d.ShouldBeRemoved); + this.HandleRemovedNodes(); // The collision node on the DTR element is always the width of its content if (dtr->UldManager.NodeList == null) return; @@ -206,7 +214,7 @@ namespace Dalamud.Game.Gui.Dtr var dtr = GetDtr(); if (dtr == null || dtr->RootNode == null) return false; - for (int i = 0; i < dtr->UldManager.NodeListCount; i++) + for (var i = 0; i < dtr->UldManager.NodeListCount; i++) { if (dtr->UldManager.NodeList[i]->NodeID > 1000) return true; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index dc751c448..e1ac35d11 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -14,6 +13,7 @@ using CheapLoc; using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; +using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; @@ -790,6 +790,10 @@ namespace Dalamud.Plugin.Internal } } + // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. + var dtr = Service.Get(); + dtr.HandleRemovedNodes(); + try { await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update); From 08b5739369700fba64e9f6ced0d946e931dabccf Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 14 Feb 2022 01:52:50 +0100 Subject: [PATCH 11/22] fix: always run HandleRemoveNodes, even if DTR element doesn't exist --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 9fcf0de6e..fce98c8d0 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -133,11 +133,11 @@ namespace Dalamud.Game.Gui.Dtr private void Update(Framework unused) { + this.HandleRemovedNodes(); + var dtr = GetDtr(); if (dtr == null) return; - this.HandleRemovedNodes(); - // The collision node on the DTR element is always the width of its content if (dtr->UldManager.NodeList == null) return; @@ -236,7 +236,7 @@ namespace Dalamud.Game.Gui.Dtr private bool AddNode(AtkTextNode* node) { var dtr = GetDtr(); - if (dtr == null || dtr->RootNode == null || node == null) return false; + if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; @@ -256,7 +256,7 @@ namespace Dalamud.Game.Gui.Dtr private bool RemoveNode(AtkTextNode* node) { var dtr = GetDtr(); - if (dtr == null || dtr->RootNode == null || node == null) return false; + if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; From e62984d270ab66e0e84ee3b90628370fca9ebf25 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 14 Feb 2022 01:53:26 +0100 Subject: [PATCH 12/22] fix: run HandleRemovedNodes inbetween devplugin reloads --- Dalamud/Plugin/Internal/LocalPlugin.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs index f6372ecfd..ba6297103 100644 --- a/Dalamud/Plugin/Internal/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/LocalPlugin.cs @@ -5,6 +5,7 @@ using System.Reflection; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.Game.Gui.Dtr; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; @@ -416,6 +417,11 @@ namespace Dalamud.Plugin.Internal public void Reload() { this.Unload(true); + + // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. + var dtr = Service.Get(); + dtr.HandleRemovedNodes(); + this.Load(PluginLoadReason.Reload, true); } From 7864570a56f702db9a6f475396660d80202b5b26 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 14 Feb 2022 04:56:27 +0100 Subject: [PATCH 13/22] fix: allow null in CallGateChannel args --- .../Ipc/Exceptions/IpcValueNullError.cs | 21 +++++++++++++++++++ .../Plugin/Ipc/Internal/CallGateChannel.cs | 10 ++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs diff --git a/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs new file mode 100644 index 000000000..04d5550a9 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a null value is passed to an IPC requiring a value type. +/// +public class IpcValueNullError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Name of the IPC. + /// The type expected. + /// Index of the failing argument. + public IpcValueNullError(string name, Type expectedType, int index) + : base($"IPC {name} expects a value type({expectedType.FullName}) at index {index}, null given.") + { + // ignored + } +} diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 7dbda203e..c933b4cd1 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -105,7 +105,7 @@ namespace Dalamud.Plugin.Ipc.Internal var paramTypes = methodInfo.GetParameters() .Select(pi => pi.ParameterType).ToArray(); - if (args.Length != paramTypes.Length) + if (args?.Length != paramTypes.Length) throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length); for (var i = 0; i < args.Length; i++) @@ -113,6 +113,14 @@ namespace Dalamud.Plugin.Ipc.Internal var arg = args[i]; var paramType = paramTypes[i]; + if (arg == null) + { + if (paramType.IsValueType) + throw new IpcValueNullError(this.Name, paramType, i); + + continue; + } + var argType = arg.GetType(); if (argType != paramType) { From c866c23d91517b5d23cf780678a5c8f07a5d0244 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:10:18 +0100 Subject: [PATCH 14/22] feat(ImGuiHelpers): add SafeTextWrapped --- Dalamud/Interface/ImGuiHelpers.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs index 4006f719e..b71b7cdd5 100644 --- a/Dalamud/Interface/ImGuiHelpers.cs +++ b/Dalamud/Interface/ImGuiHelpers.cs @@ -130,6 +130,12 @@ namespace Dalamud.Interface if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{textCopy}"); } + /// + /// Write unformatted text wrapped. + /// + /// The text to write. + public static void SafeTextWrapped(string text) => ImGui.TextWrapped(text.Replace("%", "%%")); + /// /// Get data needed for each new frame. /// From 623327d407a3816d9fa35f8db7bda683c5a1719b Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:10:49 +0100 Subject: [PATCH 15/22] fix(PluginInstallerWindow): Escape imgui string formatting --- .../PluginInstaller/PluginInstallerWindow.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 81c6be519..45e5adb03 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -399,11 +399,11 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller if (ImGui.BeginPopupModal(modalTitle, ref this.feedbackModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar)) { - ImGui.Text(Locs.FeedbackModal_Text(this.feedbackPlugin.Name)); + ImGui.TextUnformatted(Locs.FeedbackModal_Text(this.feedbackPlugin.Name)); if (this.feedbackPlugin?.FeedbackMessage != null) { - ImGui.TextWrapped(this.feedbackPlugin.FeedbackMessage); + ImGuiHelpers.SafeTextWrapped(this.feedbackPlugin.FeedbackMessage); } if (this.pluginListUpdatable.Any( @@ -1133,7 +1133,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller var cursor = ImGui.GetCursorPos(); // Name - ImGui.Text(label); + ImGui.TextUnformatted(label); // Download count var downloadCountText = manifest.DownloadCount > 0 @@ -1164,9 +1164,9 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller if (plugin is { IsBanned: true }) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.TextWrapped(plugin.BanReason.IsNullOrEmpty() - ? Locs.PluginBody_Banned - : Locs.PluginBody_BannedReason(plugin.BanReason)); + ImGuiHelpers.SafeTextWrapped(plugin.BanReason.IsNullOrEmpty() + ? Locs.PluginBody_Banned + : Locs.PluginBody_BannedReason(plugin.BanReason)); ImGui.PopStyleColor(); } @@ -1176,16 +1176,16 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller { if (!string.IsNullOrWhiteSpace(manifest.Punchline)) { - ImGui.TextWrapped(manifest.Punchline); + ImGuiHelpers.SafeTextWrapped(manifest.Punchline); } else if (!string.IsNullOrWhiteSpace(manifest.Description)) { const int punchlineLen = 200; var firstLine = manifest.Description.Split(new[] { '\r', '\n' })[0]; - ImGui.TextWrapped(firstLine.Length < punchlineLen - ? firstLine - : firstLine[..punchlineLen]); + ImGuiHelpers.SafeTextWrapped(firstLine.Length < punchlineLen + ? firstLine + : firstLine[..punchlineLen]); } } @@ -1225,7 +1225,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller ImGui.SameLine(); var cursor = ImGui.GetCursorPos(); - ImGui.Text(log.Title); + ImGui.TextUnformatted(log.Title); ImGui.SameLine(); ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{log.Version}"); @@ -1233,7 +1233,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller cursor.Y += ImGui.GetTextLineHeightWithSpacing(); ImGui.SetCursorPos(cursor); - ImGui.TextWrapped(log.Text); + ImGuiHelpers.SafeTextWrapped(log.Text); var endCursor = ImGui.GetCursorPos(); @@ -1294,7 +1294,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller // Description if (!string.IsNullOrWhiteSpace(manifest.Description)) { - ImGui.TextWrapped(manifest.Description); + ImGuiHelpers.SafeTextWrapped(manifest.Description); } ImGuiHelpers.ScaledDummy(5); @@ -1503,7 +1503,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller ImGui.Indent(); // Name - ImGui.Text(manifest.Name); + ImGui.TextUnformatted(manifest.Name); // Download count var downloadText = plugin.IsDev @@ -1533,7 +1533,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller // Description if (!string.IsNullOrWhiteSpace(manifest.Description)) { - ImGui.TextWrapped(manifest.Description); + ImGuiHelpers.SafeTextWrapped(manifest.Description); } // Available commands (if loaded) @@ -1548,7 +1548,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f)); foreach (var command in commands) { - ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}"); + ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}"); } } } @@ -1617,7 +1617,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller { ImGui.Text("Changelog:"); ImGuiHelpers.ScaledDummy(2); - ImGui.TextWrapped(manifest.Changelog); + ImGuiHelpers.SafeTextWrapped(manifest.Changelog); } ImGui.EndChild(); From aa545ce2098fe98e7a38c8af6c7d99a9e00fae19 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:19:44 +0100 Subject: [PATCH 16/22] feat: expose GamepadState as plugin service --- .../Game/ClientState/GamePad/GamepadState.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 4a703ff29..bcd100120 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -1,6 +1,8 @@ using System; using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; using ImGuiNET; using Serilog; @@ -11,6 +13,8 @@ namespace Dalamud.Game.ClientState.GamePad /// /// Will block game's gamepad input if is set. /// + [PluginInterface] + [InterfaceVersion("1.0.0")] public unsafe class GamepadState : IDisposable { private readonly Hook gamepadPoll; @@ -32,14 +36,6 @@ namespace Dalamud.Game.ClientState.GamePad this.gamepadPoll = new Hook(resolver.GamepadPoll, this.GamepadPollDetour); } - /// - /// Finalizes an instance of the class. - /// - ~GamepadState() - { - this.Dispose(false); - } - private delegate int ControllerPoll(IntPtr controllerInput); /// @@ -164,14 +160,6 @@ namespace Dalamud.Game.ClientState.GamePad /// 1 the whole time button is pressed, 0 otherwise. public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0; - /// - /// Enables the hook of the GamepadPoll function. - /// - public void Enable() - { - this.gamepadPoll.Enable(); - } - /// /// Disposes this instance, alongside its hooks. /// @@ -181,6 +169,14 @@ namespace Dalamud.Game.ClientState.GamePad GC.SuppressFinalize(this); } + /// + /// Enables the hook of the GamepadPoll function. + /// + internal void Enable() + { + this.gamepadPoll.Enable(); + } + private int GamepadPollDetour(IntPtr gamepadInput) { var original = this.gamepadPoll.Original(gamepadInput); From b7c47b4c970e65104d8d311eea7936249f6fd594 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:21:49 +0100 Subject: [PATCH 17/22] build: 6.3.0.4 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 93fe54d24..2d2ef88b6 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 6.3.0.3 + 6.3.0.4 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From f3588dfe2326f2af85274492ab2761da9eb869d4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 24 Feb 2022 18:56:34 +0900 Subject: [PATCH 18/22] Implement feature to use game resource fonts --- .../Internal/DalamudConfiguration.cs | 6 + Dalamud/Dalamud.cs | 4 + Dalamud/Interface/GameFonts/FdtReader.cs | 428 ++++++++++++++++++ Dalamud/Interface/GameFonts/GameFont.cs | 174 +++++++ Dalamud/Interface/GameFonts/GameFontHandle.cs | 35 ++ .../Interface/GameFonts/GameFontManager.cs | 407 +++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 44 +- .../Internal/Windows/SettingsWindow.cs | 18 + Dalamud/Interface/UiBuilder.cs | 23 + 9 files changed, 1137 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/FdtReader.cs create mode 100644 Dalamud/Interface/GameFonts/GameFont.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontHandle.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 3d2b58683..5359861cf 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Style; using Newtonsoft.Json; using Serilog; @@ -128,6 +129,11 @@ namespace Dalamud.Configuration.Internal /// public float GlobalUiScale { get; set; } = 1.0f; + /// + /// Gets or sets the game font to use for Dalamud UI. + /// + public GameFont DefaultFontFromGame { get; set; } = GameFont.Undefined; + /// /// Gets or sets a value indicating whether or not plugin UI should be hidden. /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8d313a646..537ffa516 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -18,6 +18,7 @@ using Dalamud.Game.Network.Internal; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking.Internal; using Dalamud.Interface; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -191,6 +192,9 @@ namespace Dalamud Service.Set().Enable(); Log.Information("[T2] IM OK!"); + Service.Set(); + Log.Information("[T2] GFM OK!"); + #pragma warning disable CS0618 // Type or member is obsolete Service.Set(); #pragma warning restore CS0618 // Type or member is obsolete diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs new file mode 100644 index 000000000..ceaca8096 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Parses a game font file. + /// + public class FdtReader + { + /// + /// Initializes a new instance of the class. + /// + /// Content of a FDT file. + public FdtReader(byte[] data) + { + unsafe + { + fixed (byte* ptr = data) + { + this.FileHeader = *(FdtHeader*)ptr; + this.FontHeader = *(FontTableHeader*)(ptr + this.FileHeader.FontTableHeaderOffset); + this.KerningHeader = *(KerningTableHeader*)(ptr + this.FileHeader.KerningTableHeaderOffset); + + var glyphs = (FontTableEntry*)(ptr + this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf(this.FontHeader)); + for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) + this.Glyphs.Add(glyphs[i]); + + var kerns = (KerningTableEntry*)(ptr + this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf(this.KerningHeader)); + for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) + this.Distances.Add(kerns[i]); + } + } + } + + /// + /// Gets the header of this file. + /// + public FdtHeader FileHeader { get; init; } + + /// + /// Gets the font header of this file. + /// + public FontTableHeader FontHeader { get; init; } + + /// + /// Gets the kerning table header of this file. + /// + public KerningTableHeader KerningHeader { get; init; } + + /// + /// Gets all the glyphs defined in this file. + /// + public List Glyphs { get; init; } = new(); + + /// + /// Gets all the kerning entries defined in this file. + /// + public List Distances { get; init; } = new(); + + /// + /// Finds glyph definition for corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding FontTableEntry, or null if not found. + public FontTableEntry? FindGlyph(int codepoint) + { + var i = this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8int32(codepoint) }); + if (i < 0 || i == this.Glyphs.Count) + return null; + return this.Glyphs[i]; + } + + /// + /// Returns glyph definition for corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding FontTableEntry, or that of a fallback character. + public FontTableEntry GetGlyph(int codepoint) + { + return (this.FindGlyph(codepoint) + ?? this.FindGlyph('〓') + ?? this.FindGlyph('?') + ?? this.FindGlyph('='))!.Value; + } + + /// + /// Returns distance adjustment between two adjacent characters. + /// + /// Left character. + /// Right character. + /// Supposed distance adjustment between given characters. + public int GetDistance(int codepoint1, int codepoint2) + { + var i = this.Distances.BinarySearch(new KerningTableEntry { LeftUtf8 = CodePointToUtf8int32(codepoint1), RightUtf8 = CodePointToUtf8int32(codepoint2) }); + if (i < 0 || i == this.Distances.Count) + return 0; + return this.Distances[i].RightOffset; + } + + private static int CodePointToUtf8int32(int codepoint) + { + if (codepoint <= 0x7F) + { + return codepoint; + } + else if (codepoint <= 0x7FF) + { + return ((0xC0 | (codepoint >> 6)) << 8) + | ((0x80 | ((codepoint >> 0) & 0x3F)) << 0); + } + else if (codepoint <= 0xFFFF) + { + return ((0xE0 | (codepoint >> 12)) << 16) + | ((0x80 | ((codepoint >> 6) & 0x3F)) << 8) + | ((0x80 | ((codepoint >> 0) & 0x3F)) << 0); + } + else if (codepoint <= 0x10FFFF) + { + return ((0xF0 | (codepoint >> 18)) << 24) + | ((0x80 | ((codepoint >> 12) & 0x3F)) << 16) + | ((0x80 | ((codepoint >> 6) & 0x3F)) << 8) + | ((0x80 | ((codepoint >> 0) & 0x3F)) << 0); + } + else + { + return 0xFFFE; + } + } + + private static int Utf8Uint32ToCodePoint(int n) + { + if ((n & 0xFFFFFF80) == 0) + { + return n & 0x7F; + } + else if ((n & 0xFFFFE0C0) == 0xC080) + { + return + (((n >> 0x08) & 0x1F) << 6) | + (((n >> 0x00) & 0x3F) << 0); + } + else if ((n & 0xF0C0C0) == 0xE08080) + { + return + (((n >> 0x10) & 0x0F) << 12) | + (((n >> 0x08) & 0x3F) << 6) | + (((n >> 0x00) & 0x3F) << 0); + } + else if ((n & 0xF8C0C0C0) == 0xF0808080) + { + return + (((n >> 0x18) & 0x07) << 18) | + (((n >> 0x10) & 0x3F) << 12) | + (((n >> 0x08) & 0x3F) << 6) | + (((n >> 0x00) & 0x3F) << 0); + } + else + { + return 0xFFFF; // Guaranteed non-unicode + } + } + + /// + /// Header of game font file format. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct FdtHeader + { + /// + /// Signature: "fcsv". + /// + public fixed byte Signature[8]; + + /// + /// Offset to FontTableHeader. + /// + public int FontTableHeaderOffset; + + /// + /// Offset to KerningTableHeader. + /// + public int KerningTableHeaderOffset; + + /// + /// Unused/unknown. + /// + public fixed byte Padding[0x10]; + } + + /// + /// Header of glyph table. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct FontTableHeader + { + /// + /// Signature: "fthd". + /// + public fixed byte Signature[4]; + + /// + /// Number of glyphs defined in this file. + /// + public int FontTableEntryCount; + + /// + /// Number of kerning informations defined in this file. + /// + public int KerningTableEntryCount; + + /// + /// Unused/unknown. + /// + public fixed byte Padding[0x04]; + + /// + /// Width of backing texture. + /// + public ushort TextureWidth; + + /// + /// Height of backing texture. + /// + public ushort TextureHeight; + + /// + /// Size of the font defined from this file, in points unit. + /// + public float Size; + + /// + /// Line height of the font defined forom this file, in pixels unit. + /// + public int LineHeight; + + /// + /// Ascent of the font defined from this file, in pixels unit. + /// + public int Ascent; + + /// + /// Gets descent of the font defined from this file, in pixels unit. + /// + public int Descent => this.LineHeight - this.Ascent; + } + + /// + /// Glyph table entry. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct FontTableEntry : IComparable + { + /// + /// Mapping of texture channel index to byte index. + /// + public static readonly int[] TextureChannelOrder = { 2, 1, 0, 3 }; + + /// + /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian. + /// + public int CharUtf8; + + /// + /// Integer representation of a Shift_JIS character in reverse order, read in little endian. + /// + public ushort CharSjis; + + /// + /// Index of backing texture. + /// + public ushort TextureIndex; + + /// + /// Horizontal offset of glyph image in the backing texture. + /// + public ushort TextureOffsetX; + + /// + /// Vertical offset of glyph image in the backing texture. + /// + public ushort TextureOffsetY; + + /// + /// Bounding width of this glyph. + /// + public byte BoundingWidth; + + /// + /// Bounding height of this glyph. + /// + public byte BoundingHeight; + + /// + /// Distance adjustment for drawing next character. + /// + public sbyte NextOffsetX; + + /// + /// Distance adjustment for drawing current character. + /// + public sbyte CurrentOffsetY; + + /// + /// Gets the index of the file among all the backing texture files. + /// + public int TextureFileIndex => this.TextureIndex / 4; + + /// + /// Gets the channel index in the backing texture file. + /// + public int TextureChannelIndex => this.TextureIndex % 4; + + /// + /// Gets the byte index in a multichannel pixel corresponding to the channel. + /// + public int TextureChannelByteIndex => TextureChannelOrder[this.TextureChannelIndex]; + + /// + /// Gets the advance width of this character. + /// + public int AdvanceWidth => this.BoundingWidth + this.NextOffsetX; + + /// + /// Gets the Unicode codepoint of the character for this entry in int type. + /// + public int CharInt => Utf8Uint32ToCodePoint(this.CharUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in char type. + /// + public char Char => (char)Utf8Uint32ToCodePoint(this.CharUtf8); + + /// + public int CompareTo(FontTableEntry other) + { + return this.CharUtf8 - other.CharUtf8; + } + } + + /// + /// Header of kerning table. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KerningTableHeader + { + /// + /// Signature: "knhd". + /// + public fixed byte Signature[4]; + + /// + /// Number of kerning entries in this table. + /// + public int Count; + + /// + /// Unused/unknown. + /// + public fixed byte Padding[0x08]; + } + + /// + /// Kerning table entry. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KerningTableEntry : IComparable + { + /// + /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character. + /// + public int LeftUtf8; + + /// + /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the right character. + /// + public int RightUtf8; + + /// + /// Integer representation of a Shift_JIS character in reverse order, read in little endian, for the left character. + /// + public ushort LeftSjis; + + /// + /// Integer representation of a Shift_JIS character in reverse order, read in little endian, for the right character. + /// + public ushort RightSjis; + + /// + /// Horizontal offset adjustment for the right character. + /// + public int RightOffset; + + /// + /// Gets the Unicode codepoint of the character for this entry in int type. + /// + public int LeftInt => Utf8Uint32ToCodePoint(this.LeftUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in char type. + /// + public char Left => (char)Utf8Uint32ToCodePoint(this.LeftUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in int type. + /// + public int RightInt => Utf8Uint32ToCodePoint(this.RightUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in char type. + /// + public char Right => (char)Utf8Uint32ToCodePoint(this.RightUtf8); + + /// + public int CompareTo(KerningTableEntry other) + { + if (this.LeftUtf8 == other.LeftUtf8) + return this.RightUtf8 - other.RightUtf8; + else + return this.LeftUtf8 - other.LeftUtf8; + } + } + } +} diff --git a/Dalamud/Interface/GameFonts/GameFont.cs b/Dalamud/Interface/GameFonts/GameFont.cs new file mode 100644 index 000000000..f095b6fcc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFont.cs @@ -0,0 +1,174 @@ +namespace Dalamud.Interface.GameFonts +{ + /// + /// Enum of available game fonts. + /// + public enum GameFont : int + { + /// + /// Placeholder meaning unused. + /// + Undefined, + + /// + /// AXIS (9.6pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis96, + + /// + /// AXIS (12pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis12, + + /// + /// AXIS (14pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis14, + + /// + /// AXIS (18pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis18, + + /// + /// AXIS (36pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis36, + + /// + /// Jupiter (16pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter16, + + /// + /// Jupiter (20pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter20, + + /// + /// Jupiter (23pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter23, + + /// + /// Jupiter (45pt) + /// + /// Serif font. Contains mostly numbers. Used in game for flying texts. + /// + Jupiter45, + + /// + /// Jupiter (46pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter46, + + /// + /// Jupiter (90pt) + /// + /// Serif font. Contains mostly numbers. Used in game for flying texts. + /// + Jupiter90, + + /// + /// Meidinger (16pt) + /// + /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. + /// + Meidinger16, + + /// + /// Meidinger (20pt) + /// + /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. + /// + Meidinger20, + + /// + /// Meidinger (40pt) + /// + /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. + /// + Meidinger40, + + /// + /// MiedingerMid (10pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid10, + + /// + /// MiedingerMid (12pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid12, + + /// + /// MiedingerMid (14pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid14, + + /// + /// MiedingerMid (18pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid18, + + /// + /// MiedingerMid (36pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid36, + + /// + /// TrumpGothic (18.4pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic184, + + /// + /// TrumpGothic (23pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic23, + + /// + /// TrumpGothic (34pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic34, + + /// + /// TrumpGothic (688pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic68, + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs new file mode 100644 index 000000000..6d9274ac0 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -0,0 +1,35 @@ +using System; + +using ImGuiNET; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Prepare and keep game font loaded for use in OnDraw. + /// + public class GameFontHandle : IDisposable + { + private readonly GameFontManager manager; + private readonly GameFont font; + + /// + /// Initializes a new instance of the class. + /// + /// GameFontManager instance. + /// Font to use. + internal GameFontHandle(GameFontManager manager, GameFont font) + { + this.manager = manager; + this.font = font; + } + + /// + /// Gets the font. + /// + /// Corresponding font or null. + public ImFontPtr? Get() => this.manager.GetFont(this.font); + + /// + public void Dispose() => this.manager.DecreaseFontRef(this.font); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs new file mode 100644 index 000000000..9cca00236 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +using Dalamud.Data; +using Dalamud.Interface.Internal; +using ImGuiNET; +using Lumina.Data.Files; +using Serilog; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Loads game font for use in ImGui. + /// + internal class GameFontManager : IDisposable + { + private static readonly string[] FontNames = + { + null, + "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", + "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", + "Meidinger_16", "Meidinger_20", "Meidinger_40", + "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", + "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", + }; + + private readonly object syncRoot = new(); + + private readonly InterfaceManager interfaceManager; + + private readonly FdtReader?[] fdts; + private readonly List texturePixels; + private readonly ImFontPtr?[] fonts = new ImFontPtr?[FontNames.Length]; + + private readonly int[] fontUseCounter = new int[FontNames.Length]; + private readonly List>> glyphRectIds = new(); + + /// + /// Initializes a new instance of the class. + /// + public GameFontManager() + { + var dataManager = Service.Get(); + + this.fdts = FontNames.Select(fontName => + { + var file = fontName == null ? null : dataManager.GetFile($"common/font/{fontName}.fdt"); + return file == null ? null : new FdtReader(file!.Data); + }).ToArray(); + this.texturePixels = Enumerable.Range(1, 1 + this.fdts.Where(x => x != null).Select(x => x.Glyphs.Select(x => x.TextureFileIndex).Max()).Max()).Select(x => dataManager.GameData.GetFile($"common/font/font{x}.tex").ImageData).ToList(); + + this.interfaceManager = Service.Get(); + } + + /// + /// Describe font into a string. + /// + /// Font to describe. + /// A string in a form of "FontName (NNNpt)". + public static string DescribeFont(GameFont font) + { + return font switch + { + GameFont.Undefined => "-", + GameFont.Axis96 => "AXIS (9.6pt)", + GameFont.Axis12 => "AXIS (12pt)", + GameFont.Axis14 => "AXIS (14pt)", + GameFont.Axis18 => "AXIS (18pt)", + GameFont.Axis36 => "AXIS (36pt)", + GameFont.Jupiter16 => "Jupiter (16pt)", + GameFont.Jupiter20 => "Jupiter (20pt)", + GameFont.Jupiter23 => "Jupiter (23pt)", + GameFont.Jupiter45 => "Jupiter Numeric (45pt)", + GameFont.Jupiter46 => "Jupiter (46pt)", + GameFont.Jupiter90 => "Jupiter Numeric (90pt)", + GameFont.Meidinger16 => "Meidinger Numeric (16pt)", + GameFont.Meidinger20 => "Meidinger Numeric (20pt)", + GameFont.Meidinger40 => "Meidinger Numeric (40pt)", + GameFont.MiedingerMid10 => "MiedingerMid (10pt)", + GameFont.MiedingerMid12 => "MiedingerMid (12pt)", + GameFont.MiedingerMid14 => "MiedingerMid (14pt)", + GameFont.MiedingerMid18 => "MiedingerMid (18pt)", + GameFont.MiedingerMid36 => "MiedingerMid (36pt)", + GameFont.TrumpGothic184 => "Trump Gothic (18.4pt)", + GameFont.TrumpGothic23 => "Trump Gothic (23pt)", + GameFont.TrumpGothic34 => "Trump Gothic (34pt)", + GameFont.TrumpGothic68 => "Trump Gothic (68pt)", + _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), + }; + } + + /// + /// Determines whether a font should be able to display most of stuff. + /// + /// Font to check. + /// True if it can. + public static bool IsGenericPurposeFont(GameFont font) + { + return font switch + { + GameFont.Axis96 => true, + GameFont.Axis12 => true, + GameFont.Axis14 => true, + GameFont.Axis18 => true, + GameFont.Axis36 => true, + _ => false, + }; + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// 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) + { + if (!source.HasValue || !target.HasValue) + return; + + 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) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) + { + 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->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize); + } + 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->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; + } + } + } + + if (rebuildLookupTable) + target.Value!.BuildLookupTable(); + } + + /// + public void Dispose() + { + } + + /// + /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. + /// + /// Font to use. + /// Handle to game font that may or may not be ready yet. + public GameFontHandle NewFontRef(GameFont gameFont) + { + var fontIndex = (int)gameFont; + var needRebuild = false; + + lock (this.syncRoot) + { + var prev = this.fontUseCounter[fontIndex] == 0; + this.fontUseCounter[fontIndex] += 1; + needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + } + + if (needRebuild) + this.interfaceManager.RebuildFonts(); + + return new(this, gameFont); + } + + /// + /// Gets the font. + /// + /// Font to get. + /// Corresponding font or null. + public ImFontPtr? GetFont(GameFont gameFont) => this.fonts[(int)gameFont]; + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFont target, bool missingOnly, bool rebuildLookupTable) + { + GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[(int)target], missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFont source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + { + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], target, missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFont source, GameFont target, bool missingOnly, bool rebuildLookupTable) + { + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], this.fonts[(int)target], missingOnly, rebuildLookupTable); + } + + /// + /// Build fonts before plugins do something more. To be called from InterfaceManager. + /// + public void BuildFonts() + { + this.glyphRectIds.Clear(); + var io = ImGui.GetIO(); + io.Fonts.TexDesiredWidth = 4096; + + for (var i = 0; i < FontNames.Length; i++) + { + this.fonts[i] = null; + this.glyphRectIds.Add(new()); + + var fdt = this.fdts[i]; + if (this.fontUseCounter[i] == 0 || fdt == null) + continue; + + Log.Information($"GameFontManager BuildFont: {FontNames[i]}"); + + var font = io.Fonts.AddFontDefault(); + this.fonts[i] = font; + foreach (var glyph in fdt.Glyphs) + { + var c = glyph.Char; + if (c < 32 || c >= 0xFFFF) + continue; + + this.glyphRectIds[i][c] = Tuple.Create(io.Fonts.AddCustomRectFontGlyph(font, c, glyph.BoundingWidth + 1, glyph.BoundingHeight + 1, glyph.BoundingWidth + glyph.NextOffsetX, new Vector2(0, glyph.CurrentOffsetY)), glyph); + } + } + } + + /// + /// Post-build fonts before plugins do something more. To be called from InterfaceManager. + /// + public unsafe void AfterBuildFonts() + { + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height); + var pixels32 = (uint*)pixels8; + + for (var i = 0; i < this.fonts.Length; i++) + { + if (!this.fonts[i].HasValue) + continue; + + var font = this.fonts[i]!.Value; + var fdt = this.fdts[i]; + var fontPtr = font.NativePtr; + fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.LineHeight; + fontPtr->Ascent = fdt.FontHeader.Ascent; + fontPtr->Descent = fdt.FontHeader.Descent; + fontPtr->EllipsisChar = '…'; + foreach (var fallbackCharCandidate in "〓?!") + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); + if ((IntPtr)glyph.NativePtr != IntPtr.Zero) + { + font.SetFallbackChar(fallbackCharCandidate); + break; + } + } + + fixed (char* c = FontNames[i]) + { + for (var j = 0; j < 40; j++) + fontPtr->ConfigData->Name[j] = 0; + Encoding.UTF8.GetBytes(c, FontNames[i].Length, fontPtr->ConfigData->Name, 40); + } + + foreach (var (c, (rectId, glyph)) in this.glyphRectIds[i]) + { + var rc = io.Fonts.GetCustomRectByIndex(rectId); + var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; + var sourceBufferDelta = glyph.TextureChannelByteIndex; + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; + pixels32[((rc.Y + y) * width) + rc.X + x] = (uint)(a << 24) | 0xFFFFFFu; + } + } + } + } + + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis96, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis12, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis14, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis18, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis36, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Jupiter16, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter20, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter23, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter45, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter46, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter90, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Meidinger16, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger20, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger40, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis96, GameFont.MiedingerMid10, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis12, GameFont.MiedingerMid12, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis14, GameFont.MiedingerMid14, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.MiedingerMid18, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.MiedingerMid36, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.TrumpGothic184, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic23, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic34, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic68, true, false); + + foreach (var font in this.fonts) + font?.BuildLookupTable(); + } + + /// + /// Decrease font reference counter and release if nobody is using it. + /// + /// Font to release. + internal void DecreaseFontRef(GameFont gameFont) + { + var fontIndex = (int)gameFont; + var needRebuild = false; + + lock (this.syncRoot) + { + var prev = this.fontUseCounter[fontIndex] == 0; + this.fontUseCounter[fontIndex] -= 1; + needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + } + + if (needRebuild) + this.interfaceManager.RebuildFonts(); + } + + private struct ImFontGlyphReal + { + public uint ColoredVisibleCodepoint; + public float AdvanceX; + public float X0; + public float Y0; + public float X1; + public float Y1; + public float U0; + public float V0; + public float U1; + public float V1; + + public bool Colored + { + get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u); + } + + public bool Visible + { + get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u); + } + + public int Codepoint + { + get => (int)(this.ColoredVisibleCodepoint >> 2); + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); + } + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index b285c60ed..1e9654dc0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -16,6 +16,7 @@ using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.Internal; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows.StyleEditor; @@ -56,6 +57,9 @@ namespace Dalamud.Interface.Internal private readonly SwapChainVtableResolver address; private RawDX11Scene? scene; + private GameFont overwriteDefaultFontFromGameFont = GameFont.Undefined; + private GameFontHandle? overwriteDefaultFontFromGameFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; private bool isRebuildingFonts = false; @@ -128,10 +132,15 @@ namespace Dalamud.Interface.Internal public event Action ResizeBuffers; /// - /// Gets or sets an action that is executed when fonts are rebuilt. + /// Gets or sets an action that is executed right before fonts are rebuilt. /// public event Action BuildFonts; + /// + /// Gets or sets an action that is executed right after fonts are rebuilt. + /// + public event Action AfterBuildFonts; + /// /// Gets the default ImGui font. /// @@ -304,6 +313,16 @@ namespace Dalamud.Interface.Internal if (!this.isRebuildingFonts) { Log.Verbose("[FONT] RebuildFonts() trigger"); + var configuration = Service.Get(); + if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) + { + this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; + this.overwriteDefaultFontFromGameFontHandle?.Dispose(); + if (configuration.DefaultFontFromGame == GameFont.Undefined) + this.overwriteDefaultFontFromGameFontHandle = null; + else + this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); + } this.isRebuildingFonts = true; this.scene.OnNewRenderFrame += this.RebuildFontsInternal; @@ -384,6 +403,16 @@ namespace Dalamud.Interface.Internal this.scene.OnBuildUI += this.Display; this.scene.OnNewInputFrame += this.OnNewInputFrame; + if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) + { + this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; + this.overwriteDefaultFontFromGameFontHandle?.Dispose(); + if (configuration.DefaultFontFromGame == GameFont.Undefined) + this.overwriteDefaultFontFromGameFontHandle = null; + else + this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); + } + this.SetupFonts(); StyleModel.TransferOldModels(); @@ -496,7 +525,6 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().Fonts.Clear(); ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.MergeMode = true; fontConfig.PixelSnapH = true; var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); @@ -522,7 +550,9 @@ namespace Dalamud.Interface.Internal }, GCHandleType.Pinned); + 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"); @@ -546,6 +576,9 @@ namespace Dalamud.Interface.Internal MonoFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathMono, 16.0f); + var gameFontManager = Service.Get(); + gameFontManager.BuildFonts(); + Log.Verbose("[FONT] Invoke OnBuildFonts"); this.BuildFonts?.Invoke(); Log.Verbose("[FONT] OnBuildFonts OK!"); @@ -557,6 +590,13 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().Fonts.Build(); + gameFontManager.AfterBuildFonts(); + GameFontManager.CopyGlyphsAcrossFonts(this.overwriteDefaultFontFromGameFontHandle?.Get(), DefaultFont, false, true); + + Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); + this.AfterBuildFonts?.Invoke(); + Log.Verbose("[FONT] OnAfterBuildFonts OK!"); + Log.Verbose("[FONT] Fonts built!"); this.fontBuildSignal.Set(); diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index d02dae56d..6fa6df808 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -12,6 +12,7 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Utility; @@ -27,6 +28,8 @@ namespace Dalamud.Interface.Internal.Windows private const float MinScale = 0.3f; private const float MaxScale = 2.0f; + private readonly List validFontChoices; + private readonly string[] validFontNames; private readonly string[] languages; private readonly string[] locLanguages; private int langIndex; @@ -66,6 +69,8 @@ namespace Dalamud.Interface.Internal.Windows private bool doButtonsSystemMenu; private bool disableRmtFiltering; + private int validFontIndex; + #region Experimental private bool doPluginTest; @@ -111,6 +116,10 @@ namespace Dalamud.Interface.Internal.Windows this.doButtonsSystemMenu = configuration.DoButtonsSystemMenu; this.disableRmtFiltering = configuration.DisableRmtFiltering; + this.validFontChoices = Enum.GetValues().Where(x => x == GameFont.Undefined || GameFontManager.IsGenericPurposeFont(x)).ToList(); + this.validFontNames = this.validFontChoices.Select(x => GameFontManager.DescribeFont(x)).ToArray(); + this.validFontIndex = Math.Max(0, this.validFontChoices.IndexOf(configuration.DefaultFontFromGame)); + this.languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); try { @@ -286,6 +295,11 @@ namespace Dalamud.Interface.Internal.Windows ImGuiHelpers.ScaledDummy(10, 16); + ImGui.Text(Loc.Localize("DalamudSettingsGlobalFont", "Global Font")); + ImGui.Combo("##DalamudSettingsGlobalFontDrag", ref this.validFontIndex, this.validFontNames, this.validFontNames.Length); + + ImGuiHelpers.ScaledDummy(10, 16); + if (ImGui.Button(Loc.Localize("DalamudSettingsOpenStyleEditor", "Open Style Editor"))) { Service.Get().OpenStyleEditor(); @@ -800,6 +814,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.IsFocusManagementEnabled = this.doFocus; configuration.ShowTsm = this.doTsm; + configuration.DefaultFontFromGame = this.validFontChoices[this.validFontIndex]; + // This is applied every frame in InterfaceManager::CheckViewportState() configuration.IsDisableViewport = !this.doViewport; @@ -842,6 +858,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.Save(); + Service.Get().RebuildFonts(); + _ = Service.Get().ReloadPluginMastersAsync(); } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 445ac3618..35c82102b 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; @@ -39,6 +40,7 @@ namespace Dalamud.Interface var interfaceManager = Service.Get(); interfaceManager.Draw += this.OnDraw; interfaceManager.BuildFonts += this.OnBuildFonts; + interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; interfaceManager.ResizeBuffers += this.OnResizeBuffers; } @@ -67,6 +69,15 @@ namespace Dalamud.Interface /// public event Action BuildFonts; + /// + /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
+ /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// (at any time), so you should both reload your custom fonts and restore those + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + ///
+ public event Action AfterBuildFonts; + /// /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// @@ -201,6 +212,13 @@ namespace Dalamud.Interface public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => Service.Get().LoadImageRaw(imageData, width, height, numChannels); + /// + /// Gets a game font. + /// + /// Font to get. + /// Handle to the game font which may or may not be available for use yet. + public GameFontHandle GetGameFontHandle(GameFont gameFont) => Service.Get().NewFontRef(gameFont); + /// /// Call this to queue a rebuild of the font atlas.
/// This will invoke any handlers and ensure that any loaded fonts are @@ -320,6 +338,11 @@ namespace Dalamud.Interface this.BuildFonts?.Invoke(); } + private void OnAfterBuildFonts() + { + this.AfterBuildFonts?.Invoke(); + } + private void OnResizeBuffers() { this.ResizeBuffers?.Invoke(); From 17c77e6bfd7fa084541469b8dea54d8290e9897b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 25 Feb 2022 00:05:26 +0900 Subject: [PATCH 19/22] Add italic/bold to font settings and make AXIS as a checkbox in settings window --- .../Internal/DalamudConfiguration.cs | 4 +- Dalamud/Interface/GameFonts/GameFontFamily.cs | 49 ++++ .../{GameFont.cs => GameFontFamilyAndSize.cs} | 4 +- Dalamud/Interface/GameFonts/GameFontHandle.cs | 26 +- .../Interface/GameFonts/GameFontManager.cs | 241 ++++++++++-------- Dalamud/Interface/GameFonts/GameFontStyle.cs | 235 +++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 46 ++-- .../Internal/Windows/SettingsWindow.cs | 27 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 224 ++++++++-------- Dalamud/Interface/UiBuilder.cs | 4 +- 10 files changed, 598 insertions(+), 262 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/GameFontFamily.cs rename Dalamud/Interface/GameFonts/{GameFont.cs => GameFontFamilyAndSize.cs} (97%) create mode 100644 Dalamud/Interface/GameFonts/GameFontStyle.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 5359861cf..7930f5c79 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -130,9 +130,9 @@ namespace Dalamud.Configuration.Internal public float GlobalUiScale { get; set; } = 1.0f; /// - /// Gets or sets the game font to use for Dalamud UI. + /// Gets or sets a value indicating whether to use AXIS fonts from the game. /// - public GameFont DefaultFontFromGame { get; set; } = GameFont.Undefined; + public bool UseAxisFontsFromGame { get; set; } = false; /// /// Gets or sets a value indicating whether or not plugin UI should be hidden. diff --git a/Dalamud/Interface/GameFonts/GameFontFamily.cs b/Dalamud/Interface/GameFonts/GameFontFamily.cs new file mode 100644 index 000000000..2aa836927 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamily.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Enum of available game font families. + /// + public enum GameFontFamily + { + /// + /// Placeholder meaning unused. + /// + Undefined, + + /// + /// Sans-serif fonts used for the whole UI. Contains Japanese characters in addition to Latin characters. + /// + Axis, + + /// + /// Serif fonts used for job names. Contains Latin characters. + /// + Jupiter, + + /// + /// Digit-only serif fonts used for flying texts. Contains numbers. + /// + JupiterNumeric, + + /// + /// Digit-only sans-serif horizontally wide fonts used for HP/MP/IL numbers. + /// + Meidinger, + + /// + /// Sans-serif horizontally wide font used for names of gauges. Contains Latin characters. + /// + MiedingerMid, + + /// + /// Sans-serif horizontally narrow font used for addon titles. Contains Latin characters. + /// + TrumpGothic, + } +} diff --git a/Dalamud/Interface/GameFonts/GameFont.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs similarity index 97% rename from Dalamud/Interface/GameFonts/GameFont.cs rename to Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index f095b6fcc..1cbdf210f 100644 --- a/Dalamud/Interface/GameFonts/GameFont.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -1,9 +1,9 @@ namespace Dalamud.Interface.GameFonts { /// - /// Enum of available game fonts. + /// Enum of available game fonts in specific sizes. /// - public enum GameFont : int + public enum GameFontFamilyAndSize : int { /// /// Placeholder meaning unused. diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 6d9274ac0..69e754727 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -10,26 +10,40 @@ namespace Dalamud.Interface.GameFonts public class GameFontHandle : IDisposable { private readonly GameFontManager manager; - private readonly GameFont font; + private readonly GameFontStyle fontStyle; /// /// Initializes a new instance of the class. /// /// GameFontManager instance. /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFont font) + internal GameFontHandle(GameFontManager manager, GameFontStyle font) { this.manager = manager; - this.font = font; + this.fontStyle = font; } + /// + /// Gets the font style. + /// + public GameFontStyle Style => this.fontStyle; + + /// + /// Gets a value indicating whether this font is ready for use. + /// + public bool Available => this.manager.GetFont(this.fontStyle) != null; + /// /// Gets the font. /// - /// Corresponding font or null. - public ImFontPtr? Get() => this.manager.GetFont(this.font); + public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + + /// + /// Gets the FdtReader. + /// + public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); /// - public void Dispose() => this.manager.DecreaseFontRef(this.font); + public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); } } diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 9cca00236..34d385bba 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -33,10 +33,9 @@ namespace Dalamud.Interface.GameFonts private readonly FdtReader?[] fdts; private readonly List texturePixels; - private readonly ImFontPtr?[] fonts = new ImFontPtr?[FontNames.Length]; - - private readonly int[] fontUseCounter = new int[FontNames.Length]; - private readonly List>> glyphRectIds = new(); + private readonly Dictionary fonts = new(); + private readonly Dictionary fontUseCounter = new(); + private readonly Dictionary>> glyphRectIds = new(); /// /// Initializes a new instance of the class. @@ -60,34 +59,34 @@ namespace Dalamud.Interface.GameFonts /// /// Font to describe. /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFont font) + public static string DescribeFont(GameFontFamilyAndSize font) { return font switch { - GameFont.Undefined => "-", - GameFont.Axis96 => "AXIS (9.6pt)", - GameFont.Axis12 => "AXIS (12pt)", - GameFont.Axis14 => "AXIS (14pt)", - GameFont.Axis18 => "AXIS (18pt)", - GameFont.Axis36 => "AXIS (36pt)", - GameFont.Jupiter16 => "Jupiter (16pt)", - GameFont.Jupiter20 => "Jupiter (20pt)", - GameFont.Jupiter23 => "Jupiter (23pt)", - GameFont.Jupiter45 => "Jupiter Numeric (45pt)", - GameFont.Jupiter46 => "Jupiter (46pt)", - GameFont.Jupiter90 => "Jupiter Numeric (90pt)", - GameFont.Meidinger16 => "Meidinger Numeric (16pt)", - GameFont.Meidinger20 => "Meidinger Numeric (20pt)", - GameFont.Meidinger40 => "Meidinger Numeric (40pt)", - GameFont.MiedingerMid10 => "MiedingerMid (10pt)", - GameFont.MiedingerMid12 => "MiedingerMid (12pt)", - GameFont.MiedingerMid14 => "MiedingerMid (14pt)", - GameFont.MiedingerMid18 => "MiedingerMid (18pt)", - GameFont.MiedingerMid36 => "MiedingerMid (36pt)", - GameFont.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFont.TrumpGothic23 => "Trump Gothic (23pt)", - GameFont.TrumpGothic34 => "Trump Gothic (34pt)", - GameFont.TrumpGothic68 => "Trump Gothic (68pt)", + GameFontFamilyAndSize.Undefined => "-", + GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", + GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", + GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", + GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", + GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", + GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", + GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", + GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", + GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", + GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", + GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", + GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", + GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", + GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", + GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", + GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", + GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", + GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", + GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", + GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", + GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", + GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", + GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), }; } @@ -97,15 +96,15 @@ namespace Dalamud.Interface.GameFonts /// /// Font to check. /// True if it can. - public static bool IsGenericPurposeFont(GameFont font) + public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) { return font switch { - GameFont.Axis96 => true, - GameFont.Axis12 => true, - GameFont.Axis14 => true, - GameFont.Axis18 => true, - GameFont.Axis36 => true, + GameFontFamilyAndSize.Axis96 => true, + GameFontFamilyAndSize.Axis12 => true, + GameFontFamilyAndSize.Axis14 => true, + GameFontFamilyAndSize.Axis18 => true, + GameFontFamilyAndSize.Axis36 => true, _ => false, }; } @@ -174,32 +173,38 @@ namespace Dalamud.Interface.GameFonts /// /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. /// - /// Font to use. + /// Font to use. /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFont gameFont) + public GameFontHandle NewFontRef(GameFontStyle style) { - var fontIndex = (int)gameFont; var needRebuild = false; lock (this.syncRoot) { - var prev = this.fontUseCounter[fontIndex] == 0; - this.fontUseCounter[fontIndex] += 1; - needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0); + var newValue = this.fontUseCounter[style] = prevValue + 1; + needRebuild = (prevValue == 0) != (newValue == 0); } if (needRebuild) this.interfaceManager.RebuildFonts(); - return new(this, gameFont); + return new(this, style); } /// /// Gets the font. /// - /// Font to get. + /// Font to get. /// Corresponding font or null. - public ImFontPtr? GetFont(GameFont gameFont) => this.fonts[(int)gameFont]; + public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); + + /// + /// Gets the corresponding FdtReader. + /// + /// Font to get. + /// Corresponding FdtReader or null. + public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; /// /// Fills missing glyphs in target font from source font, if both are not null. @@ -208,9 +213,9 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFont target, bool missingOnly, bool rebuildLookupTable) + public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[(int)target], missingOnly, rebuildLookupTable); + GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -220,9 +225,9 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFont source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], target, missingOnly, rebuildLookupTable); + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); } /// @@ -232,9 +237,9 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFont source, GameFont target, bool missingOnly, bool rebuildLookupTable) + public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], this.fonts[(int)target], missingOnly, rebuildLookupTable); + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -242,30 +247,38 @@ namespace Dalamud.Interface.GameFonts /// public void BuildFonts() { - this.glyphRectIds.Clear(); var io = ImGui.GetIO(); io.Fonts.TexDesiredWidth = 4096; - for (var i = 0; i < FontNames.Length; i++) - { - this.fonts[i] = null; - this.glyphRectIds.Add(new()); + this.glyphRectIds.Clear(); + this.fonts.Clear(); - var fdt = this.fdts[i]; - if (this.fontUseCounter[i] == 0 || fdt == null) + foreach (var style in this.fontUseCounter.Keys) + { + var rectIds = this.glyphRectIds[style] = new(); + + var fdt = this.fdts[(int)style.FamilyAndSize]; + if (fdt == null) continue; - Log.Information($"GameFontManager BuildFont: {FontNames[i]}"); - var font = io.Fonts.AddFontDefault(); - this.fonts[i] = font; + this.fonts[style] = font; foreach (var glyph in fdt.Glyphs) { var c = glyph.Char; if (c < 32 || c >= 0xFFFF) continue; - this.glyphRectIds[i][c] = Tuple.Create(io.Fonts.AddCustomRectFontGlyph(font, c, glyph.BoundingWidth + 1, glyph.BoundingHeight + 1, glyph.BoundingWidth + glyph.NextOffsetX, new Vector2(0, glyph.CurrentOffsetY)), glyph); + 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); } } } @@ -279,13 +292,9 @@ namespace Dalamud.Interface.GameFonts io.Fonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height); var pixels32 = (uint*)pixels8; - for (var i = 0; i < this.fonts.Length; i++) + foreach (var (style, font) in this.fonts) { - if (!this.fonts[i].HasValue) - continue; - - var font = this.fonts[i]!.Value; - var fdt = this.fdts[i]; + var fdt = this.fdts[(int)style.FamilyAndSize]; var fontPtr = font.NativePtr; fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.LineHeight; fontPtr->Ascent = fdt.FontHeader.Ascent; @@ -301,75 +310,83 @@ namespace Dalamud.Interface.GameFonts } } - fixed (char* c = FontNames[i]) + fixed (char* c = FontNames[(int)style.FamilyAndSize]) { for (var j = 0; j < 40; j++) fontPtr->ConfigData->Name[j] = 0; - Encoding.UTF8.GetBytes(c, FontNames[i].Length, fontPtr->ConfigData->Name, 40); + Encoding.UTF8.GetBytes(c, FontNames[(int)style.FamilyAndSize].Length, fontPtr->ConfigData->Name, 40); } - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[i]) + foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) { var rc = io.Fonts.GetCustomRectByIndex(rectId); var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; var sourceBufferDelta = glyph.TextureChannelByteIndex; - for (var y = 0; y < glyph.BoundingHeight; y++) + var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); + if (widthAdjustment == 0) { - for (var x = 0; x < glyph.BoundingWidth; x++) + for (var y = 0; y < glyph.BoundingHeight; y++) { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc.Y + y) * width) + rc.X + x] = (uint)(a << 24) | 0xFFFFFFu; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; + pixels32[((rc.Y + y) * width) + rc.X + x] = (uint)(a << 24) | 0xFFFFFFu; + } + } + } + else + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) + pixels32[((rc.Y + y) * width) + rc.X + x] = 0xFFFFFFu; + } + + for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) + { + var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); + for (var y = 0; y < glyph.BoundingHeight; y++) + { + float xDelta = xbold; + if (style.SkewStrength > 0) + xDelta += style.SkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; + else if (style.SkewStrength < 0) + xDelta -= style.SkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; + var xDeltaInt = (int)Math.Floor(xDelta); + var xness = xDelta - xDeltaInt; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; + var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; + var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; + var n = (a1 * xness) + (a2 * (1 - xness)); + var targetOffset = ((rc.Y + y) * width) + rc.X + x + xDeltaInt; + pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); + } + } } } } } - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis96, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis12, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis14, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis18, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis36, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Jupiter16, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter20, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter23, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter45, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter46, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter90, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Meidinger16, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger20, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger40, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis96, GameFont.MiedingerMid10, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis12, GameFont.MiedingerMid12, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis14, GameFont.MiedingerMid14, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.MiedingerMid18, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.MiedingerMid36, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.TrumpGothic184, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic23, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic34, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic68, true, false); - - foreach (var font in this.fonts) - font?.BuildLookupTable(); + foreach (var font in this.fonts.Values) + { + CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); + font.BuildLookupTable(); + } } /// - /// Decrease font reference counter and release if nobody is using it. + /// Decrease font reference counter. /// - /// Font to release. - internal void DecreaseFontRef(GameFont gameFont) + /// Font to release. + internal void DecreaseFontRef(GameFontStyle style) { - var fontIndex = (int)gameFont; - var needRebuild = false; - lock (this.syncRoot) { - var prev = this.fontUseCounter[fontIndex] == 0; - this.fontUseCounter[fontIndex] -= 1; - needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + if ((this.fontUseCounter[style] -= 1) == 0) + this.fontUseCounter.Remove(style); } - - if (needRebuild) - this.interfaceManager.RebuildFonts(); } private struct ImFontGlyphReal diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs new file mode 100644 index 000000000..8a713f1cf --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Describes a font based on game resource file. + /// + public struct GameFontStyle + { + /// + /// Font family of the font. + /// + public GameFontFamilyAndSize FamilyAndSize; + + /// + /// Weight of the font. + /// + /// 0 is unaltered. + /// Any value greater than 0 will make it bolder. + /// + public float Weight; + + /// + /// Skewedness of the font. + /// + /// 0 is unaltered. + /// Greater than 1 will make upper part go rightwards. + /// Less than 1 will make lower part go rightwards. + /// + public float SkewStrength; + + /// + /// Initializes a new instance of the struct. + /// + /// Font family. + /// Size in points. + public GameFontStyle(GameFontFamily family, float size) + { + this.FamilyAndSize = GetRecommendedFamilyAndSize(family, size); + this.Weight = this.SkewStrength = 0f; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Font family and size. + public GameFontStyle(GameFontFamilyAndSize familyAndSize) + { + this.FamilyAndSize = familyAndSize; + this.Weight = this.SkewStrength = 0f; + } + + /// + /// Gets the font family. + /// + public GameFontFamily Family => this.FamilyAndSize switch + { + GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, + GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis12 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis14 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis18 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis36 => GameFontFamily.Axis, + GameFontFamilyAndSize.Jupiter16 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter20 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter23 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter45 => GameFontFamily.JupiterNumeric, + GameFontFamilyAndSize.Jupiter46 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter90 => GameFontFamily.JupiterNumeric, + GameFontFamilyAndSize.Meidinger16 => GameFontFamily.Meidinger, + GameFontFamilyAndSize.Meidinger20 => GameFontFamily.Meidinger, + GameFontFamilyAndSize.Meidinger40 => GameFontFamily.Meidinger, + GameFontFamilyAndSize.MiedingerMid10 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid12 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid14 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid18 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid36 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.TrumpGothic184 => GameFontFamily.TrumpGothic, + GameFontFamilyAndSize.TrumpGothic23 => GameFontFamily.TrumpGothic, + GameFontFamilyAndSize.TrumpGothic34 => GameFontFamily.TrumpGothic, + GameFontFamilyAndSize.TrumpGothic68 => GameFontFamily.TrumpGothic, + _ => throw new InvalidOperationException(), + }; + + /// + /// Gets the font size. + /// + public float Size => this.FamilyAndSize switch + { + GameFontFamilyAndSize.Undefined => 0, + GameFontFamilyAndSize.Axis96 => 9.6f, + GameFontFamilyAndSize.Axis12 => 12, + GameFontFamilyAndSize.Axis14 => 14, + GameFontFamilyAndSize.Axis18 => 18, + GameFontFamilyAndSize.Axis36 => 36, + GameFontFamilyAndSize.Jupiter16 => 16, + GameFontFamilyAndSize.Jupiter20 => 20, + GameFontFamilyAndSize.Jupiter23 => 23, + GameFontFamilyAndSize.Jupiter45 => 45, + GameFontFamilyAndSize.Jupiter46 => 46, + GameFontFamilyAndSize.Jupiter90 => 90, + GameFontFamilyAndSize.Meidinger16 => 16, + GameFontFamilyAndSize.Meidinger20 => 20, + GameFontFamilyAndSize.Meidinger40 => 40, + GameFontFamilyAndSize.MiedingerMid10 => 10, + GameFontFamilyAndSize.MiedingerMid12 => 12, + GameFontFamilyAndSize.MiedingerMid14 => 14, + GameFontFamilyAndSize.MiedingerMid18 => 18, + GameFontFamilyAndSize.MiedingerMid36 => 36, + GameFontFamilyAndSize.TrumpGothic184 => 18.4f, + GameFontFamilyAndSize.TrumpGothic23 => 23, + GameFontFamilyAndSize.TrumpGothic34 => 34, + GameFontFamilyAndSize.TrumpGothic68 => 8, + _ => throw new InvalidOperationException(), + }; + + /// + /// Gets or sets a value indicating whether this font is bold. + /// + public bool Bold + { + get => this.Weight > 0f; + set => this.Weight = value ? 1f : 0f; + } + + /// + /// Gets or sets a value indicating whether this font is italic. + /// + public bool Italic + { + get => this.SkewStrength != 0; + set => this.SkewStrength = value ? 4 : 0; + } + + /// + /// Gets the recommend GameFontFamilyAndSize given family and size. + /// + /// Font family. + /// Font size in points. + /// Recommended GameFontFamilyAndSize. + public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) + { + if (size <= 0) + return GameFontFamilyAndSize.Undefined; + + switch (family) + { + case GameFontFamily.Undefined: + return GameFontFamilyAndSize.Undefined; + + case GameFontFamily.Axis: + if (size <= 9.6) + return GameFontFamilyAndSize.Axis96; + else if (size <= 12) + return GameFontFamilyAndSize.Axis12; + else if (size <= 14) + return GameFontFamilyAndSize.Axis14; + else if (size <= 18) + return GameFontFamilyAndSize.Axis18; + else + return GameFontFamilyAndSize.Axis36; + + case GameFontFamily.Jupiter: + if (size <= 16) + return GameFontFamilyAndSize.Jupiter16; + else if (size <= 20) + return GameFontFamilyAndSize.Jupiter20; + else if (size <= 23) + return GameFontFamilyAndSize.Jupiter23; + else + return GameFontFamilyAndSize.Jupiter46; + + case GameFontFamily.JupiterNumeric: + if (size <= 45) + return GameFontFamilyAndSize.Jupiter45; + else + return GameFontFamilyAndSize.Jupiter90; + + case GameFontFamily.Meidinger: + if (size <= 16) + return GameFontFamilyAndSize.Meidinger16; + else if (size <= 20) + return GameFontFamilyAndSize.Meidinger20; + else + return GameFontFamilyAndSize.Meidinger40; + + case GameFontFamily.MiedingerMid: + if (size <= 10) + return GameFontFamilyAndSize.MiedingerMid10; + else if (size <= 12) + return GameFontFamilyAndSize.MiedingerMid12; + else if (size <= 14) + return GameFontFamilyAndSize.MiedingerMid14; + else if (size <= 18) + return GameFontFamilyAndSize.MiedingerMid18; + else + return GameFontFamilyAndSize.MiedingerMid36; + + case GameFontFamily.TrumpGothic: + if (size <= 18.4) + return GameFontFamilyAndSize.TrumpGothic184; + else if (size <= 23) + return GameFontFamilyAndSize.TrumpGothic23; + else if (size <= 34) + return GameFontFamilyAndSize.TrumpGothic34; + else + return GameFontFamilyAndSize.TrumpGothic68; + + default: + return GameFontFamilyAndSize.Undefined; + } + } + + /// + /// Calculates the adjustment to width resulting fron Weight and SkewStrength. + /// + /// Font information. + /// Glyph. + /// Width adjustment in pixel unit. + public int CalculateWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) + { + var widthDelta = this.Weight; + if (this.SkewStrength > 0) + widthDelta += 1f * this.SkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; + else if (this.SkewStrength < 0) + widthDelta -= 1f * this.SkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; + + return (int)Math.Ceiling(widthDelta); + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1e9654dc0..58f18bf70 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -57,8 +57,7 @@ namespace Dalamud.Interface.Internal private readonly SwapChainVtableResolver address; private RawDX11Scene? scene; - private GameFont overwriteDefaultFontFromGameFont = GameFont.Undefined; - private GameFontHandle? overwriteDefaultFontFromGameFontHandle; + private GameFontHandle? axisFontHandle; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -313,16 +312,7 @@ namespace Dalamud.Interface.Internal if (!this.isRebuildingFonts) { Log.Verbose("[FONT] RebuildFonts() trigger"); - var configuration = Service.Get(); - if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) - { - this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; - this.overwriteDefaultFontFromGameFontHandle?.Dispose(); - if (configuration.DefaultFontFromGame == GameFont.Undefined) - this.overwriteDefaultFontFromGameFontHandle = null; - else - this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); - } + this.SetAxisFonts(); this.isRebuildingFonts = true; this.scene.OnNewRenderFrame += this.RebuildFontsInternal; @@ -342,6 +332,26 @@ namespace Dalamud.Interface.Internal 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"); } + 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.axisFontHandle?.Dispose(); + this.axisFontHandle = Service.Get().NewFontRef(new(expectedFamilyAndSize)); + } + else + { + this.axisFontHandle?.Dispose(); + this.axisFontHandle = null; + } + } + /* * NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg. * Seems to work fine regardless, I guess, so whatever. @@ -403,15 +413,7 @@ namespace Dalamud.Interface.Internal this.scene.OnBuildUI += this.Display; this.scene.OnNewInputFrame += this.OnNewInputFrame; - if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) - { - this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; - this.overwriteDefaultFontFromGameFontHandle?.Dispose(); - if (configuration.DefaultFontFromGame == GameFont.Undefined) - this.overwriteDefaultFontFromGameFontHandle = null; - else - this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); - } + this.SetAxisFonts(); this.SetupFonts(); @@ -591,7 +593,7 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().Fonts.Build(); gameFontManager.AfterBuildFonts(); - GameFontManager.CopyGlyphsAcrossFonts(this.overwriteDefaultFontFromGameFontHandle?.Get(), DefaultFont, false, true); + GameFontManager.CopyGlyphsAcrossFonts(this.axisFontHandle?.ImFont, DefaultFont, false, true); Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); this.AfterBuildFonts?.Invoke(); diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 6fa6df808..ac01392a7 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -28,8 +28,6 @@ namespace Dalamud.Interface.Internal.Windows private const float MinScale = 0.3f; private const float MaxScale = 2.0f; - private readonly List validFontChoices; - private readonly string[] validFontNames; private readonly string[] languages; private readonly string[] locLanguages; private int langIndex; @@ -40,6 +38,7 @@ namespace Dalamud.Interface.Internal.Windows private bool doCfChatMessage; private float globalUiScale; + private bool doUseAxisFontsFromGame; private bool doToggleUiHide; private bool doToggleUiHideDuringCutscenes; private bool doToggleUiHideDuringGpose; @@ -69,8 +68,6 @@ namespace Dalamud.Interface.Internal.Windows private bool doButtonsSystemMenu; private bool disableRmtFiltering; - private int validFontIndex; - #region Experimental private bool doPluginTest; @@ -94,6 +91,7 @@ namespace Dalamud.Interface.Internal.Windows this.doCfChatMessage = configuration.DutyFinderChatMessage; this.globalUiScale = configuration.GlobalUiScale; + this.doUseAxisFontsFromGame = configuration.UseAxisFontsFromGame; this.doToggleUiHide = configuration.ToggleUiHide; this.doToggleUiHideDuringCutscenes = configuration.ToggleUiHideDuringCutscenes; this.doToggleUiHideDuringGpose = configuration.ToggleUiHideDuringGpose; @@ -116,10 +114,6 @@ namespace Dalamud.Interface.Internal.Windows this.doButtonsSystemMenu = configuration.DoButtonsSystemMenu; this.disableRmtFiltering = configuration.DisableRmtFiltering; - this.validFontChoices = Enum.GetValues().Where(x => x == GameFont.Undefined || GameFontManager.IsGenericPurposeFont(x)).ToList(); - this.validFontNames = this.validFontChoices.Select(x => GameFontManager.DescribeFont(x)).ToArray(); - this.validFontIndex = Math.Max(0, this.validFontChoices.IndexOf(configuration.DefaultFontFromGame)); - this.languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); try { @@ -286,20 +280,19 @@ namespace Dalamud.Interface.Internal.Windows { this.globalUiScale = 1.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; + Service.Get().RebuildFonts(); } if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f")) + { ImGui.GetIO().FontGlobalScale = this.globalUiScale; + Service.Get().RebuildFonts(); + } ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays.")); ImGuiHelpers.ScaledDummy(10, 16); - ImGui.Text(Loc.Localize("DalamudSettingsGlobalFont", "Global Font")); - ImGui.Combo("##DalamudSettingsGlobalFontDrag", ref this.validFontIndex, this.validFontNames, this.validFontNames.Length); - - ImGuiHelpers.ScaledDummy(10, 16); - if (ImGui.Button(Loc.Localize("DalamudSettingsOpenStyleEditor", "Open Style Editor"))) { Service.Get().OpenStyleEditor(); @@ -311,6 +304,9 @@ namespace Dalamud.Interface.Internal.Windows ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")); + ImGui.Checkbox(Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"), ref this.doUseAxisFontsFromGame); + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font.")); + ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHide", "Hide plugin UI when the game UI is toggled off"), ref this.doToggleUiHide); ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideHint", "Hide any open windows by plugins when toggling the game overlay.")); @@ -814,7 +810,7 @@ namespace Dalamud.Interface.Internal.Windows configuration.IsFocusManagementEnabled = this.doFocus; configuration.ShowTsm = this.doTsm; - configuration.DefaultFontFromGame = this.validFontChoices[this.validFontIndex]; + configuration.UseAxisFontsFromGame = this.doUseAxisFontsFromGame; // This is applied every frame in InterfaceManager::CheckViewportState() configuration.IsDisableViewport = !this.doViewport; @@ -858,9 +854,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.Save(); - Service.Get().RebuildFonts(); - _ = Service.Get().ReloadPluginMastersAsync(); + Service.Get().RebuildFonts(); } } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index c57a130b4..b4c089fbe 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Numerics; @@ -8,6 +8,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Windowing; using ImGuiNET; using ImGuiScene; @@ -19,6 +20,7 @@ namespace Dalamud.Interface.Internal.Windows /// internal class TitleScreenMenuWindow : Window, IDisposable { + private const float TargetFontSize = 16.2f; private readonly TextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); @@ -27,6 +29,8 @@ namespace Dalamud.Interface.Internal.Windows private InOutCubic? fadeOutEasing; + private GameFontHandle? axisFontHandle; + private State state = State.Hide; /// @@ -67,14 +71,19 @@ 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(); } @@ -90,128 +99,143 @@ namespace Dalamud.Interface.Internal.Windows /// public override void Draw() { - ImGui.SetWindowFontScale(1.3f); + ImGui.SetWindowFontScale(TargetFontSize / ImGui.GetFont().FontSize * 4 / 3); var tsm = Service.Get(); switch (this.state) { case State.Show: - { - for (var i = 0; i < tsm.Entries.Count; i++) { - var entry = tsm.Entries[i]; - - if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) + for (var i = 0; i < tsm.Entries.Count; i++) { - moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400)); - this.moveEasings.Add(entry.Id, moveEasing); + var entry = tsm.Entries[i]; + + if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) + { + moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400)); + this.moveEasings.Add(entry.Id, moveEasing); + } + + if (!moveEasing.IsRunning && !moveEasing.IsDone) + { + moveEasing.Restart(); + } + + if (moveEasing.IsDone) + { + moveEasing.Stop(); + } + + moveEasing.Update(); + + var finalPos = (i + 1) * this.shadeTexture.Height; + var pos = moveEasing.Value * finalPos; + + // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. + if (moveEasing.IsDone) + { + pos = finalPos; + } + + this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true); + + var cursor = ImGui.GetCursorPos(); + cursor.Y = (float)pos; + ImGui.SetCursorPos(cursor); } - if (!moveEasing.IsRunning && !moveEasing.IsDone) + if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenOverlapped | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) { - moveEasing.Restart(); + this.state = State.FadeOut; } - if (moveEasing.IsDone) - { - moveEasing.Stop(); - } - - moveEasing.Update(); - - var finalPos = (i + 1) * this.shadeTexture.Height; - var pos = moveEasing.Value * finalPos; - - // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. - if (moveEasing.IsDone) - { - pos = finalPos; - } - - this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true); - - var cursor = ImGui.GetCursorPos(); - cursor.Y = (float)pos; - ImGui.SetCursorPos(cursor); + break; } - if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | - ImGuiHoveredFlags.AllowWhenOverlapped | - ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) - { - this.state = State.FadeOut; - } - - break; - } - case State.FadeOut: - { - this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400)) { - IsInverse = true, - }; + this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400)) + { + IsInverse = true, + }; - if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone) - { - this.fadeOutEasing.Restart(); + if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone) + { + this.fadeOutEasing.Restart(); + } + + if (this.fadeOutEasing.IsDone) + { + this.fadeOutEasing.Stop(); + } + + this.fadeOutEasing.Update(); + + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value); + + for (var i = 0; i < tsm.Entries.Count; i++) + { + var entry = tsm.Entries[i]; + + var finalPos = (i + 1) * this.shadeTexture.Height; + + this.DrawEntry(entry, i != 0, true, i == 0, false); + + var cursor = ImGui.GetCursorPos(); + cursor.Y = finalPos; + ImGui.SetCursorPos(cursor); + } + + ImGui.PopStyleVar(); + + var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenOverlapped | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + + if (!isHover && this.fadeOutEasing!.IsDone) + { + this.state = State.Hide; + this.fadeOutEasing = null; + } + else if (isHover) + { + this.state = State.Show; + this.fadeOutEasing = null; + } + + break; } - if (this.fadeOutEasing.IsDone) - { - this.fadeOutEasing.Stop(); - } - - this.fadeOutEasing.Update(); - - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value); - - for (var i = 0; i < tsm.Entries.Count; i++) - { - var entry = tsm.Entries[i]; - - var finalPos = (i + 1) * this.shadeTexture.Height; - - this.DrawEntry(entry, i != 0, true, i == 0, false); - - var cursor = ImGui.GetCursorPos(); - cursor.Y = finalPos; - ImGui.SetCursorPos(cursor); - } - - ImGui.PopStyleVar(); - - var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | - ImGuiHoveredFlags.AllowWhenOverlapped | - ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); - - if (!isHover && this.fadeOutEasing!.IsDone) - { - this.state = State.Hide; - this.fadeOutEasing = null; - } - else if (isHover) - { - this.state = State.Show; - this.fadeOutEasing = null; - } - - break; - } - case State.Hide: - { - if (this.DrawEntry(tsm.Entries[0], true, false, true, true)) { - this.state = State.Show; - } + if (this.DrawEntry(tsm.Entries[0], true, false, true, true)) + { + this.state = State.Show; + } - this.moveEasings.Clear(); - this.logoEasings.Clear(); - this.shadeEasings.Clear(); - break; - } + this.moveEasings.Clear(); + this.logoEasings.Clear(); + this.shadeEasings.Clear(); + break; + } + } + } + + private void SetAxisFonts() + { + var configuration = Service.Get(); + if (configuration.UseAxisFontsFromGame) + { + if (this.axisFontHandle == null) + this.axisFontHandle = Service.Get().NewFontRef(new(GameFontFamily.Axis, TargetFontSize)); + } + else + { + this.axisFontHandle?.Dispose(); + this.axisFontHandle = null; } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 35c82102b..e3f85426c 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -215,9 +215,9 @@ namespace Dalamud.Interface /// /// Gets a game font. /// - /// Font to get. + /// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFont gameFont) => Service.Get().NewFontRef(gameFont); + public GameFontHandle GetGameFontHandle(GameFontStyle style) => Service.Get().NewFontRef(style); /// /// Call this to queue a rebuild of the font atlas.
From 3026eb6fa8577a78758fa23f70a69ad055b8cfd7 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 25 Feb 2022 12:44:24 +0900 Subject: [PATCH 20/22] Add kerned text draw helper --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 54 ++- .../Interface/GameFonts/GameFontLayoutPlan.cs | 414 ++++++++++++++++++ .../Interface/GameFonts/GameFontManager.cs | 2 +- 3 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 69e754727..a50941883 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,5 +1,5 @@ using System; - +using System.Numerics; using ImGuiNET; namespace Dalamud.Interface.GameFonts @@ -43,7 +43,59 @@ namespace Dalamud.Interface.GameFonts ///
public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); + /// + /// Creates a new GameFontLayoutPlan.Builder. + /// + /// Text. + /// A new builder for GameFontLayoutPlan. + public GameFontLayoutPlan.Builder LayoutBuilder(string text) + { + return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); + } + /// public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + + /// + /// Draws text. + /// + /// Text to draw. + public void Text(string text) + { + if (!this.Available) + { + ImGui.TextUnformatted(text); + } + else + { + this.LayoutBuilder(text) + .Build() + .Draw(ImGui.GetWindowDrawList(), ImGui.GetWindowPos() + ImGui.GetCursorPos(), ImGui.GetColorU32(ImGuiCol.Text)); + } + } + + /// + /// Draws text in given color. + /// + /// Color. + /// Text to draw. + public void TextColored(Vector4 col, string text) + { + ImGui.PushStyleColor(ImGuiCol.Text, col); + this.Text(text); + ImGui.PopStyleColor(); + } + + /// + /// Draws disabled text. + /// + /// Text to draw. + public void TextDisabled(string text) + { + unsafe + { + this.TextColored(*ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled), text); + } + } } } diff --git a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs new file mode 100644 index 000000000..dc2d5f380 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +using ImGuiNET; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Plan on how glyphs will be rendered. + /// + public class GameFontLayoutPlan + { + /// + /// Horizontal alignment. + /// + public enum HorizontalAlignment + { + /// + /// Align to left. + /// + Left, + + /// + /// Align to center. + /// + Center, + + /// + /// Align to right. + /// + Right, + } + + /// + /// Gets the associated ImFontPtr. + /// + public ImFontPtr ImFontPtr { get; internal set; } + + /// + /// Gets the size in points of the text. + /// + public float Size { get; internal set; } + + /// + /// Gets the x offset of the leftmost glyph. + /// + public float X { get; internal set; } + + /// + /// Gets the width of the text. + /// + public float Width { get; internal set; } + + /// + /// Gets the height of the text. + /// + public float Height { get; internal set; } + + /// + /// Gets the list of plannen elements. + /// + public IList Elements { get; internal set; } + + /// + /// Draws font to ImGui. + /// + /// Target ImDrawList. + /// Position. + /// 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) + continue; + + this.ImFontPtr.RenderChar( + drawListPtr, + this.Size, + new Vector2( + this.X + pos.X + element.X, + pos.Y + element.Y), + col, + element.Glyph.Char); + } + } + + /// + /// Plan on how each glyph will be rendered. + /// + public class Element + { + /// + /// Gets the original codepoint. + /// + public int Codepoint { get; init; } + + /// + /// Gets the corresponding or fallback glyph. + /// + public FdtReader.FontTableEntry Glyph { get; init; } + + /// + /// Gets the X offset of this glyph. + /// + public float X { get; internal set; } + + /// + /// Gets the Y offset of this glyph. + /// + public float Y { get; internal set; } + + /// + /// Gets a value indicating whether whether this codepoint is a control character. + /// + public bool IsControl + { + get + { + return this.Codepoint < 0x10000 && char.IsControl((char)this.Codepoint); + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a space. + /// + public bool IsSpace + { + get + { + return this.Codepoint < 0x10000 && char.IsWhiteSpace((char)this.Codepoint); + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a line break character. + /// + public bool IsLineBreak + { + get + { + return this.Codepoint == '\n' || this.Codepoint == '\r'; + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a chinese character. + /// + public bool IsChineseCharacter + { + get + { + // CJK Symbols and Punctuation(〇) + if (this.Codepoint >= 0x3007 && this.Codepoint <= 0x3007) + return true; + + // CJK Unified Ideographs Extension A + if (this.Codepoint >= 0x3400 && this.Codepoint <= 0x4DBF) + return true; + + // CJK Unified Ideographs + if (this.Codepoint >= 0x4E00 && this.Codepoint <= 0x9FFF) + return true; + + // CJK Unified Ideographs Extension B + if (this.Codepoint >= 0x20000 && this.Codepoint <= 0x2A6DF) + return true; + + // CJK Unified Ideographs Extension C + if (this.Codepoint >= 0x2A700 && this.Codepoint <= 0x2B73F) + return true; + + // CJK Unified Ideographs Extension D + if (this.Codepoint >= 0x2B740 && this.Codepoint <= 0x2B81F) + return true; + + // CJK Unified Ideographs Extension E + if (this.Codepoint >= 0x2B820 && this.Codepoint <= 0x2CEAF) + return true; + + // CJK Unified Ideographs Extension F + if (this.Codepoint >= 0x2CEB0 && this.Codepoint <= 0x2EBEF) + return true; + + return false; + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a good position to break word after. + /// + public bool IsWordBreakPoint + { + get + { + if (this.IsChineseCharacter) + return true; + + if (this.Codepoint >= 0x10000) + return false; + + // TODO: Whatever + switch (char.GetUnicodeCategory((char)this.Codepoint)) + { + case System.Globalization.UnicodeCategory.SpaceSeparator: + case System.Globalization.UnicodeCategory.LineSeparator: + case System.Globalization.UnicodeCategory.ParagraphSeparator: + case System.Globalization.UnicodeCategory.Control: + case System.Globalization.UnicodeCategory.Format: + case System.Globalization.UnicodeCategory.Surrogate: + case System.Globalization.UnicodeCategory.PrivateUse: + case System.Globalization.UnicodeCategory.ConnectorPunctuation: + case System.Globalization.UnicodeCategory.DashPunctuation: + case System.Globalization.UnicodeCategory.OpenPunctuation: + case System.Globalization.UnicodeCategory.ClosePunctuation: + case System.Globalization.UnicodeCategory.InitialQuotePunctuation: + case System.Globalization.UnicodeCategory.FinalQuotePunctuation: + case System.Globalization.UnicodeCategory.OtherPunctuation: + case System.Globalization.UnicodeCategory.MathSymbol: + case System.Globalization.UnicodeCategory.ModifierSymbol: + case System.Globalization.UnicodeCategory.OtherSymbol: + case System.Globalization.UnicodeCategory.OtherNotAssigned: + return true; + } + + return false; + } + } + } + + /// + /// Build a GameFontLayoutPlan. + /// + public class Builder + { + private readonly ImFontPtr fontPtr; + private readonly FdtReader fdt; + private readonly string text; + private int maxWidth = int.MaxValue; + private float size; + private HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left; + + /// + /// Initializes a new instance of the class. + /// + /// Corresponding ImFontPtr. + /// FDT file to base on. + /// Text. + public Builder(ImFontPtr fontPtr, FdtReader fdt, string text) + { + this.fontPtr = fontPtr; + this.fdt = fdt; + this.text = text; + this.size = fdt.FontHeader.LineHeight; + } + + /// + /// Sets the size of resulting text. + /// + /// Size in pixels. + /// This. + public Builder WithSize(float size) + { + this.size = size; + return this; + } + + /// + /// Sets the maximum width of the text. + /// + /// Maximum width in pixels. + /// This. + public Builder WithMaxWidth(int maxWidth) + { + this.maxWidth = maxWidth; + return this; + } + + /// + /// Sets the horizontal alignment of the text. + /// + /// Horizontal alignment. + /// This. + public Builder WithHorizontalAlignment(HorizontalAlignment horizontalAlignment) + { + this.horizontalAlignment = horizontalAlignment; + return this; + } + + /// + /// Builds the layout plan. + /// + /// Newly created layout plan. + public GameFontLayoutPlan Build() + { + var scale = this.size / this.fdt.FontHeader.LineHeight; + var unscaledMaxWidth = (float)Math.Ceiling(this.maxWidth / scale); + var elements = new List(); + foreach (var c in this.text) + elements.Add(new() { Codepoint = c, Glyph = this.fdt.GetGlyph(c), }); + + var lastBreakIndex = 0; + List lineBreakIndices = new() { 0 }; + for (var i = 1; i < elements.Count; i++) + { + var prev = elements[i - 1]; + var curr = elements[i]; + + if (prev.IsLineBreak) + { + curr.X = 0; + curr.Y = prev.Y + this.fdt.FontHeader.LineHeight; + lineBreakIndices.Add(i); + } + else + { + curr.X = prev.X + prev.Glyph.NextOffsetX + prev.Glyph.BoundingWidth + this.fdt.GetDistance(prev.Codepoint, curr.Codepoint); + curr.Y = prev.Y; + } + + if (prev.IsWordBreakPoint) + lastBreakIndex = i; + + if (curr.IsSpace) + continue; + + if (curr.X + curr.Glyph.BoundingWidth < unscaledMaxWidth) + continue; + + if (!prev.IsSpace && elements[lastBreakIndex].X > 0) + { + prev = elements[lastBreakIndex - 1]; + curr = elements[lastBreakIndex]; + i = lastBreakIndex; + } + else + { + lastBreakIndex = i; + } + + curr.X = 0; + curr.Y = prev.Y + this.fdt.FontHeader.LineHeight; + lineBreakIndices.Add(i); + } + + lineBreakIndices.Add(elements.Count); + + var targetX = 0f; + var targetWidth = 0f; + var targetHeight = 0f; + for (var i = 1; i < lineBreakIndices.Count; i++) + { + var from = lineBreakIndices[i - 1]; + var to = lineBreakIndices[i]; + while (to > from && elements[to - 1].IsSpace) + { + to--; + } + + if (from >= to) + continue; + + var right = 0f; + for (var j = from; j < to; j++) + { + var e = elements[j]; + right = Math.Max(right, e.X + Math.Max(e.Glyph.BoundingWidth, e.Glyph.AdvanceWidth)); + targetHeight = Math.Max(targetHeight, e.Y + e.Glyph.BoundingHeight); + } + + targetWidth = Math.Max(targetWidth, right - elements[from].X); + float offsetX; + if (this.horizontalAlignment == HorizontalAlignment.Center) + offsetX = (unscaledMaxWidth - right) / 2; + else if (this.horizontalAlignment == HorizontalAlignment.Right) + offsetX = unscaledMaxWidth - right; + else if (this.horizontalAlignment == HorizontalAlignment.Left) + offsetX = 0; + else + throw new ArgumentException("Invalid horizontal alignment"); + for (var j = from; j < to; j++) + elements[j].X += offsetX; + targetX = i == 1 ? elements[from].X : Math.Min(targetX, elements[from].X); + } + + targetHeight = Math.Max(targetHeight, this.fdt.FontHeader.LineHeight * (lineBreakIndices.Count - 1)); + + targetWidth *= scale; + targetHeight *= scale; + targetX *= scale; + foreach (var e in elements) + { + e.X *= scale; + e.Y *= scale; + } + + return new GameFontLayoutPlan() + { + ImFontPtr = this.fontPtr, + Size = this.size, + X = targetX, + Width = targetWidth, + Height = targetHeight, + Elements = elements, + }; + } + } + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 34d385bba..44772bc48 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -183,7 +183,7 @@ namespace Dalamud.Interface.GameFonts { var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0); var newValue = this.fontUseCounter[style] = prevValue + 1; - needRebuild = (prevValue == 0) != (newValue == 0); + needRebuild = (prevValue == 0) != (newValue == 0) && !this.fonts.ContainsKey(style); } if (needRebuild) From fa4c57c8861b29b5b9d0e5c2aef27f71434c2a09 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Feb 2022 14:30:53 +0100 Subject: [PATCH 21/22] Add two customization points for the window system that are executed always and that can control whether the window is drawn without toggling state. --- Dalamud/Interface/Windowing/Window.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index ff0a7b958..e1333e072 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -121,6 +121,28 @@ namespace Dalamud.Interface.Windowing this.IsOpen ^= true; } + /// + /// Code to always be executed before the open-state of the window is checked. + /// + public virtual void PreOpenCheck() + { + } + + /// + /// Additional conditions for the window to be drawn, regardless of its open-state. + /// + /// + /// True if the window should be drawn, false otherwise. + /// + /// + /// Not being drawn due to failing this condition will not change focus or trigger OnClose. + /// This is checked before PreDraw, but after Update. + /// + public virtual bool DrawConditions() + { + return true; + } + /// /// Code to be executed before conditionals are applied and the window is drawn. /// @@ -170,6 +192,8 @@ namespace Dalamud.Interface.Windowing ///
internal void DrawInternal() { + this.PreOpenCheck(); + if (!this.IsOpen) { if (this.internalIsOpen != this.internalLastIsOpen) @@ -184,6 +208,8 @@ namespace Dalamud.Interface.Windowing } this.Update(); + if (!this.DrawConditions()) + return; var hasNamespace = !string.IsNullOrEmpty(this.Namespace); From 1b4e3a913c5d2e06e386d1c45aa957caa38cdc79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Feb 2022 14:31:12 +0100 Subject: [PATCH 22/22] fix: random comment typos --- Dalamud/ClientLanguageExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/ClientLanguageExtensions.cs b/Dalamud/ClientLanguageExtensions.cs index dccefb93f..abfba3ad5 100644 --- a/Dalamud/ClientLanguageExtensions.cs +++ b/Dalamud/ClientLanguageExtensions.cs @@ -10,8 +10,8 @@ namespace Dalamud /// /// Converts a Dalamud ClientLanguage to the corresponding Lumina variant. /// - /// Langauge to convert. - /// Converted langauge. + /// Language to convert. + /// Converted language. public static Lumina.Data.Language ToLumina(this ClientLanguage language) { return language switch